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


person deceze♦    schedule 23.07.2015    source источник
comment
Вы имеете в виду, что хотите сделать f.bar не свойством (и если да, не затрагивая другие экземпляры?). Обратите внимание, что свойства, как и другие дескрипторы, хранятся в классе.   -  person jonrsharpe    schedule 23.07.2015
comment
Да, я хочу повлиять на значение, которое возвращается при доступе к f.bar только в этом одном экземпляре.   -  person deceze♦    schedule 23.07.2015
comment
Я думаю, что единственный способ сделать это - напрямую изменить f.__dict__, предоставив новый атрибут bar, который будет просматриваться до того, как свойство будет найдено, но только для этого единственного экземпляра.   -  person jonrsharpe    schedule 23.07.2015
comment
Я пробовал это, но свойство всегда, кажется, ищут в первую очередь. Хороший рабочий пример ...   -  person deceze♦    schedule 23.07.2015
comment
У меня был открыт не тот переводчик; это работает только с классами старого стиля. Считайте меня смущенным.   -  person jonrsharpe    schedule 23.07.2015
comment
Foo.bar = 42 ... или вы просто хотите изменить свойство экземпляра?   -  person LittleQ    schedule 23.07.2015
comment
@LittleQ Как обсуждалось выше, всего один экземпляр.   -  person deceze♦    schedule 23.07.2015
comment
Мое решение заключалось в том, чтобы сделать копию класса, изменить дескриптор и затем заменить класс экземпляра: stackoverflow.com/a/30578922/248296 < / а>   -  person warvariuc    schedule 23.07.2015
comment
почему бы не использовать подкласс Foo? @deceze   -  person LittleQ    schedule 23.07.2015
comment
@LittleQ Потому что я откуда-то получаю экземпляр Foo и не контролирую его создание. Я также должен работать с этим экземпляром объекта, поскольку он отражается на других, у которых есть ссылка на него.   -  person deceze♦    schedule 23.07.2015
comment
@deceze Вы можете изменить класс, используя Foo().__class__ = SubFoo   -  person Markus Meskanen    schedule 23.07.2015
comment
@Markus Не могли бы вы развить это до ответа? Похоже, это лучший подход.   -  person deceze♦    schedule 23.07.2015


Ответы (7)


Создайте подкласс базового класса (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
person Markus Meskanen    schedule 23.07.2015
comment
Похоже на победителя! - person deceze♦; 23.07.2015
comment
Будет ли лучше заменить bar = 0 на bar = f.bar в объявлении _SubFoo, чтобы сохранить предыдущее значение f.bar? - person f43d65; 23.07.2015
comment
@ f43d65 Если подкласс используется только для одного экземпляра, тогда обязательно. Вы даже можете установить полосу прямо на 42, если вам все равно нужно это значение. Но если несколько экземпляров должны использовать один и тот же класс (чтобы переопределить один и тот же атрибут), вам лучше дать ему простое значение по умолчанию, например 0. - person Markus Meskanen; 24.07.2015

Чтобы обезьяна исправить свойство, есть еще более простой способ:

from module import ClassToPatch

def get_foo(self):
    return 'foo'

ClassToPatch.foo = property(get_foo)
person fralau    schedule 18.09.2017
comment
Вопрос был в том, как перезаписать существующее свойство. Этот ответ не имеет к этому никакого отношения. - person MarredCheese; 03.01.2018
comment
Я не уверен, что понимаю ваш комментарий? Насколько я понимаю, это действительно будет перекрывать существующее свойство (вопрос был не в подклассе, а в исправлении обезьяны). - person fralau; 03.01.2018
comment
Извините, мой комментарий был расплывчатым и основан на том, что я неверно истолковал ваш ответ. Я думал, что вы выполняете instance.foo = ..., что не удалось бы, если foo было бы фактически существующим свойством этого класса, но вы на самом деле написали class.foo = ..., что отлично работает. Значит, я ошибался. Однако, пока мы занимаемся этим, есть две другие проблемы: 1) спрашивающий хотел исправить экземпляр, а не класс, и 2) он хотел исправить его статическим значением, а не другим свойством. Тем не менее, я думаю, что ваш ответ по-прежнему имеет определенную ценность. - person MarredCheese; 03.01.2018

Идея: заменить дескриптор свойства, чтобы разрешить установку для определенных объектов. Если значение не задано явно таким образом, вызывается средство получения исходного свойства.

Проблема в том, как сохранить явно заданные значения. Мы не можем использовать dict ключи с исправленными объектами, так как 1) они не обязательно сопоставимы по идентичности; 2) это предотвращает сборку мусора для исправленных объектов. Для 1) мы могли бы написать Handle, который обертывает объекты и переопределяет семантику сравнения по идентификатору, а для 2) мы могли бы использовать weakref.WeakKeyDictionary. Однако я не мог заставить этих двоих работать вместе.

Поэтому мы используем другой подход к хранению явно установленных значений в самом объекте, используя «очень маловероятное имя атрибута». Конечно, все еще возможно, что это имя будет с чем-то конфликтовать, но это в значительной степени присуще таким языкам, как Python.

Это не сработает с объектами, у которых нет слота __dict__. Однако аналогичная проблема может возникнуть и для weakrefs.

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
person doublep    schedule 23.07.2015
comment
Интересный подход. По сути, класс нужно пропатчить, только экземпляр не может? - person deceze♦; 23.07.2015

Похоже, вам нужно перейти от свойств к области дескрипторов данных и дескрипторов, не относящихся к данным. Свойства - это просто специализированная версия дескрипторов данных. Функции являются примером дескрипторов, не относящихся к данным - когда вы извлекаете их из экземпляра, они возвращают метод, а не саму функцию.

Дескриптор, не связанный с данными, - это просто экземпляр класса, у которого есть метод __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__ объекта (если он определен).
person Dunes    schedule 23.07.2015
comment
Это хорошо и все такое, но я действительно пытаюсь уклониться от сторонней библиотеки. ;) - person deceze♦; 23.07.2015
comment
Тогда вы можете просто исправить свойство класса. например. Foo.bar = nondatadescriptor(Foo.bar.fget). Доступ к остальным свойствам останется прежним, но теперь вы можете установить foo.bar. - person Dunes; 23.07.2015
comment
Конечно, но я бы, наверное, сделал это немного по-другому. Он прилагает много усилий, чтобы найти место, где можно разместить новую ценность. Но из-за семантики __getattribute__ foo.__dict__["bar"] - идеальное укрытие. Если, конечно, Foo не делает какие-нибудь фанковые вещи с __dict__ экземпляра. - person Dunes; 23.07.2015

Вы также можете исправить установщики свойств. Используя ответ @fralau:

from module import ClassToPatch

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

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

ссылка

person Sebastian Wagner    schedule 25.07.2019

Если кому-то нужно исправить свойство, имея возможность вызвать исходную реализацию, вот пример:

@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
person warvariuc    schedule 25.09.2019

person    schedule
comment
Вы, кажется, являетесь участником с высокой репутацией, извините за то, что нарушил это вам. Я почти уверен, что вы понимаете, что было бы хорошо и полезно для всех, если бы вы могли просто подробнее рассказать о том, что / как / почему написанного вами алгоритма. - person RLEE; 22.03.2016
comment
Идеально! Я использовал это, чтобы исправить базовый класс фреймворка для поддержки столь необходимой функции. Безусловно, это самый простой способ сделать это, хотя я думаю, что приведенное здесь описание могло бы быть немного лучше :) Если вы знаете свой .py, тогда даже небольшой фрагмент кода имеет достаточно смысла. - person Andrew Backer; 07.11.2016