什么是依赖注入的 Pythonic 方式?

What is a Pythonic way for Dependency Injection?

简介

对于 Java,依赖注入作为纯 OOP 工作,即您提供一个要实现的接口,并在您的框架代码中接受一个 class 实例来实现定义的接口。

现在对于 Python,您可以采用相同的方式,但我认为对于 Python,该方法开销太大。那么你将如何以 Pythonic 方式实现它?

用例

假设这是框架代码:

class FrameworkClass():
    def __init__(self, ...):
        ...

    def do_the_job(self, ...):
        # some stuff
        # depending on some external function

基本方法

最天真的(也许是最好的?)方法是要求将外部函数提供给 FrameworkClass 构造函数,然后从 do_the_job 方法中调用。

框架代码:

class FrameworkClass():
    def __init__(self, func):
        self.func = func

    def do_the_job(self, ...):
        # some stuff
        self.func(...)

客户代码:

def my_func():
    # my implementation

framework_instance = FrameworkClass(my_func)
framework_instance.do_the_job(...)

问题

问题很简短。有没有更好的常用 Pythonic 方法来做到这一点?或者可能有支持此类功能的库?

更新:具体情况

假设我开发了一个微型 Web 框架,它使用令牌处理身份验证。该框架需要一个函数来提供从令牌中获得的一些 ID 并获取与该 ID.

对应的用户

显然,该框架对用户或任何其他应用程序特定逻辑一无所知,因此客户端代码必须将用户 getter 功能注入到框架中才能使身份验证工作。

我们在项目中进行依赖注入的方式是使用inject lib. Check out the documentation。我强烈建议将它用于 DI。只有一个功能有点没有意义,但当您必须管理多个数据源等时就开始变得很有意义了。

按照你的例子,它可能类似于:

# framework.py
class FrameworkClass():
    def __init__(self, func):
        self.func = func

    def do_the_job(self):
        # some stuff
        self.func()

您的自定义函数:

# my_stuff.py
def my_func():
    print('aww yiss')

您想在应用程序中的某处创建一个 bootstrap 文件来跟踪所有已定义的依赖项:

# bootstrap.py
import inject
from .my_stuff import my_func

def configure_injection(binder):
    binder.bind(FrameworkClass, FrameworkClass(my_func))

inject.configure(configure_injection)

然后您可以这样使用代码:

# some_module.py (has to be loaded with bootstrap.py already loaded somewhere in your app)
import inject
from .framework import FrameworkClass

framework_instance = inject.instance(FrameworkClass)
framework_instance.do_the_job()

我担心这是它所能得到的 pythonic(该模块有一些 python 甜头,比如装饰器可以通过参数等注入 - 检查文档),因为 python 没有接口或类型提示等花哨的东西。

所以直接回答你的问题会非常困难。我认为真正的问题是:python 对 DI 有一些原生支持吗?遗憾的是,答案是:没有。

有关如何使用超继承和多重继承而不是 DI 的争论,请参阅 Raymond Hettinger - Super considered super! - PyCon 2015。如果您没有时间观看整个视频,请跳至第 15 分钟(但我建议观看全部)。

下面是一个示例,说明如何将此视频中描述的内容应用到您的示例中:

框架代码:

class TokenInterface():
    def getUserFromToken(self, token):
        raise NotImplementedError

class FrameworkClass(TokenInterface):
    def do_the_job(self, ...):
        # some stuff
        self.user = super().getUserFromToken(...)

客户代码:

class SQLUserFromToken(TokenInterface):
    def getUserFromToken(self, token):      
        # load the user from the database
        return user

class ClientFrameworkClass(FrameworkClass, SQLUserFromToken):
    pass

framework_instance = ClientFrameworkClass()
framework_instance.do_the_job(...)

这将起作用,因为 Python MRO 将保证调用 getUserFromToken 客户端方法(如果使用 super())。如果您使用 Python 2.x.

,则必须更改代码

这里的一个额外好处是,如果客户端未提供实现,这将引发异常。

当然,这并不是真正的依赖注入,它是多重继承和混入,但它是解决您问题的 Pythonic 方法。

我认为 DI 和可能的 AOP 通常不被视为 Pythonic,因为典型的 Python 开发人员偏好,而不是语言特性。

事实上,您可以使用元classes 和 class 装饰器来实现 a basic DI framework in <100 lines

对于侵入性较小的解决方案,这些构造可用于将自定义实现插入通用框架。

前段时间我编写了依赖注入微框架,并希望将其变成 Pythonic - Dependency Injector。这就是您的代码在使用时的样子:

"""Example of dependency injection in Python."""

import logging
import sqlite3

import boto.s3.connection

import example.main
import example.services

import dependency_injector.containers as containers
import dependency_injector.providers as providers


class Platform(containers.DeclarativeContainer):
    """IoC container of platform service providers."""

    logger = providers.Singleton(logging.Logger, name='example')

    database = providers.Singleton(sqlite3.connect, ':memory:')

    s3 = providers.Singleton(boto.s3.connection.S3Connection,
                             aws_access_key_id='KEY',
                             aws_secret_access_key='SECRET')


class Services(containers.DeclarativeContainer):
    """IoC container of business service providers."""

    users = providers.Factory(example.services.UsersService,
                              logger=Platform.logger,
                              db=Platform.database)

    auth = providers.Factory(example.services.AuthService,
                             logger=Platform.logger,
                             db=Platform.database,
                             token_ttl=3600)

    photos = providers.Factory(example.services.PhotosService,
                               logger=Platform.logger,
                               db=Platform.database,
                               s3=Platform.s3)


