Подтвердить что ты не робот

Обезьяна, исправляющая @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 являются черным ящиком, а не косвенным. Весь вызов метода необходимо заменить. Он должен влиять только на один экземпляр (исправление на уровне класса - это нормально, если это неизбежно, но измененное поведение должно только выборочно воздействовать на данный экземпляр, а не на все экземпляры этого класса).

4b9b3361

Ответ 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
>>> 

Я знаю, что это может не соответствовать условию одного экземпляра, но если бы мой ответ уже был здесь, это спасло бы меня некоторое время;)