跳转到主要内容

使用附加组件(以前称为ObjectRoles)动态扩展其他对象

项目描述

(版本0.6新增:`Registry`基类和`ClassAddOn.for_frame()`类方法。)

在任何足够大的应用程序或框架中,通常会将许多不同的关注点放入同一个类中。例如,您可能将业务逻辑、持久化代码和UI都挤入一个单独的类中。各种不同操作的属性和方法名称都被推入一个单一的命名空间——即使在使用混入类时也是如此。

然而,将关注点分离到不同的对象中,可以使编写可重用和可独立测试的组件更容易。附加组件包(peak.util.addons)允许您使用附加组件类来管理关注点。

附加组件类类似于动态混入,但具有自己的私有属性和方法命名空间。使用附加组件实现的关注点可以在运行时添加到任何具有可写__dict__属性的对象,或者任何可弱引用的对象。

附加组件类也类似于适配器,但每次请求一个实例时,如果可能,返回现有实例。这样,附加组件可以跟踪持续状态。例如,持久化附加组件可能会跟踪其主题是否已保存到磁盘。

>>> from peak.util.addons import AddOn

>>> class Persistence(AddOn):
...     saved = True
...     def changed(self):
...         self.saved = False
...     def save_if_needed(self):
...         if not self.saved:
...             print "saving"
...             self.saved = True

>>> class Thing: pass
>>> aThing = Thing()

>>> Persistence(aThing).saved
True
>>> Persistence(aThing).changed()
>>> Persistence(aThing).saved
False
>>> Persistence(aThing).save_if_needed()
saving
>>> Persistence(aThing).save_if_needed() # no action taken

这使得我们很容易编写循环来保存大量对象,因为我们不需要担心初始化持久化附加组件的状态。一个类不需要从特殊的基类继承才能跟踪这种状态,也不需要知道如何初始化它。

当然,在持久化的情况下,一个类确实需要知道何时调用持久化方法,以指示变更并请求保存。然而,提供此类附加功能的库也可以提供装饰器和其它工具来简化这个过程,同时仍然在很大程度上与涉及的对象保持独立。

实际上,AddOns库的创建就是为了简化使用函数或方法装饰器来实现功能。例如,可以创建一个@synchronized装饰器来安全地锁定一个对象——请参见下文线程关注点部分中的示例。

总之,AddOns库为你提供了一种基本形式的面向方面编程(AOP),允许你使用私有命名空间向对象附加(或在AspectJ术语中“引入”)额外的属性和方法。(如果你还想执行AspectJ风格的“建议”,可以使用PEAK-Rules包结合插件进行“之前”、“之后”和“周围”建议。)

基本API

如果你需要,你可以查询插件的存在

>>> Persistence.exists_for(aThing)
True

默认情况下,它不存在

>>> anotherThing = Thing()
>>> Persistence.exists_for(anotherThing)
False

直到你直接引用它,例如

>>> Persistence(aThing) is Persistence(anotherThing)
False

此时它当然存在

>>> Persistence.exists_for(anotherThing)
True

并保持其状态,与主题相关联

>>> Persistence(anotherThing) is Persistence(anotherThing)
True

除非你删除它(或其主题被垃圾收集)

>>> Persistence.delete_from(anotherThing)
>>> Persistence.exists_for(anotherThing)
False

插件键和实例

插件存储在其主题的__dict__中,如果没有(或是一个具有只读__dict__的类型对象),则通过弱引用与主题相关联的专用字典中存储。

默认情况下,字典键是插件类本身,因此每个主题只有一个插件实例

>>> aThing.__dict__
{<class 'Persistence'>: <Persistence object at...>}

但在某些情况下,你可能希望为特定主题拥有多个给定插件类的实例。(例如,PEAK-Rules使用插件来表示规则中包含的不同表达式上的索引。)为此,你可以重新定义你的插件__init__方法,使其除了主题外还接受额外的参数。这些额外参数成为实例存储下的键的一部分,这样就可以为给定对象存在多个插件实例

>>> class Index(AddOn, dict):
...     def __init__(self, subject, expression):
...         self.expression = expression

>>> something = Thing()
>>> Index(something, "x>y")["a"] = "b"
>>> dir(something)
['__doc__', '__module__', (<class 'Index'>, 'x>y')]

>>> "a" in Index(something, "z<22")
False

>>> Index(something, "x>y")
{'a': 'b'}

