如何做依赖注入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
与知道选项的来源是分离的。您可以从配置文件中读取密钥和超时时间,甚至可以从数据库中获取它们。
Service
与ApiClient
解耦。它不再创建它。您可以提供存根或其他兼容对象。
函数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?的主要内容,如果未能解决你的问题,请参考以下文章