私が制御していないクラスのインスタンスの@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
の実装の詳細はブラックボックスであり、間接的にモンキーパッチ可能ではありません。メソッド呼び出し全体を置き換える必要があります。単一のインスタンスのみに影響を与える必要があります(不可避の場合、クラスレベルのパッチ適用は問題ありませんが、変更された動作は、そのクラスのすべてのインスタンスではなく、特定のインスタンスにのみ選択的に影響を与える必要があります)。
基本クラス(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
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)
アイデア:特定のオブジェクトに設定できるようにプロパティ記述子を置き換えます。この方法で値が明示的に設定されていない限り、元のプロパティゲッターが呼び出されます。
問題は、明示的に設定された値を保存する方法です。パッチされたオブジェクトによってキーが付けられた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
プロパティからデータ記述子と非データ記述子の領域に移る必要があるようです。プロパティは、データ記述子の特別なバージョンにすぎません。関数は非データ記述子の例です。インスタンスから取得すると、関数自体ではなくメソッドが返されます。
非データ記述子は、__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__
です。現時点で適切なドキュメントが見つかりませんが、検索の順序は次のとおりです。
__get__
と__set__
の両方を持つオブジェクト)、それらの__get__
メソッドが呼び出されます。__get__
メソッドのみを持つオブジェクト)。__getattr__
メソッドが最後の手段として呼び出されます(定義されている場合)。プロパティセッターにパッチを適用することもできます。 @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