为什么IoC / DI在Python中不常见?

在Java中,国际奥委会 / 是一种非常常见的实践,广泛用于web应用程序,几乎所有可用的框架和Java EE。另一方面,也有很多大型的Python web应用程序,但除了Zope(我听说它的编码真的很糟糕)之外,IoC在Python世界中似乎并不常见。(如果你认为我是错的,请举一些例子)。

当然,有一些流行的Java IoC框架的克隆可用于Python,例如springpython。但它们似乎都没有被实际使用。至少,我从来没有碰到过基于Djangosqlalchemy+<insert your favorite wsgi toolkit here>的web应用程序使用这样的东西。

在我看来,IoC有合理的优势,可以很容易地取代django-default-user-model,但在Python中广泛使用接口类和IoC看起来有点奇怪,而且不»pythonic«。但是也许有人有更好的解释,为什么IoC在Python中没有被广泛使用。

154253 次浏览

在我看来,像依赖注入这样的事情是僵化和过于复杂的框架的症状。当代码主体变得过于沉重而难以更改时,你会发现自己不得不选择其中的一小部分,为它们定义接口,然后允许人们通过插入这些接口的对象来改变行为。这很好,但最好在一开始就避免这种复杂性。

这也是静态类型语言的症状。当您必须表达抽象的唯一工具是继承时,那么您几乎到处都在使用继承。话虽如此,c++与之非常相似,但从未像Java开发人员那样对构建器和接口着迷。我们很容易以编写太多的泛型代码,几乎没有真正的好处为代价,过度追求灵活和可扩展的梦想。我认为这是文化问题。

通常我认为Python人习惯于为工作选择正确的工具,这是一个连贯而简单的整体,而不是一个真正的工具(带有一千种可能的插件),它可以做任何事情,但提供了令人眼花缭乱的可能配置排列。在必要的地方仍然有可互换的部分,但由于duck-typing的灵活性和语言的相对简单性,不需要定义固定接口的大形式主义。

我已经好几年没用过Python了,但我想说的是,Python是一种动态类型的语言。举个简单的例子,在Java中,如果我想测试某些东西是否符合标准,我可以使用DI并传入任何PrintStream来捕获正在写入的文本并验证它。然而,当我在Ruby中工作时,我可以动态地替换STDOUT上的“puts”方法来进行验证,完全不考虑DI。如果我创建抽象的唯一原因是测试使用它的类(想想文件系统操作或Java中的时钟),那么DI/IoC在解决方案中创建了不必要的复杂性。

部分原因是模块系统在Python中的工作方式。你可以免费获得一种“单例”,只需从模块中导入它。在模块中定义一个对象的实际实例,然后任何客户端代码都可以导入它,并实际获得一个工作的、完全构造/填充的对象。

这与Java相反,在Java中不导入对象的实际实例。这意味着你总是必须自己实例化它们(或使用某种IoC/DI风格的方法)。您可以通过使用静态工厂方法(或实际的工厂类)来减轻必须自己实例化所有东西的麻烦,但这样您仍然会产生每次实际创建新方法的资源开销。

我实际上并不认为DI/IoC在Python中是不常见的。然而,不常见的是DI/IoC 框架/容器

想想看:DI容器做什么?它允许你

  1. 将独立的组件连接成一个完整的应用程序……
  2. ... 在运行时。

我们有“一起布线”的名字;And "at runtime"

  1. 脚本
  2. 动态

因此,DI容器只是动态脚本语言的解释器。实际上,让我换一种说法:典型的Java/。NET DI容器只是一个蹩脚的解释器,适用于一种非常糟糕的动态脚本语言,语法非常难看,有时是基于xml的。

当您使用Python编程时,为什么要使用一种丑陋、糟糕的脚本语言,而不是拥有一种漂亮、出色的脚本语言呢?实际上,这是一个更普遍的问题:当您使用几乎任何语言进行编程时,当您可以使用Jython和IronPython时,为什么还要使用一种丑陋、糟糕的脚本语言呢?

所以,概括一下:DI/IoC的实践在Python中和在Java中一样重要,原因完全相同。然而,DI/IoC的实现是内置在语言中的,而且通常是轻量级的,以至于完全消失了。

(这里有一个简单的类比:在汇编中,子例程调用是一件相当重要的事情——你必须将你的局部变量和寄存器保存到内存中,将你的返回地址保存到某个地方,更改你正在调用的子例程的指令指针,当它完成时安排它以某种方式跳回你的子例程,将参数放在被调用者可以找到的地方,等等。IOW:在汇编中,“子程序调用”;是一种设计模式,在像Fortran这样内置子例程调用的语言出现之前,人们都在构建自己的“子例程框架”。你会说子程序调用“不常见”吗?在Python中,仅仅因为你不使用子例程框架?)

