跳转到主要内容

Python依赖注入库

项目描述

https://badge.fury.io/py/pinject.svg https://travis-ci.cn/google/pinject.svg?branch=master https://pepy.tech/badge/pinject https://pepy.tech/badge/pinject/month

Pinject是Python的一个依赖注入库。

Pinject的主要目标是帮助您以简单、可维护的方式将对象组装成图。

如果您已经熟悉其他依赖注入库,您可能希望阅读末尾的简要总结部分,以便了解Pinject是什么样的以及它可能如何与您习惯的库有所不同。

本README末尾有发布版本之间的差异日志。

为什么使用Pinject?

如果您想知道为什么要使用依赖注入库:如果您在Python中编写大量面向对象代码,那么它会使您的生活更轻松。例如

如果您想知道为什么使用Pinject而不是其他Python依赖注入库,以下是一些原因

  • Pinject易于入门。无需用@inject_this@annotate_that装饰代码即可开始。使用Pinject,您只需调用new_object_graph(),一行代码,就可以开始了。

  • Pinject是一个Pythonic依赖注入库。其他库(如Spring或Guice)的Python端口保留了为静态类型语言设计的(感觉和冗长)。Pinject从头开始为Python设计。

  • Pinject的设计选择基于在Google工作的几位依赖注入专家多年的经验。一些常见且令人困惑或误导性的功能已完全从Pinject中省略。

  • Pinject 具有出色的错误信息。它们会明确告诉你哪里出了错,以及具体的错误位置。与其他依赖框架相比,这种改变应该会受到欢迎,因为它们具有大量且难以理解的堆栈跟踪。

查看 Pinject 以及其他类似库的最简单的入门示例。Pinject 应该更容易使用,更容易阅读,并且需要添加的样板代码更少。如果你发现并非如此,请发送电子邮件!

安装

安装 Pinject 最简单的方法是从 PyPI 获取最新发布的版本

sudo pip install pinject

如果你对开发版本感兴趣,你可以从 Test PyPI 安装下一个版本

sudo pip install \
    --no-deps \
    --no-cache \
    --upgrade \
    --index-url https://test.pypi.org/simple/ \
    pinject

你也可以查看所有源代码,包括测试、设计和 TODOs

git clone https://github.com/google/pinject

基本依赖注入

pinject 模块中最重要的函数是 new_object_graph()。它创建一个 ObjectGraph,你可以使用它通过依赖注入来实例化对象。如果你不向 new_object_graph() 传递任何参数,它将返回一个合理配置的默认 ObjectGraph

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

如你所见,你不需要告诉 Pinject 如何构建其 ObjectGraph,也不需要在代码中放置装饰器。Pinject 具有合理的默认值,允许它开箱即用。

Pinject 的 绑定 是一个 参数名 和一个 提供者 之间的关联。在上面的示例中,Pinject 创建了一个参数名 inner_class 和一个隐式创建的用于 InnerClass 类的提供者的绑定。它创建的绑定是 Pinject 知道在实例化 OuterClass 时应该将 InnerClass 的实例作为 inner_class 参数的值的依据。

隐式类绑定

Pinject 为类创建隐式绑定。隐式绑定假设你的代码遵循 PEP8 规范:你的类名使用 CamelCase 命名,你的参数名使用 lower_with_underscores 命名。Pinject 通过将单词转换为小写并使用下划线连接它们,将类名转换为可注入的参数名。它还将忽略类名前的任何下划线。

类名

参数名

Foo

foo

FooBar

foo_bar

_Foo

foo

_FooBar

foo_bar

如果两个类映射到同一个参数名,无论这些类是否在同一模块中或在不同的模块中,Pinject 不会为该参数名创建隐式绑定(尽管它不会引发错误)。

为隐式绑定查找类和提供者

到目前为止,示例没有告诉 new_object_graph() 它应该为哪些类创建隐式绑定。默认情况下,new_object_graph() 会查看所有导入的模块,但你可能偶尔想要限制 new_object_graph() 为哪些类创建隐式绑定。如果是这样,new_object_graph() 有两个参数用于此目的。

  • modules 参数指定在哪些(Python)模块中查找类;默认为 ALL_IMPORTED_MODULES

  • classes 参数指定一个确切的类列表;默认为 None

>>> class SomeClass(object):
...     def __init__(self, foo):
...         self.foo = foo
...
>>> class Foo(object):
...     pass
...
>>> obj_graph = pinject.new_object_graph(modules=None, classes=[SomeClass])
>>> # obj_graph.provide(SomeClass)  # would raise a NothingInjectableForArgError
>>> obj_graph = pinject.new_object_graph(modules=None, classes=[SomeClass, Foo])
>>> some_class = obj_graph.provide(SomeClass)
>>>

自动复制参数到字段

通过初始化器进行依赖注入时可能令人烦恼的一件事是,你需要编写 __init__() 方法来将参数复制到字段中。这些 __init__() 方法可能非常重复,尤其是当你有多个初始化器参数时。

>>> class ClassWithTediousInitializer(object):
...     def __init__(self, foo, bar, baz, quux):
...         self._foo = foo
...         self._bar = bar
...         self._baz = baz
...         self._quux = quux
...
>>>

Pinject 提供了可以用来避免重复初始化器体的装饰器。

  • @copy_args_to_internal_fields 在参数名前添加一个下划线,即它将名为 foo 的参数复制到名为 _foo 的字段中。这对于普通类很有用。

  • @copy_args_to_public_fields 以原样复制参数名,即它将名为 foo 的参数复制到名为 foo 的字段中。这对于数据对象很有用。