>>> Index(something, "x>y").expression
'x>y'

>>> dir(something)
['__doc__', '__module__', (<class 'Index'>, 'x>y'), (<class 'Index'>, 'z<22')]

>>> Index.exists_for(something, 'x>y')
True

>>> Index.exists_for(anotherThing, 'q==42')
False

默认情况下,插件类的键要么是类本身,要么是包含类的元组,后面跟着插件主题构造函数调用后出现的任何参数。然而,你可以在子类中重新定义addon_key类方法,并将其更改为不同的操作。例如,你可以让不同的插件类生成重叠的键,或者你可以使用参数的属性来生成键。你甚至可以生成一个字符串键,以使插件作为属性附加!

>>> class Leech(AddOn):
...     def addon_key(cls):
...         return "__leech__"
...     addon_key = classmethod(addon_key)

>>> something = Thing()

>>> Leech(something) is something.__leech__
True

addon_key方法只接收构造函数调用中主题之后出现的参数。因此,在上面的例子中,它没有接收任何参数。如果我们用额外的参数调用它,我们将得到一个错误

>>> Leech(something, 42)
Traceback (most recent call last):
  ...
TypeError: addon_key() takes exactly 1 argument (2 given)

自然,你的addon_key__init__(以及/或__new__)方法也应该就参数的数量及其含义达成一致!

通常,你应该将你的插件类(或某些插件类)作为键的一部分,以便使与他人的插件类发生冲突成为不可能。在适用的情况下,键也应设计为线程安全。(有关更多详细信息,请参阅下文线程关注点部分。)

角色存储和垃圾收集

顺便说一句,上面提到的使用字符串作为附加键的方法并不总是能使附加属性成为主题的属性!如果一个对象没有 __dict____dict__ 不可写(例如类型对象),那么附加属性将存储在另一个地方的弱键字典中。

>>> class NoDict(object):
...     __slots__ = '__weakref__'

>>> dictless = NoDict()

>>> Leech(dictless)
<Leech object at ...>

>>> dictless.__leech__
Traceback (most recent call last):
  ...
AttributeError: 'NoDict' object has no attribute '__leech__'

当然,如果一个对象既没有字典也不是弱引用,那就根本无法为其存储附加属性。

>>> ob = object()
>>> Leech(ob)
Traceback (most recent call last):
  ...
TypeError: cannot create weak reference to 'object' object

然而,在 peak.util.addons 模块中有一个 addons_for() 函数,你可以使用 PEAK-Rules 建议(advice)来扩展它。一旦你添加了一个支持类型的方法,否则这个类型无法与附加属性一起使用,你应该能够使用任何类型的附加对象。当然,前提是你能够实现合适的存储机制!

最后,关于垃圾回收说几句。如果你想避免创建引用循环,不要在附加属性中存储主题的引用。即使 __init____new__ 消息传递了主题,你也没有义务存储主题,通常也不需要这样做。通常,访问附加属性的代码知道正在使用哪个主题,并在需要时将其传递给附加属性的方法。附加属性通常不需要在 __new__()__init__() 调用之后继续保持对主题的引用。

除非有其他引用,否则附加属性实例通常会在其主题被垃圾回收时一起被回收。如果它们保持对主题的引用,它们的垃圾回收可能会被延迟,直到 Python 的循环收集器运行。但是,如果没有保持引用,它们通常会在主题被删除时立即被删除。

>>> def deleting(r):
...     print "deleting", r

>>> from weakref import ref

>>> r = ref(Leech(something), deleting)
>>> del something
deleting <weakref at ...; dead>

然而,存储在主题实例字典之外的附加属性可能需要更长的时间,因为 Python 处理弱引用回调。

也不建议在你的附加对象上实现 __del__ 方法,尤其是如果你保留了主题的引用。在这种情况下,垃圾回收可能变得不可能,附加属性和其主题都会“泄露”(即永久占用内存而不被回收)。

类插件

有时,将附加属性附加到类而不是实例上是有用的。当然,你可以使用普通的 AddOn 类,因为它们与经典类和新式类型(包括内置类型)一起工作得很好。

>>> Persistence.exists_for(int)
False

>>> Persistence(int) is Persistence(int)
True

>>> Persistence.exists_for(int)
True

>>> class X: pass

>>> Persistence.exists_for(X)
False

>>> Persistence(X) is Persistence(X)
True

