如何做依赖注入python-way?

时间:2010-04-27 15:23:54

标签: python design-patterns dependency-injection

我最近一直在阅读很多关于python-way的内容,所以我的问题是

  

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

我正在讨论通常的情况,例如,服务A需要访问UserService以进行授权检查。

5 个答案:

答案 0 :(得分:14)

这一切都取决于具体情况。例如,如果您使用依赖注入进行测试 - 因此您可以轻松地模拟某些东西 - 您通常可以完全放弃注入:您可以模拟出您注入的模块或类:

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()

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

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

当使用这样的类属性时,考虑到选项很少需要,你应该注意使用staticmethod。如果不这样做,并且您要插入的对象是普通函数对象或其他类型的描述符(如属性),从类或实例中检索时会执行某些特殊操作,它会执行错误的操作。更糟糕的是,如果你使用的东西现在不是描述符(如示例中的subprocess.Popen类)它现在可以工作,但是如果有问题的对象改变为正常功能的未来,它会让人迷惑不解。

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

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(...)

当设置这样的实例属性时,你永远不必担心触发描述符,所以只需分配函数(或类或其他callables或实例)就可以了。

答案 1 :(得分:1)

这种“仅限二传手”注射配方怎么样? http://code.activestate.com/recipes/413268/

它非常pythonic,使用__get__() / __set__()的“描述符”协议,而是侵入性的,需要用RequiredFeature实例初始化的Feature替换所有属性设置代码所需的{{1}}的str-name。

答案 2 :(得分:0)

我最近发布了一个可以帮助你的python的DI框架。我认为这是一个相当新鲜的看法,但我不确定' pythonic'它是。为自己判断。反馈非常受欢迎。

https://github.com/suned/serum

答案 3 :(得分:0)

@Thomas Wouters的答案已经完成并通过。只是添加它:通常我更喜欢最简单的方法,那些不涉及复杂框架和冗长设置的方法。所以我最常用的是将我的依赖项作为构造函数参数。

问题是我添加到代码中的样板文件只是为了保证每个依赖项都被初始化。

作为装饰器的忠实粉丝从程序范围中移除样板代码我只是做了一个装饰器来处理它:

@autowired 一个Python 3装饰器,可以轻松,干净地依赖注入:

  • 该功能无需了解所有
  • 的自动装配
  • 依赖关系可以延迟初始化
  • 如果需要,调用者可以显式传递依赖项实例

装饰者的重点是这样的代码

def __init__(self, *, model: Model = None, service: Service = None):
    if model is None:
        model = Model()

    if service is None:
        service = Service()

    self.model = model
    self.service = service
    # actual code

进入

@autowired
def __init__(self, *, model: Model, service: Service):
    self.model = model
    self.service = service
    # actual code

没有复杂的东西,没有设置,没有强制执行工作流程。现在,您的功能代码不再与依赖关系初始化代码混杂在一起。

装饰器方法非常简约,但可能是一个完整的框架更适合你的情况。为此,有一些优秀的模块,如Injector

答案 4 :(得分:0)

什么是依赖注入?

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

耦合和内聚是关于组件连接的坚固程度。

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

当内聚力高时,耦合度低。

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

如何实现依赖注入?

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

之前:


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 进行授权