>>> class ClassWithTediousInitializer(object):
...     @pinject.copy_args_to_internal_fields
...     def __init__(self, foo, bar, baz, quux):
...         pass
...
>>> cwti = ClassWithTediousInitializer('a-foo', 'a-bar', 'a-baz', 'a-quux')
>>> print cwti._foo
'a-foo'
>>>

使用这些装饰器时,您通常会在初始化器的主体中传递参数,但如果需要,您也可以在那里放置其他语句。在执行初始化器主体之前,将参数复制到字段中。

这些装饰器可以应用于接受 **kwargs 的初始化器,但不能应用于接受 *pargs 的初始化器(因为这会不清楚使用哪个字段名)。

绑定规范

要创建比上面描述的隐式类绑定更复杂的任何绑定,您可以使用 绑定规范。绑定规范是任何继承自 BindingSpec 的 Python 类。绑定规范可以执行以下三项操作:

  • 它的 configure() 方法可以创建对类或实例的显式绑定,以及要求绑定而不创建它们。

  • 它的 dependencies() 方法可以返回依赖的绑定规范。

  • 它可以有提供者方法,为这些方法创建显式绑定。

函数 new_object_graph() 接收绑定规范实例的序列作为其 binding_specs 参数。 new_object_graph() 接收绑定规范实例,而不是绑定规范类,这样您就可以根据需要手动将任何初始依赖项注入到绑定规范中。

绑定规范通常应放在名为 binding_specs.py 的文件中,即使只有一个绑定规范,每个文件的名称也应该是复数形式。理想情况下,一个目录的函数应该可以用一个绑定规范来覆盖。如果不是这样,您可以在同一个 binding_specs.py 文件中创建多个绑定规范。如果您有如此多的绑定规范需要分割到多个文件中,您应该将每个文件命名为带有 _binding_specs.py 后缀的名称。

绑定规范 configure() 方法

Pinject 为类创建隐式绑定,但有时隐式绑定并不是您想要的。例如,如果您有一个 SomeReallyLongClassName,您可能不想将初始化器参数命名为 some_really_long_class_name,而只想使用更短的名字如 long_name,仅为此类。

对于这种情况,您可以使用绑定规范的 configure() 方法创建显式绑定。 configure() 方法接收一个函数 bind() 作为参数,并调用该函数来创建显式绑定。

>>> class SomeClass(object):
...     def __init__(self, long_name):
...         self.long_name = long_name
...
>>> class SomeReallyLongClassName(object):
...     def __init__(self):
...         self.foo = 'foo'
...
>>> class MyBindingSpec(pinject.BindingSpec):
...     def configure(self, bind):
...         bind('long_name', to_class=SomeReallyLongClassName)
...
>>> obj_graph = pinject.new_object_graph(binding_specs=[MyBindingSpec()])
>>> some_class = obj_graph.provide(SomeClass)
>>> print some_class.long_name.foo
'foo'
>>>

传递给绑定函数的 bind() 函数将其第一个参数(必须是参数名,作为 str)绑定到两种类型之一。

  • 使用 to_class 绑定到类。当使用绑定时,Pinject 注入该类的实例。

  • 使用 to_instance 绑定到某个对象的实例。每次使用绑定时,Pinject 都使用该实例。

>>> class SomeClass(object):
...     def __init__(self, foo):
...         self.foo = foo
...
>>> class MyBindingSpec(pinject.BindingSpec):
...     def configure(self, bind):
...         bind('foo', to_instance='a-foo')
...
>>> obj_graph = pinject.new_object_graph(binding_specs=[MyBindingSpec()])
>>> some_class = obj_graph.provide(SomeClass)
>>> print some_class.foo
'a-foo'
>>>

绑定规范的 configure() 方法还可以接受一个函数 require() 作为参数,并使用该函数来要求存在一个绑定,而实际上不定义该绑定。函数 require() 接收一个参数,即需要绑定名称的参数。

>>> class MainBindingSpec(pinject.BindingSpec):
...     def configure(self, require):
...         require('foo')
...
>>> class RealFooBindingSpec(pinject.BindingSpec):
...     def configure(self, bind):
...         bind('foo', to_instance='a-real-foo')
...
>>> class StubFooBindingSpec(pinject.BindingSpec):
...     def configure(self, bind):
...         bind('foo', to_instance='a-stub-foo')
...
>>> class SomeClass(object):
...     def __init__(self, foo):
...         self.foo = foo
...
>>> obj_graph = pinject.new_object_graph(
...     binding_specs=[MainBindingSpec(), RealFooBindingSpec()])
>>> some_class = obj_graph.provide(SomeClass)
>>> print some_class.foo
'a-real-foo'
>>> # pinject.new_object_graph(
... #    binding_specs=[MainBindingSpec()])  # would raise a MissingRequiredBindingError
...
>>>

在知道代码需要满足某些依赖,但有多个实现可以满足该依赖的情况下,不需要定义绑定即可要求绑定非常有用,例如,可能有真实RPC客户端和模拟RPC客户端。声明依赖意味着在调用 new_object_graph() 时,任何预期但缺失的绑定都会被提前检测到,而不是在程序运行过程中。

您会注意到上面的 configure() 方法有不同的签名,有时接受 bind 参数,有时接受 require 参数。 configure() 方法必须至少接受一个参数,该参数是 bindrequire 中的一个,它们可能同时接受这两个参数。Pinject 会传递您的 configure() 方法需要的任何参数或参数组合。

绑定规范依赖

绑定规范可以声明依赖。绑定规范通过返回其 dependencies() 方法中依赖绑定规范的实例的序列来声明其依赖。