>>> Persistence.exists_for(X)
True

但是,有时你有专门为向类添加元数据而设计的附加属性,可能通过类或方法装饰器来实现。在这种情况下,你需要一种方法在主题存在之前就访问附加属性!

ClassAddOn 基类提供了这样的机制。它添加了一个额外的类方法 for_enclosing_class(),你可以使用它来访问当前在调用者作用域中定义的类的附加属性。例如,假设我们想要一个方法装饰器,它将方法添加到某个类级别的注册表中。

>>> from peak.util.addons import ClassAddOn

>>> class SpecialMethodRegistry(ClassAddOn):
...     def __init__(self, subject):
...         self.special_methods = {}
...         super(SpecialMethodRegistry, self).__init__(subject)

>>> def specialmethod(func):
...     smr = SpecialMethodRegistry.for_enclosing_class()
...     smr.special_methods[func.__name__] = func
...     return func

>>> class Demo:
...     def dummy(self, foo):
...         pass
...     dummy = specialmethod(dummy)

>>> SpecialMethodRegistry(Demo).special_methods
{'dummy': <function dummy at ...>}

>>> class Demo2(object):
...     def dummy(self, foo):
...         pass
...     dummy = specialmethod(dummy)

>>> SpecialMethodRegistry(Demo2).special_methods
{'dummy': <function dummy at ...>}

你当然可以使用类附加属性的常规附加属性 API。

>>> SpecialMethodRegistry.exists_for(int)
False

>>> SpecialMethodRegistry(int).special_methods['x'] = 123

>>> SpecialMethodRegistry.exists_for(int)
True

但是,你不能明确地删除它们,它们必须自然地被垃圾回收。

>>> SpecialMethodRegistry.delete_from(Demo)
Traceback (most recent call last):
  ...
TypeError: ClassAddOns cannot be deleted

延迟初始化

当一个类附加属性被初始化时,类可能还不存在。在这种情况下,__new____init__ 方法将传递 None 作为第一个参数。如果你的附加属性将在带有 for_enclosing_class() 的类定义中访问,你必须正确处理这种情况。

然而,您可以定义一个当实际类可用时立即被调用的 created_for() 实例方法。如果插件最初是为一个已存在的类创建的,默认的 __init__ 方法也会调用它。无论哪种方式,对于任何给定的插件实例,created_for() 方法只能被调用一次。例如

>>> class SpecialMethodRegistry(ClassAddOn):
...     def __init__(self, subject):
...         print "init called for", subject
...         self.special_methods = {}
...         super(SpecialMethodRegistry, self).__init__(subject)
...
...     def created_for(self, cls):
...         print "created for", cls.__name__

>>> class Demo:
...     def dummy(self, foo):
...         pass
...     dummy = specialmethod(dummy)
init called for None
created for Demo

在上面的例子中,由于类型尚不存在,因此 __init__ 方法使用 None 被调用。然而,访问现有类型的插件(尚未添加插件)将使用类型调用 __init__,并且当默认的 ClassAddOn.__init__ 实现看到主题不是 None 时,也会为我们调用 created_for()

>>> SpecialMethodRegistry(float)
init called for <type 'float'>
created for float
<SpecialMethodRegistry object at ...>

>>> SpecialMethodRegistry(float)    # created_for doesn't get called again
<SpecialMethodRegistry object at ...>

拥有这个 created_for() 方法最有用的特性之一是,它允许您设置涉及从基类继承的设置的类级别元数据。在 created_for() 中,您可以访问类的 __bases__ 和或 __mro__,然后只需为这些基类请求相同的插件实例,然后将它们的数据适当地合并到您自己的实例中。您保证您访问的任何此类插件都已初始化,包括它们已调用 created_for() 方法。

由于这是递归的,并且因为类插件甚至可以附加到内建类型如 object 上,与必须为这些基类特殊处理、检查没有添加或定义元数据的位置等相比,创建正确的类元数据注册表的工作大大简化。

相反,没有定义任何元数据的类将仅有一个包含由您的插件 __init__() 方法设置的任何内容的插件实例,以及其 created_for() 方法添加的任何附加数据。

因此,使用类插件进行元数据积累实际上可能比使用元类更简单,因为元类不能添加到现有类中。当然,类插件不能完全替代元类或基类混入,但对于它们可以做到的事情,它们更容易正确实现。

键、装饰和for_enclosing_class()

