Python依赖注入的方式是什么?


84

介绍

对于Java,依赖注入作为纯OOP工作,即,您提供要实现的接口,并且在框架代码中接受实现已定义接口的类的实例。

现在,对于Python,您可以以相同的方式进行操作,但是对于Python,我认为该方法的开销太大。那么,您将如何以Pythonic的方式实现它呢?

用例

说这是框架代码:

class FrameworkClass():
    def __init__(self, ...):
        ...

    def do_the_job(self, ...):
        # some stuff
        # depending on some external function

基本方法

最幼稚(也许是最好的?)方法是要求将外部函数提供给FrameworkClass构造函数,然后从do_the_job方法中。

框架代码:

class FrameworkClass():
    def __init__(self, func):
        self.func = func

    def do_the_job(self, ...):
        # some stuff
        self.func(...)

客户代码:

def my_func():
    # my implementation

framework_instance = FrameworkClass(my_func)
framework_instance.do_the_job(...)

问题很简短。有没有更好的常用Pythonic方法来做到这一点?也许有任何支持这种功能的库?

更新:具体情况

想象一下,我开发了一个微型Web框架,该框架使用令牌处理身份验证。该框架需要一个函数来提供ID从令牌中获得的一些令牌,并获取与令牌相对应的用户ID

显然,框架对用户或任何其他特定于应用程序的逻辑一无所知,因此客户端代码必须将用户获取器功能注入框架中以使身份验证工作。


2
为什么不“提供要实现的接口,并且在框架代码中接受实现定义的接口的类的实例”?在Python中,您将以EAFP样式执行此操作(即假设它符合该接口,AttributeError否则TypeError将引发或),但是在其他方面相同。
jonrsharpe

使用带有修饰符abs的的ABCMeta元类很容易做到这一点@abstractmethod,并且无需手动验证。只想得到几个选择和建议。您引用的是最干净的一种,但是我认为这会增加开销。
bagrat 2015年

那我不知道你想问什么问题。
jonrsharpe

好吧,我会尝试换句话说。问题很明显。问题是如何以Python方式做到这一点。选项1:您引用的方式,选项2:我在问题中描述的基本方法。所以问题是,还有其他Python方式可以做到这一点吗?
bagrat 2015年

Answers:


66

参见Raymond Hettinger-超级超级!-PyCon 2015,有关如何使用超级继承和多重继承而不是DI的争论。如果您没有时间观看整个视频,请跳至第15分钟(但我建议您观看所有视频)。

这是一个如何将此视频中的内容应用于您的示例的示例:

框架代码:

class TokenInterface():
    def getUserFromToken(self, token):
        raise NotImplementedError

class FrameworkClass(TokenInterface):
    def do_the_job(self, ...):
        # some stuff
        self.user = super().getUserFromToken(...)

客户代码:

class SQLUserFromToken(TokenInterface):
    def getUserFromToken(self, token):      
        # load the user from the database
        return user

class ClientFrameworkClass(FrameworkClass, SQLUserFromToken):
    pass

framework_instance = ClientFrameworkClass()
framework_instance.do_the_job(...)

这将起作用,因为Python MRO将保证调用getUserFromToken客户端方法(如果使用了super())。如果您使用的是Python 2.x,则必须更改代码。

此处的另一个好处是,如果客户端不提供实现,则会引发异常。

当然,这并不是真正的依赖注入,它是多个继承和混合,但这是解决问题的Python方法。


10
考虑了这个答案super():)
bagrat 2015年

2
雷蒙德将其称为CI,而我认为它是纯mixin。但是,难道在Python中mixin和CI实际上是相同的?唯一的区别是指示的程度。Mixin将依赖项注入类级别,而CI将依赖项注入实例。
nad2000

1
我认为构造函数级注入无论如何都非常容易在Python中完成,就像OP如何描述它。这种pythonic的方式看起来非常有趣。它只需要比简单的构造函数注入IMO多一些的布线。
stucash

6
尽管我觉得它很优雅,但是我在使用此方法时遇到两个问题:1.当您需要将服务器项注入到类中时会发生什么?2.继承最经常用在“是” /专业化意义上。将其用于DI违反了这个想法(例如,如果我想将服务注入到Presenter中)。
AljoSt

18

在项目中进行依赖项注入的方式是使用注入库。查看文档。我强烈建议将其用于DI。仅仅使用一个功能就没有任何意义,但是当您必须管理多个数据源等时,它就变得很有意义。