顺便说一句:关于DI的逻辑结论是什么样子的例子,看看吉拉德·布拉新话程序设计语言和他关于这个主题的文章:

Django很好地利用了反转控制。例如,由配置文件选择数据库服务器,然后框架向数据库客户机提供适当的数据库包装器实例。

区别在于Python有第一类类型。数据类型(包括类)本身就是对象。如果您想要使用特定的类,只需命名类即可。例如:

if config_dbms_name == 'postgresql':
import psycopg
self.database_interface = psycopg
elif config_dbms_name == 'mysql':
...

之后的代码可以通过以下方式创建数据库接口:

my_db_connection = self.database_interface()
# Do stuff with database.

与Java和c++需要的样板工厂函数不同,Python只需要一两行普通代码就可以完成。这就是函数式编程与命令式编程的优势所在。

实际上,使用DI编写足够干净和紧凑的代码是相当容易的(我想知道,它会/保持神谕的,但无论如何:)),例如,我实际上更喜欢这种编码方式:

def polite(name_str):
return "dear " + name_str


def rude(name_str):
return name_str + ", you, moron"


def greet(name_str, call=polite):
print "Hello, " + call(name_str) + "!"

_

>>greet("Peter")
Hello, dear Peter!
>>greet("Jack", rude)
Hello, Jack, you, moron!

是的,这可以被看作是参数化函数/类的一种简单形式,但它确实起作用了。所以,也许Python默认包含的电池在这里也足够了。

附注:我还在在Python中动态计算简单的布尔逻辑中发布了一个更大的这种幼稚方法的例子。

我支持“Jörg W Mittag”的回答:“DI/IoC的Python实现是如此的轻量级,以至于它完全消失了”。

为了支持这一说法,看看Martin Fowler从Java移植到Python的著名示例:Python: Design_Patterns: Inversion_of_Control

从上面的链接中可以看到,Python中的“Container”可以用8行代码编写:

class Container:
def __init__(self, system_data):
for component_name, component_class, component_args in system_data:
if type(component_class) == types.ClassType:
args = [self.__dict__[arg] for arg in component_args]
self.__dict__[component_name] = component_class(*args)
else:
self.__dict__[component_name] = component_class

我同意@Jorg的观点,在Python中DI/IoC是可能的,更简单,甚至更漂亮。缺少的是支持它的框架,但也有一些例外。举几个我想到的例子:

  • Django注释允许您将自己的Comment类与自定义逻辑和表单连接起来。[更多信息] .

  • Django允许你使用一个自定义Profile对象来附加到你的User模型。这不是完全的IoC,但却是一个很好的方法。就我个人而言,我希望像注释框架那样取代hole User模型。[更多信息] .

IoC/DI是一个设计概念,但不幸的是,它经常被认为是一个适用于某些语言(或类型系统)的概念。我希望看到依赖注入容器在Python中变得更加流行。有Spring,但它是一个超级框架,似乎是Java概念的直接移植,没有太多考虑“Python方式”。

鉴于Python 3中的注释,我决定尝试一个功能齐全但简单的依赖注入容器:https://github.com/zsims/dic。它基于。net依赖注入容器的一些概念(如果你曾经在那个领域玩过,IMO是很棒的),但与Python概念发生了变化。