>>> class ClassOne(object):
...    def __init__(self, foo):
...        self.foo = foo
....
>>> class BindingSpecOne(pinject.BindingSpec):
...     def configure(self, bind):
...         bind('foo', to_instance='foo-')
...
>>> class ClassTwo(object):
...     def __init__(self, class_one, bar):
...         self.foobar = class_one.foo + bar
...
>>> class BindingSpecTwo(pinject.BindingSpec):
...     def configure(self, bind):
...         bind('bar', to_instance='-bar')
...     def dependencies(self):
...         return [BindingSpecOne()]
...
>>> obj_graph = pinject.new_object_graph(binding_specs=[BindingSpecTwo()])
>>> class_two = obj_graph.provide(ClassTwo)
>>> print class_two.foobar
'foo--bar'
>>>

如果模块 A 中的类被注入到模块 B 中的类作为协作者,那么您应该考虑让模块 B 的绑定规范依赖于模块 A 的绑定规范。在上面的例子中,ClassOne 被注入到 ClassTwo 中,因此 BindingSpecTwoClassTwo 的绑定规范)依赖于 BindingSpecOneClassOne 的绑定规范)。

通过这种方式,您可以根据协作者依赖关系图构建一个绑定规范依赖关系图。

由于显式绑定不会发生冲突(请参阅下面的绑定优先级部分),绑定规范应该只具有永远不会发生选择使用的依赖。如果可能存在选择,那么在调用 new_object_graph() 时最好分别和明确地列出绑定规范。

绑定规范依赖可以是一个有向无环图(DAG);也就是说,绑定规范 A 可以是 B 和 C 的依赖,绑定规范 D 可以依赖于 B 和 C。尽管从 D 到 A 有多个依赖路径,但绑定规范 A 中的绑定只会被评估一次。

如果两个实例相等(通过 __eq__()),则作为 B 的依赖的 A 的绑定规范实例被认为是与作为 C 的依赖的实例相同的。在 BindingSpec 中的 __eq__() 的默认实现中,两个绑定规范相等,如果它们是精确相同的 Python 类型。如果您需要参数化绑定规范,即如果它接受一个或多个初始化器参数,以便两个绑定规范的实例可能表现出不同的行为,则需要重写 __eq__()(以及 __hash__())。

>>> class SomeBindingSpec(pinject.BindingSpec):
...     def __init__(self, the_instance):
...         self._the_instance = the_instance
...     def configure(self, bind):
...         bind('foo', to_instance=self._the_instance)
...     def __eq__(self, other):
...         return (type(self) == type(other) and
...                 self._the_instance == other._the_instance)
...     def __hash__(self):
...         return hash(type(self)) ^ hash(self._the_instance)
...
>>>

提供者方法

如果创建类的实例需要比调用其初始化器和注入初始化器参数更多的工作,则可以为其编写 提供者方法。Pinject 可以使用提供者方法来实例化用作其他参数注入值的对象。

Pinject会检查绑定规范中名为provider方法的方法,并为它们创建显式绑定。

>>> class SomeClass(object):
...     def __init__(self, foo):
...         self.foo = foo
...
>>> class SomeBindingSpec(pinject.BindingSpec):
...     def provide_foo(self):
...         return 'some-complex-foo'
...
>>> obj_graph = pinject.new_object_graph(binding_specs=[SomeBindingSpec()])
>>> some_class = obj_graph.provide(SomeClass)
>>> print some_class.foo
'some-complex-foo'
>>>

Pinject会检查以provide_开头的方法名称的绑定规范,并假定这些方法为其方法名剩余部分表示的内容提供者。例如,Pinject假定方法provide_foo_bar()是arg名称foo_bar的提供者方法。

当Pinject调用提供者方法时,它会注入所有没有默认值的提供者方法的参数。

>>> class SomeClass(object):
...     def __init__(self, foobar):
...         self.foobar = foobar
...
>>> class SomeBindingSpec(pinject.BindingSpec):
...     def provide_foobar(self, bar, hyphen='-'):
...         return 'foo' + hyphen + bar
...     def provide_bar(self):
...         return 'bar'
...
>>> obj_graph = pinject.new_object_graph(binding_specs=[SomeBindingSpec()])
>>> some_class = obj_graph.provide(SomeClass)
>>> print some_class.foobar
'foo-bar'
>>>

绑定优先级

绑定具有优先级:显式绑定优先于隐式绑定。

  • 显式绑定来自绑定规范。

  • 隐式绑定是为传递给new_object_graph()modulesclasses参数中的类创建的绑定。

Pinject将优先考虑显式绑定而不是隐式绑定。

>>> class SomeClass(object):
...     def __init__(self, foo):
...         self.foo = foo
...
>>> class Foo(object):
...     pass
...
>>> class SomeBindingSpec(pinject.BindingSpec):
...     def configure(self, bind):
...         bind('foo', to_instance='foo-instance')
...
>>> obj_graph = pinject.new_object_graph(binding_specs=[SomeBindingSpec()])
>>> some_class = obj_graph.provide(SomeClass)
>>> print some_class.foo
'foo-instance'
>>>

如果你有两个名称相同的类,Pinject将有两个不同(因此冲突)的隐式绑定。但是,除非你尝试使用这些绑定,否则Pinject不会抱怨。但是,如果你尝试创建不同的(因此冲突的)显式绑定,Pinject会抱怨。

安全性

Pinject试图在有帮助和保证安全之间取得平衡。有时,你可能希望或需要改变这种平衡。

new_object_graph()默认使用隐式绑定。如果你担心可能会意外注入类或无意中使用提供者函数,则可以通过将only_use_explicit_bindings=True设置,使new_object_graph()忽略隐式绑定。这样做的话,Pinject将只使用显式绑定。

