web-dev-qa-db-ja.com

サルが@propertyにパッチを適用

私が制御していないクラスのインスタンスの@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の実装の詳細はブラックボックスであり、間接的にモンキーパッチ可能ではありません。メソッド呼び出し全体を置き換える必要があります。単一のインスタンスのみに影響を与える必要があります(不可避の場合、クラスレベルのパッチ適用は問題ありませんが、変更された動作は、そのクラスのすべてのインスタンスではなく、特定のインスタンスにのみ選択的に影響を与える必要があります)。

38
deceze

基本クラス(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
20
Markus Meskanen
from module import ClassToPatch

def get_foo(self):
    return 'foo'

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

プロパティにモンキーパッチを適用するには、さらに簡単な方法があります。

from module import ClassToPatch

def get_foo(self):
    return 'foo'

ClassToPatch.foo = property(get_foo)
5
fralau

アイデア:特定のオブジェクトに設定できるようにプロパティ記述子を置き換えます。この方法で値が明示的に設定されていない限り、元のプロパティゲッターが呼び出されます。

問題は、明示的に設定された値を保存する方法です。パッチされたオブジェクトによってキーが付けられたdictを使用することはできません。 2)これにより、パッチが適用されたオブジェクトがガベージコレクションされなくなります。 1)については、オブジェクトをラップして比較セマンティクスをIDでオーバーライドするHandleを記述でき、2)についてはweakref.WeakKeyDictionaryを使用できます。しかし、この2つを一緒に動作させることはできませんでした。

したがって、「非常にありそうもない属性名」を使用して、オブジェクト自体に明示的に設定された値を格納する別のアプローチを使用します。もちろん、この名前が何かと衝突する可能性はまだありますが、それはPythonなどの言語にかなり固有のものです。

これは、__dict__スロットのないオブジェクトでは機能しません。ただし、weakrefでも同様の問題が発生します。

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

プロパティからデータ記述子と非データ記述子の領域に移る必要があるようです。プロパティは、データ記述子の特別なバージョンにすぎません。関数は非データ記述子の例です。インスタンスから取得すると、関数自体ではなくメソッドが返されます。

非データ記述子は、__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__メソッドが最後の手段として呼び出されます(定義されている場合)。
1
Dunes

プロパティセッターにパッチを適用することもできます。 @fralauの回答を使用:

from module import ClassToPatch

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

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

参照

1

元の実装を呼び出すことができる間に誰かがプロパティにパッチを適用する必要がある場合の例を次に示します。

@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
0
warvariuc