猴子修补@属性
Monkey patching a @property
是否有可能对我无法控制的 class 实例的 @property
的值进行猴子修补?
class Foo:
@property
def bar(self):
return here().be['dragons']
f = Foo()
print(f.bar) # baz
f.bar = 42 # MAGIC!
print(f.bar) # 42
显然上面的方法在尝试分配给 f.bar
时会产生错误。 # MAGIC!
有任何可能吗? @property
的实现细节是一个黑盒子,不能间接用猴子修补。需要替换整个方法调用。它只需要影响单个实例(如果不可避免,class 级修补是可以的,但更改的行为必须只选择性地影响给定实例,而不是 class 的所有实例)。
想法:替换 属性 描述符以允许对某些对象进行设置。除非以这种方式明确设置值,否则将调用原始 属性 getter。
问题是如何存储显式设置的值。我们不能使用由修补对象作为键的 dict
,因为 1) 它们不一定可以通过身份进行比较; 2) 这可以防止修补对象被垃圾收集。对于 1) 我们可以编写一个 Handle
来包装对象并通过身份覆盖比较语义,对于 2) 我们可以使用 weakref.WeakKeyDictionary
。但是,我无法让这两个一起工作。
因此,我们使用一种不同的方法,使用 "very unlikely attribute name" 将显式设置的值存储在对象本身上。这个名字当然仍然有可能与某些东西发生冲突,但这几乎是 Python.
等语言所固有的。
这不适用于缺少 __dict__
插槽的对象。不过,weakrefs 也会出现类似的问题。
class Foo:
@property
def bar (self):
return 'original'
class Handle:
def __init__(self, obj):
self._obj = obj
def __eq__(self, other):
return self._obj is other._obj
def __hash__(self):
return id (self._obj)
_monkey_patch_index = 0
_not_set = object ()
def monkey_patch (prop):
global _monkey_patch_index, _not_set
special_attr = '$_prop_monkey_patch_{}'.format (_monkey_patch_index)
_monkey_patch_index += 1
def getter (self):
value = getattr (self, special_attr, _not_set)
return prop.fget (self) if value is _not_set else value
def setter (self, value):
setattr (self, special_attr, value)
return property (getter, setter)
Foo.bar = monkey_patch (Foo.bar)
f = Foo()
print (Foo.bar.fset)
print(f.bar) # baz
f.bar = 42 # MAGIC!
print(f.bar) # 42
Subclass base class (Foo
) 并使用 __class__
更改单个实例的 class 以匹配新的 subclass属性:
>>> class Foo:
... @property
... def bar(self):
... return 'Foo.bar'
...
>>> f = Foo()
>>> f.bar
'Foo.bar'
>>> class _SubFoo(Foo):
... bar = 0
...
>>> f.__class__ = _SubFoo
>>> f.bar
0
>>> f.bar = 42
>>> f.bar
42
看来您需要从属性转到数据描述符和非数据描述符领域。属性只是数据描述符的一个特殊版本。函数是非数据描述符的一个示例——当您从实例中检索它们时,它们 return 是一个方法而不是函数本身。
非数据描述符只是具有 __get__
方法的 class 的一个实例。与数据描述符的唯一区别是它也有一个 __set__
方法。属性最初有一个 __set__
方法,除非您提供 setter 函数,否则会抛出错误。
您只需编写自己的普通非数据描述符,就可以真正轻松地实现您想要的。
class nondatadescriptor:
"""generic nondata descriptor decorator to replace @property with"""
def __init__(self, func):
self.func = func
def __get__(self, obj, objclass):
if obj is not None:
# instance based access
return self.func(obj)
else:
# class based access
return self
class Foo:
@nondatadescriptor
def bar(self):
return "baz"
foo = Foo()
another_foo = Foo()
assert foo.bar == "baz"
foo.bar = 42
assert foo.bar == 42
assert another_foo.bar == "baz"
del foo.bar
assert foo.bar == "baz"
print(Foo.bar)
让这一切顺利进行的是引擎盖下的逻辑 __getattribute__
。目前找不到合适的文档,但检索顺序是:
- 在 class 上定义的数据描述符被赋予最高优先级(具有
__get__
和 __set__
的对象),并调用它们的 __get__
方法。
- 对象本身的任何属性。
- 在 class 上定义的非数据描述符(只有
__get__
方法的对象)。
- 在 class 上定义的所有其他属性。
- 最后调用对象的
__getattr__
方法作为最后的手段(如果已定义)。
from module import ClassToPatch
def get_foo(self):
return 'foo'
setattr(ClassToPatch, 'foo', property(get_foo))
猴子补丁属性,有一个更简单的方法:
from module import ClassToPatch
def get_foo(self):
return 'foo'
ClassToPatch.foo = property(get_foo)
您还可以修补 属性 设置器。使用@fralau 的回答:
from module import ClassToPatch
def foo(self, new_foo):
self._foo = new_foo
ClassToPatch.foo = ClassToPatch.foo.setter(foo)
万一有人需要修补 属性 同时又能调用原始实现,这里有一个例子:
@property
def _cursor_args(self, __orig=mongoengine.queryset.base.BaseQuerySet._cursor_args):
# TODO: remove this hack when we upgrade MongoEngine
# https://github.com/MongoEngine/mongoengine/pull/2160
cursor_args = __orig.__get__(self)
if self._timeout:
cursor_args.pop("no_cursor_timeout", None)
return cursor_args
mongoengine.queryset.base.BaseQuerySet._cursor_args = _cursor_args
是否有可能对我无法控制的 class 实例的 @property
的值进行猴子修补?
class Foo:
@property
def bar(self):
return here().be['dragons']
f = Foo()
print(f.bar) # baz
f.bar = 42 # MAGIC!
print(f.bar) # 42
显然上面的方法在尝试分配给 f.bar
时会产生错误。 # MAGIC!
有任何可能吗? @property
的实现细节是一个黑盒子,不能间接用猴子修补。需要替换整个方法调用。它只需要影响单个实例(如果不可避免,class 级修补是可以的,但更改的行为必须只选择性地影响给定实例,而不是 class 的所有实例)。
想法:替换 属性 描述符以允许对某些对象进行设置。除非以这种方式明确设置值,否则将调用原始 属性 getter。
问题是如何存储显式设置的值。我们不能使用由修补对象作为键的 dict
,因为 1) 它们不一定可以通过身份进行比较; 2) 这可以防止修补对象被垃圾收集。对于 1) 我们可以编写一个 Handle
来包装对象并通过身份覆盖比较语义,对于 2) 我们可以使用 weakref.WeakKeyDictionary
。但是,我无法让这两个一起工作。
因此,我们使用一种不同的方法,使用 "very unlikely attribute name" 将显式设置的值存储在对象本身上。这个名字当然仍然有可能与某些东西发生冲突,但这几乎是 Python.
等语言所固有的。这不适用于缺少 __dict__
插槽的对象。不过,weakrefs 也会出现类似的问题。
class Foo:
@property
def bar (self):
return 'original'
class Handle:
def __init__(self, obj):
self._obj = obj
def __eq__(self, other):
return self._obj is other._obj
def __hash__(self):
return id (self._obj)
_monkey_patch_index = 0
_not_set = object ()
def monkey_patch (prop):
global _monkey_patch_index, _not_set
special_attr = '$_prop_monkey_patch_{}'.format (_monkey_patch_index)
_monkey_patch_index += 1
def getter (self):
value = getattr (self, special_attr, _not_set)
return prop.fget (self) if value is _not_set else value
def setter (self, value):
setattr (self, special_attr, value)
return property (getter, setter)
Foo.bar = monkey_patch (Foo.bar)
f = Foo()
print (Foo.bar.fset)
print(f.bar) # baz
f.bar = 42 # MAGIC!
print(f.bar) # 42
Subclass base class (Foo
) 并使用 __class__
更改单个实例的 class 以匹配新的 subclass属性:
>>> class Foo:
... @property
... def bar(self):
... return 'Foo.bar'
...
>>> f = Foo()
>>> f.bar
'Foo.bar'
>>> class _SubFoo(Foo):
... bar = 0
...
>>> f.__class__ = _SubFoo
>>> f.bar
0
>>> f.bar = 42
>>> f.bar
42
看来您需要从属性转到数据描述符和非数据描述符领域。属性只是数据描述符的一个特殊版本。函数是非数据描述符的一个示例——当您从实例中检索它们时,它们 return 是一个方法而不是函数本身。
非数据描述符只是具有 __get__
方法的 class 的一个实例。与数据描述符的唯一区别是它也有一个 __set__
方法。属性最初有一个 __set__
方法,除非您提供 setter 函数,否则会抛出错误。
您只需编写自己的普通非数据描述符,就可以真正轻松地实现您想要的。
class nondatadescriptor:
"""generic nondata descriptor decorator to replace @property with"""
def __init__(self, func):
self.func = func
def __get__(self, obj, objclass):
if obj is not None:
# instance based access
return self.func(obj)
else:
# class based access
return self
class Foo:
@nondatadescriptor
def bar(self):
return "baz"
foo = Foo()
another_foo = Foo()
assert foo.bar == "baz"
foo.bar = 42
assert foo.bar == 42
assert another_foo.bar == "baz"
del foo.bar
assert foo.bar == "baz"
print(Foo.bar)
让这一切顺利进行的是引擎盖下的逻辑 __getattribute__
。目前找不到合适的文档,但检索顺序是:
- 在 class 上定义的数据描述符被赋予最高优先级(具有
__get__
和__set__
的对象),并调用它们的__get__
方法。 - 对象本身的任何属性。
- 在 class 上定义的非数据描述符(只有
__get__
方法的对象)。 - 在 class 上定义的所有其他属性。
- 最后调用对象的
__getattr__
方法作为最后的手段(如果已定义)。
from module import ClassToPatch
def get_foo(self):
return 'foo'
setattr(ClassToPatch, 'foo', property(get_foo))
猴子补丁属性,有一个更简单的方法:
from module import ClassToPatch
def get_foo(self):
return 'foo'
ClassToPatch.foo = property(get_foo)
您还可以修补 属性 设置器。使用@fralau 的回答:
from module import ClassToPatch
def foo(self, new_foo):
self._foo = new_foo
ClassToPatch.foo = ClassToPatch.foo.setter(foo)
万一有人需要修补 属性 同时又能调用原始实现,这里有一个例子:
@property
def _cursor_args(self, __orig=mongoengine.queryset.base.BaseQuerySet._cursor_args):
# TODO: remove this hack when we upgrade MongoEngine
# https://github.com/MongoEngine/mongoengine/pull/2160
cursor_args = __orig.__get__(self)
if self._timeout:
cursor_args.pop("no_cursor_timeout", None)
return cursor_args
mongoengine.queryset.base.BaseQuerySet._cursor_args = _cursor_args