Обезьяна, исправляющая @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
являются черным ящиком, а не косвенным. Весь вызов метода необходимо заменить. Он должен влиять только на один экземпляр (исправление на уровне класса - это нормально, если это неизбежно, но измененное поведение должно только выборочно воздействовать на данный экземпляр, а не на все экземпляры этого класса).
Ответы
Ответ 1
Подкласс базового класса (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
Ответ 2
from module import ClassToPatch
def get_foo(self):
return 'foo'
setattr(ClassToPatch, 'foo', property(get_foo))
Ответ 3
Чтобы обезьяна заплатила свойство, есть еще более простой способ:
from module import ClassToPatch
def get_foo(self):
return 'foo'
ClassToPatch.foo = property(get_foo)
Ответ 4
Идея: замените дескриптор свойства, чтобы разрешить установку на определенные объекты. Если значение явно не задано таким образом, вызывается исходное свойство 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
Ответ 5
Похоже, вам нужно перейти от свойств к областям дескрипторов данных и дескрипторам без данных. Свойства - это просто специализированная версия дескрипторов данных. Функции представляют собой пример дескрипторов без данных - когда вы извлекаете их из экземпляра, они возвращают метод, а не функцию.
Дескриптор без данных - это всего лишь экземпляр класса с методом __get__
. Единственное отличие от дескриптора данных состоит в том, что он имеет метод __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__
. На данный момент я не могу найти соответствующую документацию, но порядок поиска:
- Дескрипторы данных, определенные в классе, имеют наивысший приоритет (объекты с
__get__
и __set__
), и их метод __get__
вызывается.
- Любой атрибут самого объекта.
- Дескрипторы без данных, определенные в классе (объекты только с методом
__get__
).
- Все остальные атрибуты, определенные в классе.
- Наконец, метод
__getattr__
объекта вызывается как последнее средство (если определено).
Ответ 6
Вы также можете исправлять установщики свойств. Используя ответ @fralau:
from module import ClassToPatch
def foo(self, new_foo):
self._foo = new_foo
ClassToPatch.foo = ClassToPatch.foo.setter(foo)
ссылка
Ответ 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
Ответ 8
Я только что нашел этот Q & A, потому что PyCharm жаловался, что панель "Свойство" не может быть установлена ", но это действительно сработало для меня:
>>> 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
>>>
Я знаю, что это может не соответствовать условию одного экземпляра, но если бы мой ответ уже был здесь, это спасло бы меня некоторое время;)