如果你想要将隐式绑定提升为显式绑定,你可以使用@inject()对相应的类进行注释。这个@inject()装饰器允许你创建显式绑定,而无需创建绑定规范,只要你能够接受绑定的默认值(例如,没有对参数进行注释,见下文)。

>>> class ExplicitlyBoundClass(object):
...     @pinject.inject()
...     def __init__(self, foo):
...         self.foo = foo
...
>>> class ImplicitlyBoundClass(object):
...     def __init__(self, foo):
...         self.foo = foo
...
>>> class SomeBindingSpec(pinject.BindingSpec):
...     def configure(self, bind):
...         bind('foo', to_instance='explicit-foo')
...
>>> obj_graph = pinject.new_object_graph(binding_specs=[SomeBindingSpec()],
...     only_use_explicit_bindings=True)
>>> # obj_graph.provide(ImplicitlyBoundClass)  # would raise a NonExplicitlyBoundClassError
>>> some_class = obj_graph.provide(ExplicitlyBoundClass)
>>> print some_class.foo
'explicit-foo'
>>>

你还可以使用@annotated_arg()(见下文),无论是否使用@inject(),都将隐式绑定提升为显式绑定。

(Pinject的先前版本包括一个@injectable装饰器。现在已弃用,改为使用@inject()。请注意,@inject()需要括号,而@injectable不需要。)

在宽容度的一端,Pinject默认会在提供者方法返回None时抱怨。如果你真的想关闭这种默认行为,可以将allow_injecting_none=True传递给new_object_graph()

注释

Pinject的annotations允许你为相同的参数名称注入不同的对象。例如,你可能在代码库的不同部分有两个同名类,你希望在代码库的不同部分使用相同的参数名称。

在参数方面,注释告诉Pinject只使用包含注释对象的绑定进行注入。你可以在初始化器或提供者方法上使用@annotate_arg()来指定注释对象。

在绑定方面,注释改变绑定,使绑定的键包括注释对象。当在绑定规范的configure()方法中使用bind()时,你可以传递一个annotated_with参数来指定注释对象。

>>> class SomeClass(object):
...     @pinject.annotate_arg('foo', 'annot')
...     def __init__(self, foo):
...         self.foo = foo
...
>>> class SomeBindingSpec(pinject.BindingSpec):
...     def configure(self, bind):
...         bind('foo', annotated_with='annot', to_instance='foo-with-annot')
...         bind('foo', annotated_with=12345, to_instance='12345-foo')
...
>>> obj_graph = pinject.new_object_graph(binding_specs=[SomeBindingSpec()])
>>> some_class = obj_graph.provide(SomeClass)
>>> print some_class.foo
'foo-with-annot'
>>>

在绑定方面,当定义提供者方法时,您可以使用 @provides() 装饰器。该装饰器允许您传递一个 annotated_with 参数来指定注解对象。装饰器的第一个参数 arg_name 还允许您覆盖提供者需要的参数名称。这虽然是可选的,但在您希望相同的绑定规范对于相同的参数名称有两个不同的提供者方法时非常有用。(否则,这些方法的名称需要相同,因为它们是针对相同的参数名称。)

>>> class SomeClass(object):
...     @pinject.annotate_arg('foo', 'annot')
...     def __init__(self, foo):
...         self.foo = foo
...
>>> class SomeBindingSpec(pinject.BindingSpec):
...     @pinject.provides('foo', annotated_with='annot')
...     def provide_annot_foo(self):
...         return 'foo-with-annot'
...     @pinject.provides('foo', annotated_with=12345)
...     def provide_12345_foo(self):
...         return '12345-foo'
...
>>> obj_graph = pinject.new_object_graph(binding_specs=[SomeBindingSpec()])
>>> some_class = obj_graph.provide(SomeClass)
>>> print some_class.foo
'foo-with-annot'
>>>

当通过将 require 参数传递到绑定规范的 configure() 方法来要求绑定时,您可以传递 annotated_with 参数来要求一个注解绑定。

>>> class MainBindingSpec(pinject.BindingSpec):
...     def configure(self, require):
...         require('foo', annotated_with='annot')
...
>>> class NonSatisfyingBindingSpec(pinject.BindingSpec):
...     def configure(self, bind):
...         bind('foo', to_instance='an-unannotated-foo')
...
>>> class SatisfyingBindingSpec(pinject.BindingSpec):
...     def configure(self, bind):
...         bind('foo', annotated_with='annot', to_instance='an-annotated-foo')
...
>>> obj_graph = pinject.new_object_graph(
...     binding_specs=[MainBindingSpec(), SatisfyingBindingSpec()])  # works
>>> # obj_graph = pinject.new_object_graph(
... #     binding_specs=[MainBindingSpec(),
... #                    NonSatisfyingBindingSpec()])  # would raise a MissingRequiredBindingError
>>>

只要对象实现了 __eq__()__hash__(),您就可以使用任何类型的对象作为注解对象。

作用域

默认情况下,Pinject 记录它注入到(可能是注解的)参数中的对象,以便它可以将其注入到具有相同名称的其他参数中。这意味着对于每个参数名称,默认情况下将创建绑定到的类的单个实例,或者由提供者方法返回的单个实例。

>>> class SomeClass(object):
...     def __init__(self, foo):
...         self.foo = foo
...
>>> class SomeBindingSpec(pinject.BindingSpec):
...     def provide_foo(self):
...         return object()
...
>>> obj_graph = pinject.new_object_graph(binding_specs=[SomeBindingSpec()])
>>> some_class_1 = obj_graph.provide(SomeClass)
>>> some_class_2 = obj_graph.provide(SomeClass)
>>> print some_class_1.foo is some_class_2.foo
True
>>>

在某些情况下,您可能希望始终或有时创建新的实例,而不是在每次注入时重复使用它们。如果这样,您需要使用 作用域

