如何做依赖注入python-way?

Posted

技术标签:

【中文标题】如何做依赖注入python-way?【英文标题】:How to do dependency injection python-way? 【发布时间】:2011-02-12 22:19:23 【问题描述】:

我最近阅读了很多关于 python-way 的文章,所以我的问题是

python-way如何进行依赖注入?

我说的是通常的场景,例如,服务 A 需要访问 UserService 以进行授权检查。

【问题讨论】:

【参考方案1】:

这一切都取决于情况。例如,如果你使用依赖注入来进行测试——这样你就可以轻松地模拟出某些东西——你通常可以完全放弃注入:你可以模拟出你本来要注入的模块或类:

subprocess.Popen = some_mock_Popen
result = subprocess.call(...)
assert some_mock_popen.result == result

subprocess.call() 将调用subprocess.Popen(),我们可以模拟它而无需以特殊方式注入依赖项。我们可以直接替换subprocess.Popen。 (这只是一个示例;在现实生活中,您会以一种更强大的方式执行此操作。)

如果您在更复杂的情况下使用依赖注入,或者在不适合模拟整个模块或类时(例如,因为您只想模拟一个特定的调用),则对依赖项使用类属性或模块全局变量是通常的选择。例如,考虑my_subprocess.py

from subprocess import Popen

def my_call(...):
    return Popen(...).communicate()

您可以通过分配给my_subprocess.Popen 轻松地仅替换my_call() 发出的Popen 调用;它不会影响对subprocess.Popen 的任何其他调用(当然,它会替换对my_subprocess.Popen 的所有调用。)同样,类属性:

class MyClass(object):
    Popen = staticmethod(subprocess.Popen)
    def call(self):
        return self.Popen(...).communicate(...)

当使用像这样的类属性时,考虑到选项很少需要,你应该小心使用staticmethod。如果你不这样做,并且你插入的对象是一个普通的函数对象或其他类型的描述符,比如一个属性,当从一个类或实例中检索时,它会做一些特殊的事情,它会做错事。更糟糕的是,如果您使用了 right now 不是描述符的东西(例如示例中的 subprocess.Popen 类),它现在可以工作,但如果相关对象更改为正常的函数 future ,它会令人困惑地崩溃。

最后,只有简单的回调;如果您只想将类的特定实例绑定到特定服务,您可以将服务(或服务的一个或多个方法)传递给类初始化器,并让它使用它:

class MyClass(object):
    def __init__(self, authenticate=None, authorize=None):
        if authenticate is None:
            authenticate = default_authenticate
        if authorize is None:
            authorize = default_authorize
        self.authenticate = authenticate
        self.authorize = authorize
    def request(self, user, password, action):
        self.authenticate(user, password)
        self.authorize(user, action)
        self._do_request(action)

...
helper = AuthService(...)
# Pass bound methods to helper.authenticate and helper.authorize to MyClass.
inst = MyClass(authenticate=helper.authenticate, authorize=helper.authorize)
inst.request(...)

当设置这样的实例属性时,您不必担心描述符会触发,因此只需分配函数(或类或其他可调用对象或实例)就可以了。

【讨论】:

我有 C# 背景,所以我习惯于通过构造函数显式注入并使用 Container 来解决所有问题。在初始化程序中将默认值指定为 None 看起来像“穷人的 DI”,这对我来说不是最佳实践,因为它不够明确。关于通过赋值替换现有方法的类似想法(这就是所谓的monkey patching?)。我是否应该改变我的心态,因为 python-way 与 C#-way 不同? 是的,如果你想编写高效的 Pythonic 代码,你应该意识到 Python 是一种完全不同的语言。你做不同的事情不同。用于测试的 Monkeypatching 模块确实非常常见且非常安全(尤其是在使用适当的模拟库为您处理细节时)。Monkeypatching 类(更改类上的特定方法)是另一回事。 None 默认值实际上不是您应该关注的事情——该示例实际上通过构造函数非常接近 DI,您可以省略默认值并根据需要将其设为必需参数。 @ThomasWouters 您能解释一下吗:您可以通过分配给 my_subprocess.Popen 来轻松替换 my_call() 发出的 Popen 调用。 你的意思是 subprocess.call 调用一个打电话给subprocess.Popen?【参考方案2】:

这个“仅限 setter”的注射配方怎么样? http://code.activestate.com/recipes/413268/

这是相当 Python 的,使用带有 __get__()/__set__() 的“描述符”协议,但相当具有侵入性,需要将所有属性设置代码替换为 RequiredFeature 实例,该实例初始化为Feature 必填。

【讨论】:

【参考方案3】:

在使用没有任何 DI 自动装配框架的 Python 和使用 Spring 的 Java 多年之后,我开始意识到简单的 Python 代码通常不需要框架来进行没有自动装配的依赖注入(自动装配是 Guice 和 Spring 在 Java 中所做的),即,只要做这样的事情就足够了:

