猴子修补@property

Monkey patching a @property

提问人:deceze 提问时间:7/23/2015 最后编辑:deceze 更新时间:9/25/2019 访问量:12067

问:

是否可以对我不控制的类的实例的 a 值进行猴子修补?@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

python-3.x 猴子补丁

评论

0赞 jonrsharpe 7/23/2015
您的意思是您不想再创建属性(如果是这样,则不影响其他实例?请注意,与其他描述符一样,属性存储在类上f.bar
0赞 deceze 7/23/2015
是的,我想影响仅在这一个实例上访问时返回的值。f.bar
1赞 jonrsharpe 7/23/2015
我认为做到这一点的唯一方法是直接更改,提供一个新属性,该属性将在属性之前被查找,但仅适用于该单个实例。f.__dict__bar
1赞 warvariuc 7/23/2015
我的解决方案是复制类,更改描述符,然后替换实例的类:stackoverflow.com/a/30578922/248296
4赞 Markus Meskanen 7/23/2015
@deceze 您可以使用以下命令更改类Foo().__class__ = SubFoo

答:

2赞 user319799 7/23/2015 #1

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

问题是如何存储显式设置的值。我们不能使用按修补对象键控的对象,因为 1) 它们不一定在标识上具有可比性;2) 这样可以防止修补的对象被垃圾回收。对于 1) 我们可以编写一个包装对象并按标识覆盖比较语义的 2) 我们可以使用 .但是,我无法让这两者一起工作。dictHandleweakref.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

评论

0赞 deceze 7/23/2015
有趣的方法。从本质上讲,类需要修补,只是实例不能?
35赞 Markus Meskanen 7/23/2015 #2

子类化基类 () 并使用属性更改单个实例的类以匹配新的子类: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赞 deceze 7/23/2015
听起来像是赢家!
1赞 f43d65 7/23/2015
如果替换为 in 声明中以保持以前的值 ,会更好吗?bar = 0bar = f.bar_SubFoof.bar
1赞 Markus Meskanen 7/24/2015
@f43d65 如果子类仅用于一个实例,则确定。您甚至可以直接将栏设置为如果这是您想要的值。但是,如果多个实例需要使用相同的类(以便覆盖相同的属性),则最好为其提供一个简单的默认值,例如 。420
1赞 Dunes 7/23/2015 #3

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

非数据描述符只是具有方法的类的实例。与数据描述符的唯一区别是它也有一个方法。属性最初有一个引发错误的方法,除非您提供 setter 函数。__get____set____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__

评论

0赞 deceze 7/23/2015
这很好,但我真的想在这里躲避第 3 方库。;)
0赞 Dunes 7/23/2015
然后,您可以对类属性进行猴子修补。例如。.所有其他属性访问权限保持不变,但您现在可以将 .Foo.bar = nondatadescriptor(Foo.bar.fget)foo.bar
0赞 Dunes 7/24/2015
当然,但我可能会做得稍微不同。他付出了很多努力来寻找放置新价值的地方。但由于 的语义,是完美的藏身之处。除非课程对实例的 .__getattribute__foo.__dict__["bar"]Foo__dict__
25赞 kzh 3/22/2016 #4
from module import ClassToPatch

def get_foo(self):
    return 'foo'

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

评论

0赞 Andrew 11/7/2016
完善!我用它来修补框架的基类,以支持急需的功能。到目前为止,这是最简单的方法,尽管我确实认为这里的描述可能会更好一些:)如果你知道你.py那么仅仅一点代码就足够有意义了。
0赞 gebbissimo 11/25/2021
@kzh:我们也可以重复使用原来的财产吗?我尝试了,但没有成功fooold_foo = ClassToPatch.fooreturn old_foo
0赞 Konstantin 3/31/2022
这对属性设置者不起作用。
0赞 kzh 4/4/2022
@Konstantin你尝试过什么来启用二传手?
14赞 fralau 9/18/2017 #5

要对属性进行猴子修补,有一种更简单的方法:

from module import ClassToPatch

def get_foo(self):
    return 'foo'

ClassToPatch.foo = property(get_foo)

评论

2赞 MarredCheese 1/3/2018
问题是如何覆盖现有属性。这个答案与此无关。
0赞 fralau 1/4/2018
我不确定我是否理解您的评论?在我的理解中,它确实会覆盖现有属性(问题不在于子类化,而在于猴子修补)。
0赞 MarredCheese 1/4/2018
对不起,我的评论含糊不清,因为我误解了你的回答。我以为你正在做,如果实际上是该类的现有属性,它会失败,但你实际上写了 ,它工作得很好。所以我错了。然而,当我们在做这件事时,还有另外两个问题:1)提问者希望修补一个实例,而不是一个类,2)他们希望用一个静态值而不是另一个属性来修补它。无论如何,我认为你的回答仍然增加了一些价值。instance.foo = ...fooclass.foo = ...
0赞 Ranel Padon 8/3/2021
直观的实现,就像一个魅力!谢谢
1赞 Sebastian Wagner 7/25/2019 #6

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

from module import ClassToPatch

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

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

参考

0赞 warvariuc 9/25/2019 #7

如果有人需要修补属性,同时能够调用原始实现,下面是一个示例:

@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