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

Posted

技术标签:

【中文标题】什么是依赖注入的 Pythonic 方式?【英文标题】:What is a Pythonic way for Dependency Injection? 【发布时间】:2015-10-19 03:47:06 【问题描述】:

简介

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

现在,对于 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 功能注入框架以使身份验证工作。

【问题讨论】:

你为什么不“提供一个要实现的接口,并在你的框架代码中接受一个实现定义接口的类的实例”?在 Python 中,您可以以 EAFP 样式执行此操作(即假设它满足该接口,否则会引发 AttributeErrorTypeError),但除此之外它是相同的。 使用absABCMeta 元类和@abstractmethod 装饰器很容易做到这一点,无需手动验证。只是想获得几个选项和建议。您引用的那个是最干净的,但我认为开销更大。 那我不知道你想问什么问题。 好的,换个说法我试试。问题很清楚。问题是如何以 Pythonic 方式做到这一点。 选项1:您引用的方式,选项2基本方法我在问题中描述。所以问题是,还有其他 Pythonic 方法吗? "There should be one-- and preferably only one --obvious way to do it." 【参考方案1】:

我们在项目中进行依赖注入的方式是使用inject lib。查看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.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 有一些原生支持?遗憾的是,答案是:不。

【讨论】:

谢谢你的回答,看起来很有趣。我将检查装饰器部分。同时,让我们等待更多的答案。 感谢“inject”库的链接。这是迄今为止我发现的最接近我希望由 DI 填补的空白 - 并且奖励,它实际上正在维护中!【参考方案2】:

请参阅Raymond Hettinger - Super considered super! - PyCon 2015,了解有关如何使用超级继承和多重继承而不是 DI 的争论。如果您没有时间观看整个视频,请跳到第 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 方式。

【讨论】:

这个答案被认为是super() :) Raymond 称它为 CI,而我认为它是一个纯粹的 mixin。但会不会是 Python 中的 mixin 和 CI 几乎是一样的?唯一的区别是不敬的程度。 Mixin 将依赖注入到类级别,而 CI 将依赖注入到实例中。 我认为构造函数级别的注入无论如何在 Python 中都很容易做到,就像 OP 描述的那样。这种pythonic方式看起来很有趣。它只需要比简单的构造函数注入 IMO 更多的布线。 虽然我觉得它非常优雅,但我对这种方法有两个问题: 1. 当你需要将多个项目注入你的类时会发生什么? 2. 继承最常用于“是”/专业化意义。将它用于 DI 违背了这个想法(例如,如果我想将 Service 注入 Presenter)。 DI 的一个优点是我可以在测试场景中注入其他依赖项。这种方法如何处理该用例?【参考方案3】:

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

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

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

【讨论】:

【参考方案4】:

前段时间,我编写了依赖注入微框架,并希望使其成为 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)

这里是此示例更详细说明的链接 - http://python-dependency-injector.ets-labs.org/examples/services_miniapp.html

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

GitHub https://github.com/ets-labs/python-dependency-injector 文档http://python-dependency-injector.ets-labs.org/

【讨论】:

谢谢@Roman Mogylatov。我很想知道你如何在运行时配置/调整这些容器,比如从配置文件中。这些依赖项似乎被硬编码到给定的容器中(PlatformServices)。是否为每个可注入库类组合创建一个新容器的解决方案? 嗨@BillDeRose。虽然我的回答被认为太长了,不能作为 SO 评论,但我创建了一个 github 问题并将我的答案发布到那里 - github.com/ets-labs/python-dependency-injector/issues/197 :) 希望它有所帮助,谢谢,Roman【参考方案5】:

由于 Python OOP 实现,IoC 和依赖注入不是 Python 世界的标准做法。但即使对于 Python,这种方法似乎也很有前景。

将依赖项用作参数是一种非 Python 方法。 Python 是一种 OOP 语言,具有优美优雅的 OOP 模型,提供了更直接的方式来维护依赖关系。 为了模仿接口类型而定义充满抽象方法的类也很奇怪。 大量的 wrapper-on-wrapper 变通办法会产生代码开销。 当我只需要一个小模式时,我也不喜欢使用库。

所以我的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.

【讨论】:

当然IOC和DI是常用的,常用的是DIframeworks,不管好坏。【参考方案6】:

还有Pinject,一个谷歌开源的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

【讨论】:

【参考方案7】:

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

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

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)

您当然也可以使用 MyClass iso 类。我的功能。如果您在 settings.py 文件中定义方法名的值,您可以根据设置文件的值加载不同版本的方法名。 Django 正在使用这样的方案来定义它的数据库连接。

【讨论】:

【参考方案8】:

依赖注入是 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 类和类型提示。他们的主要目的是为客户开发人员提供指导。如果您删除 UserStore 类和所有对它的引用,代码仍然有效。

【讨论】:

【参考方案9】:

在尝试了一些 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!

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

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

希望这会有所帮助。欲了解更多信息,请参阅

github:https://github.com/BradLewis/simple-injection 文档:https://simple-injection.readthedocs.io/en/latest/ pypi:https://pypi.org/project/simple-injection/

【讨论】:

以上是关于什么是依赖注入的 Pythonic 方式?的主要内容,如果未能解决你的问题,请参考以下文章

PHP如何实现依赖注入

依赖注入

详解.NET Core 依赖注入生命周期

Spring 依赖注入怎么回事,还有面向方面编程是怎么回事

Net Core依赖注入

FastAPI 依赖注入详解:生成依赖树