按照您的示例,它可能类似于:

# framework.py
class FrameworkClass():
    def __init__(self, func):
        self.func = func

    def do_the_job(self):
        # some stuff
        self.func()

您的自定义函数:

# my_stuff.py
def my_func():
    print('aww yiss')

您想要在应用程序中的某个位置创建一个引导文件,以跟踪所有已定义的依赖项:

# bootstrap.py
import inject
from .my_stuff import my_func

def configure_injection(binder):
    binder.bind(FrameworkClass, FrameworkClass(my_func))

inject.configure(configure_injection)

然后您可以通过以下方式使用代码:

# some_module.py (has to be loaded with bootstrap.py already loaded somewhere in your app)
import inject
from .framework import FrameworkClass

framework_instance = inject.instance(FrameworkClass)
framework_instance.do_the_job()

恐怕这是它所能得到的pythonic(该模块具有一些python甜美性,例如通过参数注入装饰器的装饰器-检查文档),因为python没有像接口或类型提示之类的奇特的东西。

因此,直接回答您的问题将非常困难。我认为真正的问题是:python是否对DI有一些本机支持?可悲的是,答案是:不会。


谢谢您的回答,似乎很有趣。我将检出装饰部分。同时,让我们等待更多答案。
bagrat 2015年

感谢您链接到“注入”库。这是到目前为止我找到的最接近的数字,它可以填补我想要由DI填补的空白-奖金,它实际上一直在维护!
安迪·莫蒂默

13

前段时间,我写了一个依赖注入微框架,以使其成为Pythonic- Dependency Injector。这就是您的代码在使用时的样子:

"""Example of dependency injection in Python."""

import logging
import sqlite3

import boto.s3.connection

import example.main
import example.services

import dependency_injector.containers as containers
import dependency_injector.providers as providers


class Platform(containers.DeclarativeContainer):
    """IoC container of platform service providers."""

    logger = providers.Singleton(logging.Logger, name='example')

    database = providers.Singleton(sqlite3.connect, ':memory:')

    s3 = providers.Singleton(boto.s3.connection.S3Connection,
                             aws_access_key_id='KEY',
                             aws_secret_access_key='SECRET')


class Services(containers.DeclarativeContainer):
    """IoC container of business service providers."""

    users = providers.Factory(example.services.UsersService,
                              logger=Platform.logger,
                              db=Platform.database)

    auth = providers.Factory(example.services.AuthService,
                             logger=Platform.logger,
                             db=Platform.database,
                             token_ttl=3600)

    photos = providers.Factory(example.services.PhotosService,
                               logger=Platform.logger,
                               db=Platform.database,
                               s3=Platform.s3)


class Application(containers.DeclarativeContainer):
    """IoC container of application component providers."""

    main = providers.Callable(example.main.main,
                              users_service=Services.users,
                              auth_service=Services.auth,
                              photos_service=Services.photos)

这是此示例的更广泛描述的链接-http://python-dependency-injector.ets-labs.org/examples/services_miniapp.html

希望它能有所帮助。有关更多信息,请访问:


谢谢@Roman Mogylatov。我很好奇,想知道如何在运行时配置/适应这些容器,例如从配置文件中。这些依赖项似乎已硬编码到给定的容器(PlatformServices)中。是否为每种可注入库类组合创建一个新容器的解决方案?
Bill DeRose

2
嗨@BillDeRose。虽然我的回答被认为太长了,无法成为SO评论,但我创建了一个github问题,并将我的回答发布在这里-github.com/ets-labs/python-dependency-injector/issues/197 :)希望对您有所帮助,谢谢,罗马
罗马Mogylatov


2

还有Google的Pinject,一个开源的python依赖注入程序。

这是一个例子

>>> class OuterClass(object):
...     def __init__(self, inner_class):
...         self.inner_class = inner_class
...
>>> class InnerClass(object):
...     def __init__(self):
...         self.forty_two = 42
...
>>> obj_graph = pinject.new_object_graph()
>>> outer_class = obj_graph.provide(OuterClass)
>>> print outer_class.inner_class.forty_two
42

这里是源代码


2

依赖注入是Python直接支持的一种简单技术。不需要其他库。使用类型提示可以提高清晰度和可读性。

框架代码:

class UserStore():
    """
    The base class for accessing a user's information.
    The client must extend this class and implement its methods.
    """
    def get_name(self, token):
        raise NotImplementedError