类插件可以有插件键,就像常规插件一样,它们以相同的方式实现。您可以将额外参数作为位置参数传递给 for_enclosing_class()。例如

>>> class Index(ClassAddOn):
...     def __init__(self, subject, expr):
...         self.expr = expr
...         self.funcs = []
...         super(Index, self).__init__(subject)

>>> def indexedmethod(expr):
...     def decorate(func):
...         Index.for_enclosing_class(expr).funcs.append(func)
...         return func
...     return decorate

>>> class Demo:
...     def dummy(self, foo):
...         pass
...     dummy = indexedmethod("x*y")(dummy)

>>> Index(Demo, "x*y").funcs
[<function dummy at ...>]

>>> Index(Demo, "y+z").funcs
[]

顺便说一下,您不需要使用函数装饰器来向类添加元数据。您只需要在从类体中直接调用的函数中调用 for_enclosing_class()

>>> def special_methods(**kw):
...     smr = SpecialMethodRegistry.for_enclosing_class()
...     smr.special_methods.update(kw)

>>> class Demo:
...     special_methods(x=23, y=55)
init called for None
created for Demo

>>> SpecialMethodRegistry(Demo).special_methods
{'y': 55, 'x': 23}

默认情况下,for_enclosing_class() 方法假设它是由一个从类体中直接调用的函数调用的,例如方法装饰器,或如上所示的自立函数调用。但是,如果您从一个类语句之外的位置进行调用,您将得到一个错误

>>> special_methods(z=42)
Traceback (most recent call last):
  ...
SyntaxError: Class decorators may only be used inside a class statement

同样,如果您有一个调用 for_enclosing_class() 的函数,然后您从这个函数中调用另一个函数,它仍然会失败

>>> def sm(**kw):
...     special_methods(**kw)

>>> class Demo:
...     sm(x=23, y=55)
Traceback (most recent call last):
  ...
SyntaxError: Class decorators may only be used inside a class statement

这是因为 for_enclosing_class() 假设类是在其帧上方两个栈级别处定义的。但是,您可以通过使用 level 关键字参数来改变这个假设。

>>> def special_methods(level=2, **kw):
...     smr = SpecialMethodRegistry.for_enclosing_class(level=level)
...     smr.special_methods.update(kw)

>>> def sm(**kw):
...     special_methods(level=3, **kw)

>>> class Demo:
...     sm(x=23)
...     special_methods(y=55)
init called for None
created for Demo

>>> SpecialMethodRegistry(Demo).special_methods
{'y': 55, 'x': 23}

或者,您可以通过传递给 for_enclosing_class()frame 关键字参数一个特定的 Python 帧对象,或者使用 for_frame() 类方法。 for_frame() 接收一个 Python 栈帧,然后是创建键所需的任何额外位置参数。

类注册表(0.6版本新增)

对于许多常见的类扩展用例,你可能只需要一个像字典一样具有“继承”功能的基础类值对象。通过子类化ClassAddOn和Python内置的dict类型,Registry基类提供这种行为,创建一个既是类扩展又是字典的类。然后它覆盖了created_for()方法,以便自动填充从基础类继承的任何值。

让我们定义一个用于存储方法“好评度”的MethodGoodness注册表。

>>> from peak.util.addons import Registry

>>> class MethodGoodness(Registry):
...     """Dictionary of method goodness"""

>>> def goodness(value):
...     def decorate(func):
...         MethodGoodness.for_enclosing_class()[func.__name__]=value
...         return func
...     return decorate

>>> class Demo(object):
...     def aMethod(self, foo):
...         pass
...     aMethod = goodness(17)(aMethod)
...     def another_method(whinge, spam):
...         woohoo
...     another_method = goodness(-99)(another_method)

>>> MethodGoodness(Demo)
{'aMethod': 17, 'another_method': -99}

到目前为止,一切顺利。让我们看看子类会发生什么。

>>> class Demo2(Demo):
...     def another_method(self, fixed):
...         pass
...     another_method = goodness(42)(another_method)

>>> MethodGoodness(Demo2)
{'another_method': 42, 'aMethod': 17}

在基础类注册表中设置的值,如果当前类没有定义条目,则自动添加到当前类相同类型和键的注册表中。Python的新式方法解析顺序用于确定继承属性的优先级。(对于经典类,创建一个临时的新式类,从经典类继承,以确定解析顺序,然后丢弃。)