class Application(containers.DeclarativeContainer):
    """IoC container of application component providers."""

    main = providers.Callable(example.main.main,
                              users_service=Services.users,
                              auth_service=Services.auth,
                              photos_service=Services.photos)

这里 link 对此示例进行了更广泛的描述 - http://python-dependency-injector.ets-labs.org/examples/services_miniapp.html

希望对您有所帮助。欲了解更多信息,请访问:

由于 Python OOP 实现,IoC 和依赖注入不是 Python 世界的标准做法。但即使对于 Python.

,该方法似乎也很有希望
  • 使用依赖项作为参数是一种非pythonic 方法。 Python 是一种 OOP 语言,具有优美优雅的 OOP 模型,提供更直接的方式来维护依赖关系。
  • 为了模仿接口类型定义类满满的抽象方法也很奇怪
  • 大量包装器对包装器的解决方法会产生代码开销。
  • 当我只需要一个小图案时,我也不喜欢使用库。

所以我的 solution 是:

# Framework internal
def MetaIoC(name, bases, namespace):
    cls = type("IoC{}".format(name), tuple(), namespace)
    return type(name, bases + (cls,), {})


# Entities level                                        
class Entity:
    def _lower_level_meth(self):
        raise NotImplementedError

    @property
    def entity_prop(self):
        return super(Entity, self)._lower_level_meth()


# Adapters level
class ImplementedEntity(Entity, metaclass=MetaIoC):          
    __private = 'private attribute value'                    

    def __init__(self, pub_attr):                            
        self.pub_attr = pub_attr                             

    def _lower_level_meth(self):                             
        print('{}\n{}'.format(self.pub_attr, self.__private))


# Infrastructure level                                       
if __name__ == '__main__':                                   
    ENTITY = ImplementedEntity('public attribute value')     
    ENTITY.entity_prop         

编辑:

小心模式。我在一个真实的项目中使用过它,但它表现出一种不太好的方式。 My post on Medium about my experience with the pattern.

还有 Pinject,一个由 Google 开发的开源 python 依赖注入器。

这是一个例子

>>> class OuterClass(object):
...     def __init__(self, inner_class):
...         self.inner_class = inner_class
...
>>> class InnerClass(object):
...     def __init__(self):
...         self.forty_two = 42
...
>>> obj_graph = pinject.new_object_graph()
>>> outer_class = obj_graph.provide(OuterClass)
>>> print outer_class.inner_class.forty_two
42

here is the source code

importlib 是一种非常简单且符合 Pythonic 的依赖注入方法。

你可以定义一个小的效用函数

def inject_method_from_module(modulename, methodname):
    """
    injects dynamically a method in a module
    """
    mod = importlib.import_module(modulename)
    return getattr(mod, methodname, None)

然后就可以使用了:

myfunction = inject_method_from_module("mypackage.mymodule", "myfunction")
myfunction("a")

在 mypackage/mymodule.py 中定义 myfunction

def myfunction(s):
    print("myfunction in mypackage.mymodule called with parameter:", s)

您当然也可以使用 class MyClass iso。函数我的函数。如果在 settings.py 文件中定义 methodname 的值,则可以根据设置文件的值加载不同版本的 methodname。 Django 就是使用这样的方案来定义它的数据库连接。

依赖注入是一种Python直接支持的简单技术。不需要额外的库。使用 type hints 可以提高清晰度和可读性。

框架代码:

class UserStore():
    """
    The base class for accessing a user's information.
    The client must extend this class and implement its methods.
    """
    def get_name(self, token):
        raise NotImplementedError

class WebFramework():
    def __init__(self, user_store: UserStore):
        self.user_store = user_store

    def greet_user(self, token):
        user_name = self.user_store.get_name(token)
        print(f'Good day to you, {user_name}!')

客户代码:

class AlwaysMaryUser(UserStore):
    def get_name(self, token):      
        return 'Mary'

class SQLUserStore(UserStore):
    def __init__(self, db_params):
        self.db_params = db_params

    def get_name(self, token):
        # TODO: Implement the database lookup
        raise NotImplementedError

client = WebFramework(AlwaysMaryUser())
client.greet_user('user_token')

UserStore class 和类型提示不是实现依赖注入所必需的。它们的主要目的是为客户端开发人员提供指导。如果您删除 UserStore class 和对它的所有引用,代码仍然有效。

在尝试了 python 中的一些 DI 框架之后,我发现在比较它在其他领域(例如 .NET Core)中的简单性时,它们使用起来感觉有点笨拙。这主要是由于通过诸如装饰器之类的东西进行连接,这些东西使代码混乱并且难以简单地将其添加到项目中或从项目中删除它,或者基于变量名进行连接。

我最近一直在研究依赖注入框架,该框架使用类型注释来进行名为 Simple-Injection 的注入。下面是一个简单的例子

from simple_injection import ServiceCollection


class Dependency:
    def hello(self):
        print("Hello from Dependency!")

class Service:
    def __init__(self, dependency: Dependency):
        self._dependency = dependency

    def hello(self):
        self._dependency.hello()

collection = ServiceCollection()
collection.add_transient(Dependency)
collection.add_transient(Service)

collection.resolve(Service).hello()
# Outputs: Hello from Dependency!

此库支持服务生命周期和将服务绑定到实现。

这个库的目标之一是它也很容易将它添加到现有的应用程序,并在提交之前看看你喜欢它,因为它只需要你的应用程序有适当的类型,然后你在入口点构建依赖关系图 运行 它。

希望这对您有所帮助。更多信息,请查看