作用域控制缓存(即,缓存)。作用域可以选择始终缓存、有时缓存或从不缓存。

Pinject 有两个内置的作用域。默认的 单例作用域 (SINGLETON) 总是缓存。另一个内置选项是 原型作用域 (PROTOTYPE),它根本不缓存。

每个绑定都与一个作用域相关联。您可以通过使用 @in_scope() 装饰提供者方法,或者在绑定规范的 configure() 方法中将 in_scope 参数传递给 bind() 来为绑定指定一个作用域。

>>> class SomeClass(object):
...     def __init__(self, foo):
...         self.foo = foo
...
>>> class SomeBindingSpec(pinject.BindingSpec):
...     @pinject.provides(in_scope=pinject.PROTOTYPE)
...     def provide_foo(self):
...         return object()
...
>>> obj_graph = pinject.new_object_graph(binding_specs=[SomeBindingSpec()])
>>> some_class_1 = obj_graph.provide(SomeClass)
>>> some_class_2 = obj_graph.provide(SomeClass)
>>> print some_class_1.foo is some_class_2.foo
False
>>>

如果绑定未显式指定作用域,则它处于单例作用域。隐式类绑定始终处于单例作用域。

类绑定的缓存是在类级别而不是绑定键级别工作的。这意味着,如果您将两个参数名称(或具有两个不同注解的相同参数名称)绑定到同一类,并且该类处于缓存作用域中,那么在注入不同的参数名称时,将提供相同的类实例。

>>> class InjectedClass(object):
...     pass
...
>>> class SomeObject(object):
...     def __init__(self, foo, bar):
...         self.foo = foo
...         self.bar = bar
...
>>> class SomeBindingSpec(pinject.BindingSpec):
...     def configure(self, bind):
...         bind('foo', to_class=InjectedClass)
...         bind('bar', to_class=InjectedClass)
...
>>> obj_graph = pinject.new_object_graph(
...     binding_specs=[SomeBindingSpec()])
>>> some_object = obj_graph.provide(SomeObject)
>>> print some_object.foo is some_object.bar
True
>>>

Pinject 以这种方式缓存类绑定,因为这更可能是您想要的结果:如果单例作用域中绑定两个不同的参数名称到同一类,您希望只有一个类的实例,即使它可能被注入到多个地方。

提供者绑定

有时,您需要注入的不仅仅是某个类的单个实例,而是需要按需创建实例的能力。(显然,当您使用的绑定不在单例作用域中时,这最有用,否则您总是得到相同的实例,您最好直接注入那个实例。)

您可以注入 Pinject 对象图,但您必须手动执行依赖注入(Pinject 不会注入自身!),并且您会注入大量您实际上只需要实例化一种类型的对象的能力。

为了解决这个问题,Pinject 为每个已绑定的参数名称创建 提供者绑定。它会检查参数名称中是否有前缀 provide_,如果有这个前缀,它就假定你想注入一个提供者函数来处理参数名称剩余的部分。例如,如果你有一个名为 provide_foo_bar 的参数,那么 Pinject 将注入一个无参数的函数,当调用该函数时,提供与参数名称 foo_bar 绑定的任何内容。

>>> class Foo(object):
...   def __init__(self):
...     self.forty_two = 42
...
>>> class SomeBindingSpec(pinject.BindingSpec):
...     def configure(self, bind):
...         bind('foo', to_class=Foo, in_scope=pinject.PROTOTYPE)
...
>>> class NeedsProvider(object):
...     def __init__(self, provide_foo):
...         self.provide_foo = provide_foo
...
>>> obj_graph = pinject.new_object_graph(binding_specs=[SomeBindingSpec()])
>>> needs_provider = obj_graph.provide(NeedsProvider)
>>> print needs_provider.provide_foo() is needs_provider.provide_foo()
False
>>> print needs_provider.provide_foo().forty_two
42
>>>

Pinject 总是查找 provide_ 前缀作为注入提供者函数的信号,无论它在哪里注入依赖项(初始化器参数、绑定规范提供者方法等)。这也意味着,例如,将名为 ProvideFooBar 的类的实例注入到名为 provide_foo_bar 的参数中可能相当困难,但假设你的类命名是以名词短语而不是动词短语为主,这不应该成为问题。

请注意:不要混淆

  • 提供者绑定,它允许你通过提供者函数注入名为 provide_something 的参数;和

  • 提供者方法,它是绑定规范的提供者,提供某些参数名称的实例。

部分注入

提供者绑定在你想按需创建类实例时很有用。但无参数提供者函数总是会以相同的方式(在给定的作用域内)返回实例。有时,你可能想要参数化提供的实例,例如,基于运行时用户配置。你想要的能力是创建实例,其中初始化数据的一部分在运行时以实例形式提供,另一部分作为依赖项注入。

为了做到这一点,其他依赖注入库要求你定义工厂类。你将依赖项注入到工厂类的初始化器函数中,然后使用实例数据调用工厂类的创建方法。

>>> class WidgetFactory(object):
...     def __init__(self, widget_polisher):
...         self._widget_polisher = widget_polisher
...     def new(self, color):  # normally would contain some non-trivial code...
...         return some_function_of(self._widget_polisher, color)
...
>>> class SomeBindingSpec(pinject.BindingSpec):
...     def provide_something_with_colored_widgets(self, colors, widget_factory):
...         return SomethingWithColoredWidgets(
...             [widget_factory.new(color) for color in colors])
...
>>>

你可以在 Pinject 中遵循这种模式,但这涉及到工厂类的无聊样板代码,保存初始化器注入的依赖项以在创建方法中使用。此外,你必须创建另一个 ...Factory 类,这让你感觉像是在用 java 编程,而不是 python。