def foo(dep = None):  # great for unit testing!
    self.dep = dep or Dep()  # callers can not care about this too
    ...

这是纯粹的依赖注入(非常简单),但没有用于为您自动注入它们的神奇框架(即自动装配)并且没有控制反转。

尽管当我处理更大的应用程序时,这种方法不再适用。所以我想出了injectable 一个微框架,它不会让人感觉非pythonic,但会提供一流的依赖注入自动装配。

人类依赖注入™的座右铭下,它看起来像这样:

# some_service.py
class SomeService:
    @autowired
    def __init__(
        self,
        database: Autowired(Database),
        message_brokers: Autowired(List[Broker]),
    ):
        pending = database.retrieve_pending_messages()
        for broker in message_brokers:
            broker.send_pending(pending)
# database.py
@injectable
class Database:
    ...
# message_broker.py
class MessageBroker(ABC):
    def send_pending(messages):
        ...
# kafka_producer.py
@injectable
class KafkaProducer(MessageBroker):
    ...
# sqs_producer.py
@injectable
class SQSProducer(MessageBroker):
    ...

【讨论】:

【参考方案4】:

我最近发布了一个 Python 的 DI 框架,它可能对您有所帮助。我认为这是一个相当新鲜的看法,但我不确定它有多“pythonic”。自己判断。非常欢迎反馈。

https://github.com/suned/serum

【讨论】:

【参考方案5】:

什么是依赖注入?

依赖注入是一个有助于减少耦合和增加内聚的原则。

耦合和凝聚力是关于组件之间的连接强度。

高耦合。如果耦合度很高,就像使用强力胶水或焊接一样。不容易拆卸。 高凝聚力。高内聚力就像使用螺丝钉。非常容易拆卸和组装回来或以不同的方式组装。它与高耦合相反。

内聚度高,耦合度低。

低耦合带来了灵活性。您的代码变得更容易更改和测试。

如何实现依赖注入?

对象不再相互创建。它们提供了一种注入依赖项的方法。

之前:


import os


class ApiClient:

    def __init__(self):
        self.api_key = os.getenv('API_KEY')  # <-- dependency
        self.timeout = os.getenv('TIMEOUT')  # <-- dependency


class Service:

    def __init__(self):
        self.api_client = ApiClient()  # <-- dependency


def main() -> None:
    service = Service()  # <-- dependency
    ...


if __name__ == '__main__':
    main()

之后:


import os


class ApiClient:

    def __init__(self, api_key: str, timeout: int):
        self.api_key = api_key  # <-- dependency is injected
        self.timeout = timeout  # <-- dependency is injected


class Service:

    def __init__(self, api_client: ApiClient):
        self.api_client = api_client  # <-- dependency is injected


def main(service: Service):  # <-- dependency is injected
    ...


if __name__ == '__main__':
    main(
        service=Service(
            api_client=ApiClient(
                api_key=os.getenv('API_KEY'),
                timeout=os.getenv('TIMEOUT'),
            ),
        ),
    )

ApiClient 与知道选项的来源是分离的。您可以从配置文件中读取密钥和超时时间,甚至可以从数据库中获取它们。

ServiceApiClient 解耦。它不再创建它。您可以提供存根或其他兼容对象。

函数main()Service 解耦。它接收它作为参数。

灵活性是有代价的。

现在你需要像这样组装和注入对象:


main(
    service=Service(
        api_client=ApiClient(
            api_key=os.getenv('API_KEY'),
            timeout=os.getenv('TIMEOUT'),
        ),
    ),
)

汇编代码可能会重复,并且更改应用程序结构会变得更加困难。

结论

依赖注入给你带来3个好处:

灵活性。组件是松散耦合的。您可以通过以不同方式组合组件轻松扩展或更改系统的功能。您甚至可以即时完成。 可测试性。测试很容易,因为您可以轻松地注入模拟,而不是使用 API 或数据库等的真实对象。 清晰性和可维护性。依赖注入可帮助您揭示依赖关系。隐式变为显式。并且“显式优于隐式”(PEP 20 - Python 之禅)。您在容器中明确定义了所有组件和依赖项。这提供了对应用程序结构的概述和控制。易于理解和更改。

——

我相信通过已经介绍的示例,您将理解这个想法并能够将其应用于您的问题,即 UserService 用于授权的实现

【讨论】:

这是一个 FastAPI + SQLAlchemy + 依赖注入器example application。或许对你的UserService 授权有帮助。

以上是关于如何做依赖注入python-way?的主要内容,如果未能解决你的问题,请参考以下文章

如何避免依赖注入构造函数的疯狂?

如何将依赖项注入到laravel作业中

PHP如何实现依赖注入

如何使用带有角度依赖注入的打字稿继承

Laravel:依赖注入身份验证

如何理解php的依赖注入