关于设计模式:为什么IoC / DI在Python中不常见?

Why is IoC / DI not common in Python?

在Java中,IOC/DI是一种非常普遍的实践,广泛应用于Web应用程序中,几乎所有可用的框架和JavaEE。另一方面,也有许多大型的python web应用程序,但是除了zope(我听说这对代码来说应该很可怕)之外,ioc在python世界中似乎并不常见。(如果你认为我错了,请举出一些例子)。

当然,对于Python,SpRypyPython,有几种流行的Java IOC框架的克隆。但它们似乎都没有实际应用。至少,我从来没有遇到过Django或sqlacalchemy+的Web应用程序,它使用类似的东西。

在我看来,ioc有合理的优势,可以很容易地替换django默认的用户模型,但是大量使用接口类和ioc在python中看起来有点奇怪,不是吗?Python?但是也许有人有更好的解释,为什么ioc没有在python中广泛使用。


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

考虑一下:DI容器做什么?它允许你

  • 将独立的组件连接到一个完整的应用程序中…
  • …在运行时。
  • 我们有"连接在一起"和"运行时"的名称:

  • 脚本编写
  • 动态
  • 因此,DI容器只是动态脚本语言的解释器。实际上,让我重申一下:典型的Java/.net DI容器只不过是一个糟糕的动态脚本语言的蹩脚的解释器,它具有难看的、有时基于XML的语法。

    当你用python编程时,当你有一个漂亮的,出色的脚本语言在你的处理下,为什么你要使用一个丑陋的,糟糕的脚本语言?实际上,这是一个更普遍的问题:当你用几乎任何一种语言编程时,当你有Jython和Ironpython可供使用时,为什么要使用一种丑陋的、糟糕的脚本语言?

    因此,要重申:DI/IOC的实践在Python中与Java同样重要,原因完全相同。然而,DI/IOC的实现是内置在语言中的,并且通常非常轻,以至于它完全消失了。

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

    顺便说一句:举一个将di带到逻辑结论的例子,看看Gilad Bracha的Newspeak编程语言和他关于这个主题的著作:

    • 施工人员认为有害
    • 注射死刑
    • 禁止进口(续)


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

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


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

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

    1
    2
    3
    4
    5
    if config_dbms_name == 'postgresql':
        import psycopg
        self.database_interface = psycopg
    elif config_dbms_name == 'mysql':
        ...

    稍后的代码可以通过编写以下内容来创建数据库接口:

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

    代替Java和C++需要的样板工厂功能,Python用一行或两行普通代码来完成它。这是函数式编程与命令式编程的优势所在。


    ioc和di在成熟的python代码中非常常见。由于duck类型化,您不需要框架来实现DI。

    最好的例子是如何使用settings.py设置django应用程序:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # 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框架大量使用DI:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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

    让我提醒一下(资料来源):

    "Dependency Injection" is a 25-dollar term for a 5-cent concept. [...] Dependency injection means giving an object its instance variables. [...].


    已经有几年没有使用过python了,但我想说的是,它与动态类型语言的关系比其他任何语言都要大。对于一个简单的例子,在Java中,如果我想测试一些适当地写入标准的东西,我可以使用任何打印流中的DI和PASS来捕获正在编写和验证的文本。但是,当我在Ruby中工作时,我可以在stdout上动态替换"puts"方法来进行验证,从而使di完全不在图片中。如果我创建抽象的唯一原因是测试使用它的类(思考文件系统操作或爪哇中的时钟),那么DI/IOC会在解决方案中造成不必要的复杂性。


    IOC/DI是一个设计概念,但不幸的是,它常常被视为一个适用于某些语言(或打字系统)的概念。我希望看到依赖注入容器在Python中变得更加流行。这里有Spring,但这是一个超级框架,它似乎是Java概念的直接端口,而不用太多地考虑"Python方式"。

    考虑到python 3中的注释,我决定破解一个功能完整但简单的依赖注入容器:https://github.com/zsims/dic。它基于来自.NET依赖注入容器的一些概念(如果您在这个空间中玩过,IMO会非常棒),但会随着python概念而改变。


    它看到人们真的不知道依赖注入和控制反转意味着什么。

    使用控制反转的实践是拥有依赖于其他类或函数的类或函数,但与其创建函数代码类中的实例,不如将其作为参数接收,这样可以实现松散耦合。这有许多好处,因为更多的可测试性和archieve的liskov替代原则。

    你看,通过使用接口和注入,你的代码变得更易于维护,因为你可以很容易地改变行为,因为你不需要重写你的类的一行代码(可能是DI配置上的一两行代码)来改变它的行为,因为实现你的类正在等待的接口的类可以变量y只要它们遵循接口就可以独立地执行。保持代码解耦和易于维护的最佳策略之一是至少遵循单一的响应性、替换和依赖倒置原则。

    如果您可以自己在包中实例化一个对象并导入它以自己注入它,那么DI库有什么用呢?选择的答案是正确的,因为Java没有程序段(类之外的代码),所有这些都进入了乏味的配置XML,因此需要一个类来实例化并以懒惰的加载方式注入依赖关系,这样就不会影响您的性能,而在Python上,只需对"程序"("C"之外的代码)进行编码。lasses)代码部分


    事实上,用DI编写足够干净和紧凑的代码是很容易的(我想知道,那时会不会是/保持pythonic,但无论如何是:)),例如,我实际上采用了这种编码方式:

    1
    2
    3
    4
    5
    6
    7
    8
    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) +"!"

    γ

    1
    2
    3
    4
    >>greet("Peter")
    Hello, dear Peter!
    >>greet("Jack", rude)
    Hello, Jack, you, moron!

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

    另外,在动态评估Python中的简单布尔逻辑时,我还发布了这种幼稚方法的更大示例。


    我回来了吗?rg w mittag"答案:"di/ioc的python实现非常轻,以至于完全消失了。

    为了支持这个语句,看看著名的Martin Fowler例子,它从Java移植到Python:Python:Debug模式:

    从上面的链接可以看到,python中的"容器"可以用8行代码编写:

    1
    2
    3
    4
    5
    6
    7
    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


    我认为,由于Python的动态特性,人们并不经常看到需要另一个动态框架。当一个类从新样式的"对象"继承时,您可以动态地创建一个新变量(https://wiki.python.org/moin/newclassvsclassicclass)。

    即在纯Python中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #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,这可能是您要找的。

    即在PYOC中

    1
    2
    3
    4
    5
    6
    7
    8
    from libs.service_locator import ServiceLocator

    #main.py
    ServiceLocator.register(PostgresConnection)

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


    我的2美分是,在大多数Python应用程序中,你不需要它,即使你需要它,也有可能是许多Java仇恨者(以及那些认为是开发者的无能的小提琴手)认为它是不好的东西,只是因为它在Java中很受欢迎。

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

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

    也就是说,在Python的情况下,IOC方法实际上是有用的,实际上,我在这里读到了Django使用它的消息。

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


    我同意@jorg的观点,即DI/IOC在Python中是可能的、更容易的,甚至更漂亮。缺少的是支持它的框架,但也有一些例外。我想举几个例子:

    • django comments允许您使用自定义逻辑和表单连接自己的注释类。[更多信息]

    • Django允许您使用自定义配置文件对象附加到您的用户模型。这不是完全的国际奥委会,但是一个很好的方法。就我个人而言,我想像评论框架那样取代hole用户模型。[更多信息]


    与Java中的强类型性质不同。python的duck类型行为使得传递对象变得非常容易。

    Java开发人员专注于构造对象的类结构和关系,同时保持事物的灵活性。国际奥委会对于实现这一目标非常重要。

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


    在我看来,依赖注入之类的东西是僵化和过于复杂的框架的症状。当代码的主体变得太重而无法轻易更改时,您会发现自己必须选择其中的一小部分,为它们定义接口,然后允许人们通过插入这些接口的对象更改行为。这一切都很好,但首先最好避免这种复杂性。

    它也是静态类型语言的症状。当您必须表达抽象的唯一工具是继承时,那么这几乎就是您在任何地方使用的工具。话虽如此,C++非常相似,但从未对Java开发人员所做的任何地方的建设者和接口着迷。人们很容易克服这样一个梦想:灵活性和可扩展性,代价是编写太多的通用代码,而实际的好处却很小。我认为这是一种文化。

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