作为一个更少重复的替代方案,Pinject 允许你在提供者绑定的提供者函数上使用 部分注入。你使用 @inject() 装饰器提前告诉 Pinject 你期望传递哪些参数(与自动注入相比),然后你在调用提供者函数时直接传递这些参数。

>>> class SomeBindingSpec(pinject.BindingSpec):
...     @pinject.inject(['widget_polisher'])
...     def provide_widget(self, color, widget_polisher):
...         return some_function_of(widget_polisher, color)
...     def provide_something_needing_widgets(self, colors, provide_widget):
...         return SomethingNeedingWidgets(
...             [provide_widget(color) for color in colors])
...
>>>

@inject() 的第一个参数 arg_names 指定了装饰方法应该注入为依赖项的哪些参数。如果指定,它必须是非空序列,包含装饰方法参数的名称。其余的装饰方法参数将直接传递。

在上面的例子中,请注意,尽管有一个名为 provide_widget() 的方法和一个名为 provide_something_needing_widgets() 的参数名为 provide_widget,但这些并不完全相同!后者是前者的一个依赖注入包装器。包装器确保 color 参数直接传递,然后注入 widget_polisher 依赖项。

@inject() 装饰器用于指定直接传递给提供者绑定到提供者方法(如上例所示)和提供者绑定到类(你可以在初始化器中直接传递参数,如下例所示)的参数。

>>> class Widget(object):
...     @pinject.inject(['widget_polisher'])
...     def __init__(self, color, widget_polisher):
...         pass  # normally something involving color and widget_polisher
...
>>> class SomeBindingSpec(pinject.BindingSpec):
...     def provide_something_needing_widgets(self, colors, provide_widget):
...         return SomethingNeedingWidgets(
...             [provide_widget(color) for color in colors])
...
>>>

装饰器 @inject() 还接受一个 all_except 参数。如果您觉得使用它比使用(第一个位置参数的)arg_names 参数更清晰和简洁,那么您可以使用它来指定哪些参数是 注入的(即直接传递的参数)。

>>> class Widget(object):
...     # equivalent to @pinject.inject(['widget_polisher']):
...     @pinject.inject(all_except=['color'])
...     def __init__(self, color, widget_polisher):
...         pass  # normally something involving color and widget_polisher
...
>>>

如果同时省略了 arg_namesall_except,则所有参数都将由 Pinject 注入,并且都不会直接传递。(arg_namesall_except 不能同时指定。)通配符位置参数和关键字参数(即 *pargs**kwargs)始终直接传递,而不是注入。

如果您使用 @inject() 来标记提供者方法(或初始化器)的至少一个参数为直接传递,那么您可能不再可以直接注入该提供者方法的相应参数名。您必须使用提供者绑定来注入提供者函数,然后按照上面的示例传递所需的直接参数。

自定义范围

如果您想创建自己的自定义范围,则自定义范围非常有用。当您有一些需要重复使用(即缓存)的对象,但其生命周期比程序整个生命周期更短时,自定义范围很有用。

自定义范围是任何实现了 Scope 接口的类。

class Scope(object):
    def provide(self, binding_key, default_provider_fn):
        raise NotImplementedError()

传递给 provide()binding_key 将是一个实现 __eq__()__hash__() 的对象,但其他方面是透明的(您不需要检查它)。您可以大致将绑定键视为封装了参数名和注解(如果有)。传递给 provide()default_provider_fn 是一个零参数函数,当调用它时,提供应该提供的任何实例。

范围 provide() 函数的职责是如果可用且适当,则返回缓存的实例,否则返回(并可能缓存)调用默认提供者函数的结果。

范围通常还有其他方法来控制清除范围的缓存。例如,范围可能有“进入范围”和“退出范围”方法,或者一个单独的直接“清除缓存”方法。当将自定义范围传递给 Pinject 时,您的代码应该保持对自定义范围的引用,并在适当的时候使用该引用来清除范围的缓存。

您可以通过将范围作为 id_to_scope 参数传递给 new_object_graph() 的范围标识符到范围的映射来使用一个或多个自定义范围。

>>> class MyScope(pinject.Scope):
...     def __init__(self):
...         self._cache = {}
...     def provide(self, binding_key, default_provider_fn):
...         if binding_key not in self._cache:
...             self._cache[binding_key] = default_provider_fn()
...         return self._cache[binding_key]
...     def clear(self):
...         self._cache = {}
...
>>> class SomeClass(object):
...     def __init__(self, foo):
...         self.foo = foo
...
>>> class SomeBindingSpec(pinject.BindingSpec):
...     @pinject.provides(in_scope='my custom scope')
...     def provide_foo(self):
...         return object()
...
>>> my_scope = MyScope()
>>> obj_graph = pinject.new_object_graph(
...     binding_specs=[SomeBindingSpec()],
...     id_to_scope={'my custom scope': my_scope})
>>> some_class_1 = obj_graph.provide(SomeClass)
>>> some_class_2 = obj_graph.provide(SomeClass)
>>> my_scope.clear()
>>> some_class_3 = obj_graph.provide(SomeClass)
>>> print some_class_1.foo is some_class_2.foo
True
>>> print some_class_2.foo is some_class_3.foo
False
>>>

范围标识符可以是任何实现了 __eq__()__hash__() 的对象。

如果您计划在多线程环境中使用 Pinject(即使您现在没有计划,但将来可能会),您应该确保您的自定义范围是线程安全的。上面的示例自定义范围可以简单地(但更详细地)重写为线程安全,如下面的示例所示。该锁是可重入的,因此 MyScope 中的某个内容可以注入到 MyScope 中的另一个内容。

