猴子修补@property

时间:2015-07-23 14:21:02

标签: python python-3.x monkeypatching

是否可以修补我无法控制的类实例@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的实现细节是一个黑盒子,而不是间接的猴子可修补。需要替换整个方法调用。它只需要影响单个实例(如果不可避免,类级别修补是可以的,但改变的行为必须只选择性地影响给定的实例,而不是该类的所有实例)。

8 个答案:

答案 0 :(得分:19)

对基类(Foo)进行子类化并使用__class__属性更改单个实例的类以匹配新的子类:

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

答案 1 :(得分:5)

from module import ClassToPatch

def get_foo(self):
    return 'foo'

setattr(ClassToPatch, 'foo', property(get_foo))

答案 2 :(得分:2)

想法:替换属性描述符以允许对某些对象进行设置。除非以这种方式显式设置值,否则将调用原始属性getter。

问题是如何存储显式设置的值。我们不能使用由修补对象键入的dict,因为1)它们不一定具有可比性; 2)这可以防止修补的对象被垃圾收集。对于1)我们可以编写一个Handle来包装对象并通过标识覆盖比较语义,以及2)我们可以使用weakref.WeakKeyDictionary。但是,我无法让这两个人一起工作。

因此,我们使用一种不同的方法将显式设置值存储在对象本身上,使用"非常不可能的属性名称"。当然,这个名称仍然可能与某些内容发生冲突,但这对Python等语言来说几乎是固有的。

这不适用于缺少__dict__插槽的对象。类似的问题会出现在弱点上。

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

答案 3 :(得分:2)

要修补属性,有一种更简单的方法:

from module import ClassToPatch

def get_foo(self):
    return 'foo'

ClassToPatch.foo = property(get_foo)

答案 4 :(得分:1)

看起来您需要从属性转移到数据描述符和非数据描述符的领域。属性只是数据描述符的专用版本。函数是非数据描述符的一个示例 - 当您从实例中检索它们时,它们返回一个方法而不是函数本身。

非数据描述符只是具有__get__方法的类的实例。与数据描述符的唯一区别在于它也具有__set__方法。除非提供setter函数,否则属性最初会有__set__方法抛出错误。

只需编写自己的简单非数据描述符,即可轻松实现您想要的功能。

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__下的逻辑。我目前找不到合适的文档,但检索顺序是:

  1. 在类上定义的数据描述符具有最高优先级(同时具有__get____set__的对象),并且调用它们的__get__方法。
  2. 对象本身的任何属性。
  3. 在类上定义的非数据描述符(仅使用__get__方法的对象)。
  4. 在班级上定义的所有其他属性。
  5. 最后,调用对象的__getattr__方法作为最后的手段(如果已定义)。

答案 5 :(得分:1)

您还可以修补属性设置器。使用@fralau的答案:

from module import ClassToPatch

def foo(self, new_foo):
    self._foo = new_foo

ClassToPatch.foo = ClassToPatch.foo.setter(foo)

reference

答案 6 :(得分:0)

如果有人需要在能够调用原始实现的同时修补属性,请参见以下示例:

@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

答案 7 :(得分:-2)

我刚刚发现这个Q& A因为PyCharm在抱怨" Property' bar'不能设置",但这实际上对我有用:

>>> class Foo:
...     @property
...     def bar(self):
...             return 'Foo.bar'
... 
>>> f = Foo()
>>> f.bar
'Foo.bar'
>>> @property
... def foo_bar(self):
...     return 42
... 
>>> Foo.bar = foo_bar
>>> f.bar
42
>>> 

我知道这可能不符合单一实例条件,但如果我的答案已经在这里,那将节省我一些时间;)