我认为由于python的动态特性,人们并不经常看到对另一个动态框架的需求。当一个类继承自new-style的“对象”时,你可以动态地创建一个新变量(https://wiki.python.org/moin/NewClassVsClassicClass)。

< >强。 在普通python中:

#application.py
class Application(object):
def __init__(self):
pass


#main.py
Application.postgres_connection = PostgresConnection()


#other.py
postgres_connection = Application.postgres_connection
db_data = postgres_connection.fetchone()

然而,看看https://github.com/noodleflake/pyioc,这可能是你正在寻找的。

即。在pyioc中

from libs.service_locator import ServiceLocator


#main.py
ServiceLocator.register(PostgresConnection)


#other.py
postgres_connection = ServiceLocator.resolve(PostgresConnection)
db_data = postgres_connection.fetchone()

不像Java中的强类型。Python的duck类型行为使得传递对象非常容易。

Java开发人员专注于构造类结构和对象之间的关系,同时保持灵活性。IoC对于实现这一目标非常重要。

Python开发人员专注于完成工作。他们只是在需要的时候把类连接起来。他们甚至不用担心课程的类型。只要它会嘎嘎叫,它就是鸭子!这种性质没有给国际奥委会留下任何余地。

我的观点是,在大多数Python应用程序中,您不需要它,即使您需要它,许多讨厌Java的人(以及自认为是开发人员的无能的摆弄者)很可能认为它很糟糕,只是因为它在Java中很受欢迎。

当您拥有复杂的对象网络时,IoC系统实际上是有用的,其中每个对象可能是其他几个对象的依赖项,反过来,每个对象本身又依赖于其他对象。在这种情况下,您希望一次性定义所有这些对象,并有一种机制将它们基于尽可能多的隐式规则自动组合在一起。如果应用程序用户/管理员还需要以一种简单的方式定义配置,那么IoC系统就可以从简单的XML文件(即配置)中读取组件。

典型的Python应用程序要简单得多,只有一堆脚本,没有这么复杂的体系结构。就我个人而言,我知道IoC实际上是什么(与那些在这里写下某些答案的人相反),在我有限的Python经验中,我从来没有觉得需要它(而且我不会在任何地方都使用Spring,当它所提供的优势不足以证明它的开发开销时)。

也就是说,在Python中,IoC方法实际上是有用的,事实上,我在这里读到Django使用了它。

上述相同的推理可以应用于Java世界中的面向方面编程,不同的是,AOP真正有价值的情况的数量甚至更有限。

似乎人们真的不明白依赖注入和控制反转意味着什么了。

使用控制反转的实践是让类或函数依赖于其他类或函数,但是与其在类或函数代码中创建实例,不如将它们作为参数接收,这样就可以实现松耦合。这有很多好处,如更多的可测试性和实现利斯科夫替换原理。

您可以看到,通过使用接口和注入,您的代码变得更加可维护,因为您可以轻松地更改行为,因为您不必重写类的一行代码(可能是DI配置中的一两行代码)来更改其行为,因为实现类所等待的接口的类只要遵循接口就可以独立地变化。保持代码解耦和易于维护的最佳策略之一是至少遵循单一职责、替换和依赖倒置原则。

如果您可以在包中自己实例化一个对象并导入它以自己注入它,那么DI库有什么好处呢?选择的答案是正确的,因为java没有过程部分(类之外的代码),所有这些都进入了无聊的配置xml,因此需要一个类来实例化和注入依赖项,以延迟加载的方式,这样你就不会影响你的性能,而在python中,你只需要在“过程”中编码注入。(类外代码)你代码的部分。

IoC和DI在成熟的Python代码中非常常见。由于鸭子类型,您不需要框架来实现依赖注入。

最好的例子是如何使用settings.py来创建Django应用程序:

# settings.py
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': REDIS_URL + '/1',
},
'local': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'snowflake',
}
}

Django Rest Framework充分利用了DI:

class FooView(APIView):
# The "injected" dependencies:
permission_classes = (IsAuthenticated, )
throttle_classes = (ScopedRateThrottle, )
parser_classes = (parsers.FormParser, parsers.JSONParser, parsers.MultiPartParser)
renderer_classes = (renderers.JSONRenderer,)


def get(self, request, *args, **kwargs):
pass


def post(self, request, *args, **kwargs):
pass

让我提醒():

“依赖注入”是一个5美分概念的25美元术语。[…依赖注入意味着给一个对象它的实例变量。[…]。

所有基于DI的pytest fixture ()

你可以手动使用Python进行依赖注入,但手动方法有其缺点:

  • 大量的样板代码来进行连接。你可以使用Python的动态特性来进行注入,但这样你就失去了IDE的支持(例如,PyCharm中的Ctrl+Space),并且你会使代码更难理解和调试
  • 没有标准:每个程序员都有自己解决相同问题的方法,这导致了重新发明轮子,理解彼此的代码很快就会成为一种痛苦。依赖注入库为插件提供了简单的框架

为了拥有这一切,我们需要一个依赖注入框架,例如这个https://python-dependency-injector.ets-labs.org/index.html似乎是Python最成熟的依赖注入框架。

对于较小的应用程序,DI容器是不必要的,对于任何有几百行代码或更多的东西,DI容器是必须的,以保持你的代码可维护性。

IoC容器被“模仿”;主要是用**狼

class A:
def __init__(self, **kwargs):
print(kwargs)


Class B:
pass


Class C:
pass


Ainstance = A(b=B, c=C)

看看FastAPI,它内置了依赖注入。例如:

from fastapi import Depends, FastAPI


async def get_db():
db = DBSession()
try:
yield db
except Exception:
db.rollback()
raise
finally:
db.close()


app = FastAPI()


@app.get("/items")
def get_items(db=Depends(get_db)):
return db.get_items()