>>> class MyScope(pinject.Scope):
...     def __init__(self):
...         self._cache = {}
...         self._rlock = threading.RLock()
...     def provide(self, binding_key, default_provider_fn):
...         with self._rlock:
...             if binding_key not in self._cache:
...                 self._cache[binding_key] = default_provider_fn()
...             return self._cache[binding_key]
...     def clear(self):
...         with self._rlock:
...             self._cache = {}
>>>

范围可访问性

为了防止您将对象注入到不合适的地方,您可能希望根据范围验证一个对象被注入到另一个对象中。

例如,您可能为程序处理的 HTTP 请求创建了一个自定义范围。请求范围内的对象将在单个 HTTP 请求的持续时间内被缓存。您可能想验证请求范围内的对象永远不会被注入到单例范围内的对象中。这种注入可能没有语义意义,因为它会使与单个 HTTP 请求相关的内容在整个程序的生命周期中使用。

Pinject 允许您将验证函数作为 is_scope_usable_from_scope 参数传递给 new_object_graph()。此函数接受两个作用域标识符,如果第一个作用域中的对象可以注入到第二个作用域的对象中,则返回 True

>>> class RequestScope(pinject.Scope):
...     def start_request(self):
...         self._cache = {}
...     def provide(self, binding_key, default_provider_fn):
...         if binding_key not in self._cache:
...             self._cache[binding_key] = default_provider_fn()
...         return self._cache[binding_key]
...
>>> class SomeClass(object):
...     def __init__(self, foo):
...         self.foo = foo
...
>>> class SomeBindingSpec(pinject.BindingSpec):
...     @pinject.provides(in_scope=pinject.SINGLETON)
...     def provide_foo(bar):
...         return 'foo-' + bar
...     @pinject.provides(in_scope='request scope')
...     def provide_bar():
...         return '-bar'
...
>>> def is_usable(scope_id_inner, scope_id_outer):
...     return not (scope_id_inner == 'request scope' and
...                 scope_id_outer == scoping.SINGLETON)
...
>>> my_request_scope = RequestScope()
>>> obj_graph = pinject.new_object_graph(
...     binding_specs=[SomeBindingSpec()],
...     id_to_scope={'request scope': my_request_scope},
...     is_scope_usable_from_scope=is_usable)
>>> my_request_scope.start_request()
>>> # obj_graph.provide(SomeClass)  # would raise a BadDependencyScopeError
>>>

默认的作用域可访问性验证器允许任何作用域的对象注入到任何其他作用域的对象中。

更改命名约定

如果您的代码遵循 PEP8 命名约定,那么您可能对默认的隐式绑定(其中类 FooBar 绑定到参数名 foo_bar)以及 provide_foo_bar() 是参数名 foo_bar 的绑定规范的提供方法)感到满意。

但如果不是这样,请继续阅读!

自定义隐式绑定

new_object_graph() 接受一个 get_arg_names_from_class_name 参数。这是用于确定隐式类绑定的函数。该函数接受一个类名(例如,FooBar)并返回该类应隐式绑定的参数名(例如,['foo_bar'])。它的默认行为在上面的“隐式类绑定”部分中描述,但该默认行为可以被覆盖。

例如,假设您的代码使用一个库,许多类的名称以字母 X 开头(例如,XFooBar),并且您希望能够将其绑定到相应的参数名,而不包含开头的 X(例如,foo_bar)。

>>> import re
>>> def custom_get_arg_names(class_name):
...     stripped_class_name = re.sub('^_?X?', '', class_name)
...     return [re.sub('(?!^)([A-Z]+)', r'_\1', stripped_class_name).lower()]
...
>>> print custom_get_arg_names('XFooBar')
['foo_bar']
>>> print custom_get_arg_names('XLibraryClass')
['library_class']
>>> class OuterClass(object):
...     def __init__(self, library_class):
...         self.library_class = library_class
...
>>> class XLibraryClass(object):
...     def __init__(self):
...         self.forty_two = 42
...
>>> obj_graph = pinject.new_object_graph(
...     get_arg_names_from_class_name=custom_get_arg_names)
>>> outer_class = obj_graph.provide(OuterClass)
>>> print outer_class.library_class.forty_two
42
>>>

传递给 new_object_graph()get_arg_names_from_class_name 参数的函数可以返回任意数量的参数名。如果它总是返回空列表(即,如果它是 lambda _: []),则禁用隐式类绑定。

自定义绑定规范方法名称

默认情况下,用于配置绑定和声明依赖项的标准绑定规范方法名为 configuredependencies。如果您需要,可以通过将 configure_method_name 和/或 dependencies_method_name 作为参数传递给 new_object_graph() 来更改它们的名称。

>>> class NonStandardBindingSpec(pinject.BindingSpec):
...     def Configure(self, bind):
...         bind('forty_two', to_instance=42)
...
>>> class SomeClass(object):
...     def __init__(self, forty_two):
...         self.forty_two = forty_two
...
>>> obj_graph = pinject.new_object_graph(
...     binding_specs=[NonStandardBindingSpec()],
...     configure_method_name='Configure')
>>> some_class = obj_graph.provide(SomeClass)
>>> print some_class.forty_two
42
>>>

自定义提供者方法名称

new_object_graph() 接受一个 get_arg_names_from_provider_fn_name 参数。这是用于在绑定规范上识别提供者方法的函数。该函数接受一个潜在提供者方法的名称(例如,provide_foo_bar)并返回提供者方法是提供者的参数名(如果有),例如(['foo_bar'])。它的默认行为在上面的“提供者方法”部分中描述,但该默认行为可以被覆盖。

