我最近一直在阅读很多关于python-way的内容,所以我的问题是
如何进行依赖注入python-way?
我正在讨论通常的情况,例如,服务A需要访问UserService以进行授权检查。
答案 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'它是。为自己判断。反馈非常受欢迎。
答案 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
与了解选项来自何处分离。您可以从配置文件中读取密钥和超时,甚至可以从数据库中获取它们。
Service
与 ApiClient
分离。它不再创建它。您可以提供存根或其他兼容对象。
函数 main()
与 Service
分离。它接收它作为参数。
灵活性是有代价的。
现在你需要像这样组装和注入对象:
main(
service=Service(
api_client=ApiClient(
api_key=os.getenv('API_KEY'),
timeout=os.getenv('TIMEOUT'),
),
),
)
汇编代码可能会重复,并且更改应用程序结构会变得更加困难。
依赖注入为您带来 3 个优势:
——
我相信通过已经提供的示例,您将理解该想法并能够将其应用于您的问题,即实现UserService 进行授权。