一旦创建了相关类,注册表将获得一个额外的属性,即defined_in_class,它是一个字典,列出了实际在对应类中定义的条目,例如:

>>> MethodGoodness(Demo).defined_in_class
{'aMethod': 17, 'another_method': -99}

>>> MethodGoodness(Demo2).defined_in_class
{'another_method': 42}

如你所见,这个第二个字典只包含在该类中注册的值,而不包含任何继承的值。

最后,请注意,Registry对象有一个额外的方法,可以用于装饰器:set(key, value)。如果为给定的键已存在不同的值,此方法将引发错误,这对于捕获类定义中的错误很有用,例如:

>>> def goodness(value):
...     def decorate(func):
...         MethodGoodness.for_enclosing_class().set(func.__name__, value)
...         return func
...     return decorate
>>> class Demo3(object):
...     def aMethod(self, foo):
...         pass
...     aMethod = goodness(17)(aMethod)
...     def aMethod(self, foo):
...         pass
...     aMethod = goodness(27)(aMethod)
Traceback (most recent call last):
  ...
ValueError: MethodGoodness['aMethod'] already contains 17; can't set to 27

线程关注点

只要扩展键不包含任何具有__hash____equals__方法的对象(与不调用任何Python代码的纯C代码相对),则扩展查找和创建是线程安全的(即没有竞态条件)。在递归的情况下,如果键是元组,则只包含内置类型实例的键的扩展,或者从内置类型继承其__hash____equals__方法类型的扩展,可以以线程安全的方式初始化。

然而,这并不意味着不能同时为同一主题创建两个或多个扩展实例!如果你希望代码是线程安全的,扩展类__new____init__方法中的代码必须不假设它将是附加到其主题的唯一扩展实例。

这是因为扩展访问机制允许多个线程同时创建扩展实例,但只有一个对象会赢得成为“唯一”扩展实例的竞争,没有线程事先知道它是否会赢。因此,如果你希望你的扩展实例在初始化时对构造函数参数执行某些操作,你必须放弃你的扩展是线程安全的,或者使用某种其他锁定机制。

当然,扩展初始化只是整个线程安全难题的一小部分。除非你的扩展仅存在于计算其主题的某些不可变元数据,否则你的扩展的其他方法也必须是线程安全的。

实现这一目标的一种方法,是使用@synchronized装饰器,结合使用Locking扩展。

>>> class Locking(AddOn):
...     def __init__(self, subject):
...         from threading import RLock
...         self.lock = RLock()
...     def acquire(self):
...         print "acquiring"
...         self.lock.acquire()
...     def release(self):
...         self.lock.release()
...         print "released"

>>> def synchronized(func):
...     def wrapper(self, *__args,**__kw):
...         Locking(self).acquire()
...         try:
...             func(self, *__args,**__kw)
...         finally:
...             Locking(self).release()
...
...     from peak.util.decorators import rewrap
...     return rewrap(func, wrapper)

>>> class AnotherThing:
...     def ping(self):
...         print "ping"
...     ping = synchronized(ping)

>>> AnotherThing().ping()
acquiring
ping
released

如果Locking()扩展构造函数不是线程安全的,则此装饰器将无法正确执行其任务,因为两个访问尚未拥有扩展的对象的线程可能会锁定两个不同的锁,并且同时运行所谓的“同步”方法!

(总的来说,线程安全性比看起来更难。但至少你不需要担心正确实现它的这个小小部分。)

当然,同步方法会比普通方法慢,这也是为什么 AddOns 除了解决线程安全问题的那一点外,不做任何其他操作,以避免惩罚非线程代码。正如 PEAK 的座右铭所说,STASCTAP!(简单的事情简单,复杂的事情可能。)

邮件列表

有关本软件的问题、讨论和错误报告应发送至 PEAK 邮件列表;有关详细信息,请参阅 http://www.eby-sarna.com/mailman/listinfo/PEAK/

项目详情


下载文件

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

源分发

AddOns-0.7.zip (34.2 kB 查看哈希值)

上传时间

构建分发

AddOns-0.7-py2.7.egg (15.3 kB 查看哈希值)

上传时间

AddOns-0.7-py2.5.egg (18.8 kB 查看哈希值)

上传时间

AddOns-0.7-py2.4.egg (18.9 kB 查看哈希值)

上传时间

AddOns-0.7-py2.3.egg (57.7 kB 查看哈希值)

上传时间

支持