什么是Pythonic方式进行依赖注入?

时间:2015-07-28 14:10:11

标签: python oop authentication dependency-injection frameworks

简介

对于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对应的用户。

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

9 个答案:

答案 0 :(得分:44)

有关如何使用超级和多重继承而不是DI的参数,请参阅Raymond Hettinger - Super considered super! - PyCon 2015。如果您没有时间观看整个视频,请跳至第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,则必须更改代码。

这里的一个额外好处是,如果客户端没有提供实现,这将引发异常。

当然,这不是依赖注入,它是多重继承和mixins,但它是解决问题的Pythonic方法。

答案 1 :(得分:15)

我们在项目中进行依赖注入的方式是使用inject lib。查看documentation。我强烈建议将它用于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有一些原生支持?答案是,遗憾的是:不。

答案 2 :(得分:6)

前段时间我编写了依赖注入微框架,其目标是使其成为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

希望它有所帮助。欲了解更多信息,请访问:

答案 3 :(得分:3)

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

框架代码:

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类及其所有引用,该代码仍然有效。

答案 4 :(得分:1)

我认为DI和可能的AOP通常不被认为是Pythonic,因为典型的Python开发人员偏好,而不是语言功能。

事实上,您可以使用元类和类装饰器来实现a basic DI framework in <100 lines

对于侵入性较小的解决方案,这些构造可用于将自定义实现插入到通用框架中。

答案 5 :(得分: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使用这种方案来定义其数据库连接。

答案 6 :(得分:1)

在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!

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

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

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

答案 7 :(得分:0)

由于Python OOP的实现,IoC和依赖项注入在Python世界中并不常见。尽管如此,即使对于Python来说,这种方法也很有希望。

  • 要将依赖项用作参数,即使它是在同一代码库中定义的类,也是非Python的方法。 Python是具有漂亮优雅的OOP模型的OOP语言,因此忽略它不是一个好主意。
  • 要定义充满抽象方法的类只是为了模仿接口类型也是很奇怪的。
  • 巨大的包装器解决方法不太优雅,无法使用。
  • 当我只需要一个小模式时,我也不喜欢使用库。

所以我的solution是:

# 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         

答案 8 :(得分:0)

还有Google的开源Python依赖注入程序Pinject。

这里是一个示例

pd.date_range

还有here is the source code