class WebFramework():
    def __init__(self, user_store: UserStore):
        self.user_store = user_store

    def greet_user(self, token):
        user_name = self.user_store.get_name(token)
        print(f'Good day to you, {user_name}!')

客户代码:

class AlwaysMaryUser(UserStore):
    def get_name(self, token):      
        return 'Mary'

class SQLUserStore(UserStore):
    def __init__(self, db_params):
        self.db_params = db_params

    def get_name(self, token):
        # TODO: Implement the database lookup
        raise NotImplementedError

client = WebFramework(AlwaysMaryUser())
client.greet_user('user_token')

UserStore不需要实现依赖注入类和类型提示。他们的主要目的是为客户开发人员提供指导。如果删除UserStore该类及其所有引用,该代码仍然有效。


1

importlib是一种非常简单的Pythonic依赖注入方法。

您可以定义一个小的实用函数

def inject_method_from_module(modulename, methodname):
    """
    injects dynamically a method in a module
    """
    mod = importlib.import_module(modulename)
    return getattr(mod, methodname, None)

然后您可以使用它:

myfunction = inject_method_from_module("mypackage.mymodule", "myfunction")
myfunction("a")

在mypackage / mymodule.py中,定义myfunction

def myfunction(s):
    print("myfunction in mypackage.mymodule called with parameter:", s)

您当然也可以使用MyClass iso类。函数myfunction。如果您在settings.py文件中定义methodname的值,则可以根据设置文件的值加载不同版本的methodname。Django使用这种方案来定义其数据库连接。


1

由于Python OOP的实现,IoC和依赖项注入不是Python世界中的标准实践。但是,即使对于Python来说,这种方法也很有希望。

  • 将依赖项用作参数是一种非Python方法。Python是一种具有精美优雅的OOP模型的OOP语言,它提供了更直接的方式来维护依赖关系。
  • 仅仅为了模仿接口类型而定义充满抽象方法的类也是很奇怪的。
  • 巨大的包装器解决方法会增加代码开销。
  • 当我需要的只是一个小模式时,我也不喜欢使用库。

所以我的解决方案是:

# Framework internal
def MetaIoC(name, bases, namespace):
    cls = type("IoC{}".format(name), tuple(), namespace)
    return type(name, bases + (cls,), {})


# Entities level                                        
class Entity:
    def _lower_level_meth(self):
        raise NotImplementedError

    @property
    def entity_prop(self):
        return super(Entity, self)._lower_level_meth()


# Adapters level
class ImplementedEntity(Entity, metaclass=MetaIoC):          
    __private = 'private attribute value'                    

    def __init__(self, pub_attr):                            
        self.pub_attr = pub_attr                             

    def _lower_level_meth(self):                             
        print('{}\n{}'.format(self.pub_attr, self.__private))


# Infrastructure level                                       
if __name__ == '__main__':                                   
    ENTITY = ImplementedEntity('public attribute value')     
    ENTITY.entity_prop         

编辑:

小心图案。我在一个真实的项目中使用了它,但它显示了一种不好的方式。我在Medium上发表的关于我使用模式的经验的文章。


当然,IOC和DI是常用的,无论好坏,DI框架都不常用。
juanpa.arrivillaga

0

在使用python中的一些DI框架进行研究之后,我发现在比较其他领域(如.NET Core)的简单性时,它们使用起来有些笨拙。这主要归因于通过诸如装饰器之类的东西的加入,这些东西会使代码混乱并使它们难以简单地添加到项目中或从项目中删除,或者基于变量名进行连接。

我最近一直在研究依赖项注入框架,该框架改为使用类型注释来进行称为Simple-Injection的注入。下面是一个简单的例子

from simple_injection import ServiceCollection


class Dependency:
    def hello(self):
        print("Hello from Dependency!")

class Service:
    def __init__(self, dependency: Dependency):
        self._dependency = dependency

    def hello(self):
        self._dependency.hello()

collection = ServiceCollection()
collection.add_transient(Dependency)
collection.add_transient(Service)

collection.resolve(Service).hello()
# Outputs: Hello from Dependency!

该库支持服务生存期,并将服务绑定到实现。

该库的目标之一是,还可以轻松地将其添加到现有应用程序中,并在提交之前查看您的喜好,因为它所需要的只是您的应用程序具有适当的类型,然后在入口点并运行它。

希望这可以帮助。有关更多信息,请参见

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.