例如,假设您为一家大型公司工作,该公司的 Python 风格指南要求您使用驼峰命名法命名函数,因此您需要将参数名 foo_bar 的提供者方法命名为 ProvideFooBar 而不是 provide_foo_bar

>>> import re
>>> def CustomGetArgNames(provider_fn_name):
...     if provider_fn_name.startswith('Provide'):
...         provided_camelcase = provider_fn_name[len('Provide'):]
...         return [re.sub('(?!^)([A-Z]+)', r'_\1', provided_camelcase).lower()]
...     else:
...         return []
...
>>> print CustomGetArgNames('ProvideFooBar')
['foo_bar']
>>> print CustomGetArgNames('ProvideFoo')
['foo']
>>> class SomeClass(object):
...     def __init__(self, foo):
...         self.foo = foo
...
>>> class SomeBindingSpec(pinject.BindingSpec):
...     def ProvideFoo(self):
...         return 'some-foo'
...
>>> obj_graph = pinject.new_object_graph(
...     binding_specs=[SomeBindingSpec()],
...     get_arg_names_from_provider_fn_name=CustomGetArgNames)
>>> some_class = obj_graph.provide(SomeClass)
>>> print some_class.foo
'some-foo'
>>>

传递给 new_object_graph()get_arg_names_from_provider_fn_name 参数的函数可以返回任意数量的参数名。如果它返回空列表,则假定该潜在提供者方法实际上不是一个提供者方法。

杂项

Pinject 抛出有用的异常,其消息包括错误所在的文件和行号。因此,Pinject 默认会缩短它引发的异常的堆栈跟踪,以便您不会看到 Pinject 库内部的多级函数调用。

在某些情况下,完整的堆栈跟踪是有帮助的,例如调试Pinject时,或者当您的代码调用Pinject时,Pinject又回调到您的代码,您的代码又回调到Pinject。在这种情况下,要禁用异常堆栈缩短,您可以将use_short_stack_traces=False传递给new_object_graph()

注意事项

Pinject有一些需要注意的事项。

线程安全

Pinject的默认作用域是SINGLETON。如果您有一个多线程程序,那么很可能Pinject从单例作用域提供的一些或所有内容将在多个线程中使用。因此,确保这些类是线程安全的非常重要。

同样,您的自定义作用域类也必须是线程安全的。即使它们提供的对象仅在一个线程中使用,也可能对象图(以及作用域本身)会在多个线程中同时使用。

请记住,在您的自定义作用域类上使锁可重入,或者以其他方式处理在您的自定义作用域中尝试注入另一个对象的情况。

这就是当前的所有注意事项。

简短摘要

如果您已经熟悉如Guice之类的依赖注入库,本节将为您提供关于Pinject的简短高级总结,以及它与其他依赖注入库的相似之处或不同之处。(如果您不理解,没关系。本文档的其余部分涵盖了这里列出的所有内容。)

  • Pinject使用代码和装饰器来配置注入,而不是单独的配置文件。

  • 绑定以参数名称为键,(而不是类类型,因为Python是动态类型的)。

  • Pinject会自动创建对some_class参数名称的绑定,对于SomeClass类。

  • 您可以让Pinject仅从绑定规范和类中创建绑定,这些类的__init__()方法被标记为@inject()

  • 绑定规范是一个创建显式绑定的类。

  • 绑定规范可以将参数名称绑定到类或实例。

  • 绑定规范可以将参数名称foo绑定到提供者方法provide_foo()

  • 绑定规范可以依赖(即,包含)其他绑定规范。

  • 您可以对参数和绑定进行注释,以区分相同参数名称的参数/绑定。

  • Pinject有两个内置作用域:“singleton”(始终缓存;默认)和“prototype”(永不缓存)。

  • 您可以定义自定义作用域,并可以配置哪些作用域可以从哪些其他作用域访问。

  • Pinject默认不允许注入None,但您可以禁用此检查。

变更日志

v0.13: master

v0.12: 2018年11月28日

  • 支持Python 3

  • 添加两位维护者:@trein和@huan

v0.10.2

  • 修复了错误:允许绑定规范仅包含提供者方法。

v0.10.1

  • 修复了错误:允许省略自定义命名的configure()绑定规范方法。

v0.10

  • BindingSpec添加了默认的__eq__(),以便DAG绑定规范依赖项可以有相等等价但不相同的依赖项。

  • 允许自定义configure()dependencies()绑定规范方法名称。

  • 弃用@injectable,改为@inject

  • 添加了部分注入。

  • 添加了require参数,允许绑定规范configure方法声明但不定义绑定。

  • 将测试(以及可能的一般功能)的速度提高了10倍。

  • 记录了更多的设计决策。

  • 添加了@copy_args_to_internal_fields@copy_args_to_public_fields

  • InjectableDecoratorAppliedToNonInitError重命名为DecoratorAppliedToNonInitError

v0.9

  • 添加了对公共参数的Python类型验证。

  • 所有Pinject引发的异常的错误信息已改进。

  • new_object_graph() 添加了 use_short_stack_traces 参数。

  • 允许单个提供者方法上有多个 @provides

v0.8

  • 第一个发布版本。

维护者

  • Kurt Steinkraus @kurt

  • Guilherme Trein @trein

  • Huan LI @huan

许可证

Apache-2.0

Pinject 和 Google

尽管Google拥有此项目的版权,但该项目不是官方的Google产品。

项目详情


下载文件

下载您平台上的文件。如果您不确定选择哪个,请了解更多关于 安装包 的信息。

源分布

pinject-0.14.1.tar.gz (60.3 kB 查看散列)

上传时间

构建分布

pinject-0.14.1-py3-none-any.whl (43.0 kB 查看散列)

上传时间 Python 3

由以下机构支持