Слабая ссылка на метод класса Python

Python 2.7 docs для модуля weakref говорит следующее:

Не все объекты могут быть слабо указаны; те объекты, которые могут включают экземпляры классов, функции, написанные на Python (но не в C), методы (как связанные, так и несвязанные),...

И Python 3.3 docs для модуля weakref говорят это:

Не все объекты могут быть слабо указаны; те объекты, которые могут включают экземпляры классов, функции, написанные на Python (но не в C), методы экземпляра,...

Для меня это указывает на то, что слабые ссылки на связанные методы (во всех версиях Python 2.7 - 3.3) должны быть хорошими, а слабые ссылки на несвязанные методы должны быть хорошими в Python 2.7.

Тем не менее, в Python 2.7, создание слабой ссылки на метод (связанный или несвязанный) приводит к мертвой слабойреф:

>>> def isDead(wr): print 'dead!'
...
>>> class Foo: 
...    def bar(self): pass
...
>>> wr=weakref.ref(Foo.bar, isDead)
dead!
>>> wr() is None
True
>>> foo=Foo()
>>> wr=weakref.ref(foo.bar, isDead)
dead!
>>> wr() is None
True

Не то, что я ожидал бы на основе документов.

Аналогично, в Python 3.3, слабыйref для связанного метода умирает при создании:

>>> wr=weakref.ref(Foo.bar, isDead)
>>> wr() is None
False
>>> foo=Foo()
>>> wr=weakref.ref(foo.bar, isDead)
dead!
>>> wr() is None
True

Снова не то, что я ожидал бы на основе документов.

Поскольку эта формулировка существует с момента появления 2.7, она, безусловно, не является надзором. Может ли кто-нибудь объяснить, как заявления и наблюдаемое поведение на самом деле не противоречат?

Редактирование/разъяснение: Другими словами, утверждение для 3.3 говорит, что "методы экземпляра могут быть слабыми"; не означает ли это, что разумно ожидать, что weakref.ref(метод экземпляра)() не является None? и если это None, то "методы экземпляра" не должны быть перечислены среди типов объектов, которые могут быть слабыми ссылками?

Ответ 1

Foo.bar создает новый объект несвязанных методов каждый раз, когда вы обращаетесь к нему, из-за некоторых подробностей о дескрипторах и способах реализации методов на Python.

Класс не имеет несвязанных методов; он владеет функциями. (Проверьте Foo.__dict__['bar'].) У этих функций просто есть __get__, который возвращает объект unbound-method. Поскольку ничто иное не содержит ссылки, оно исчезает, как только вы закончите создание слабой стороны. (В Python 3 довольно ненужный дополнительный слой уходит, а "несвязанный метод" - это только основная функция.)

Связанные методы работают примерно так же: функция __get__ возвращает объект связанного метода, который на самом деле просто partial(function, self). Каждый раз вы получаете новый, так что вы видите одно и то же явление.

Вы можете сохранить объект метода и сохранить ссылку на него, конечно:

>>> def is_dead(wr): print "blech"
... 
>>> class Foo(object):
...     def bar(self): pass
... 
>>> method = Foo.bar
>>> wr = weakref.ref(method, is_dead)
>>> 1 + 1
2
>>> method = None
blech

Все это кажется сомнительным, хотя:)

Обратите внимание, что если Python не выдавал экземпляр нового метода для каждого доступа к атрибуту, это означало бы, что классы относятся к их методам и методам, относятся к их классам. Наличие таких циклов для каждого отдельного метода в каждом отдельном экземпляре во всей программе сделало бы сбор мусора более дорогим - и до 2.1, у Python даже не было коллекции циклов, поэтому они бы застряли навсегда.

Ответ 2

@Правильный ответ правильный, но важна тонкость.

В документах Python указано, что методы экземпляра (py3k) и методы un/bound (py2.4 +) могут быть слабыми. Вы ожидали бы (наивно, как и я), чтобы weakref.ref(foo.bar)() был не-None, но это None, что делает слабый ref "мертвым по прибытии" (DOA). Это приводит к моему вопросу, если метод weakref для экземпляра DOA, почему документы говорят, что вы можете отказаться от метода?

Итак, как показала @Eevee, вы можете создать не-мертвую слабую ссылку на метод экземпляра, создав сильную ссылку на объект метода, который вы даете weakref:

m = foo.bar # creates a *new* instance method "Foo.bar" and strong refs it
wr = weakref.ref(m)
assert wr() is not None # success

Тонкость (для меня, во всяком случае) заключается в том, что новый объект метода экземпляра создается каждый раз, когда вы используете Foo.bar, поэтому даже после выполнения вышеописанного кода произойдет следующее:

wr = weakref.ref(foo.bar)
assert wr() is not None # fails

потому что foo.bar - это новый экземпляр "foo instance" foo "bar" метод, отличный от m, и нет сильного ref для этого нового экземпляра, поэтому он немедленно gc'd, даже если вы создали сильная ссылка на него ранее (это не то же самое сильное ref). Чтобы быть ясным,

>>> d1 = foo.bla # assume bla is a data member
>>> d2 = foo.bla # assume bla is a data member
>>> d1 is d2
True # which is what you expect
>>> m1 = foo.bar # assume bar is an instance method
>>> m2 = foo.bar
>>> m1 is m2
False  # !!! counter-intuitive

Это заставляет многих врасплох, так как никто не ожидает доступа к члену экземпляра для создания нового экземпляра чего-либо. Например, если foo.bla является членом данных foo, то использование foo.bla в вашем коде не создает новый экземпляр объекта, на который ссылается foo.bla. Теперь, если bla является "функцией", foo.bla создает новый экземпляр типа "метод экземпляра", представляющий связанную функцию.

Почему weakref docs (поскольку python 2.4!) не указывает, что это очень странно, но это отдельная проблема.

Ответ 3

В то время как я вижу, что есть приемлемый ответ о том, почему это должно быть так, из простой ситуации использования, когда нужно, чтобы объект, который действует как weakref для связанного метода, я считаю, что можно было бы пробираться с помощью объекта как такового. Это своего рода руна по сравнению с некоторыми "кодовыми" вещами, но это работает.

from weakref import proxy

class WeakMethod(object):
    """A callable object. Takes one argument to init: 'object.method'.
    Once created, call this object -- MyWeakMethod() -- 
    and pass args/kwargs as you normally would.
    """
    def __init__(self, object_dot_method):
        self.target = proxy(object_dot_method.__self__)
        self.method = proxy(object_dot_method.__func__)
        ###Older versions of Python can use 'im_self' and 'im_func' in place of '__self__' and '__func__' respectively

    def __call__(self, *args, **kwargs):
        """Call the method with args and kwargs as needed."""
        return self.method(self.target, *args, **kwargs)

В качестве примера его простоты использования:

class A(object):
    def __init__(self, name):
        self.name = name
    def foo(self):
        return "My name is {}".format(self.name)

>>> Stick = A("Stick")
>>> WeakFoo = WeakMethod(Stick.foo)
>>> WeakFoo()
'My name is Stick'
>>> Stick.name = "Dave"
>>> WeakFoo()
'My name is Dave'

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

>>> A.foo = lambda self: "My eyes, aww my eyes! {}".format(self.name)
>>> Stick.foo()
'My eyes, aww my eyes! Dave'
>>> WeakFoo()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in __call__
ReferenceError: weakly-referenced object no longer exists
>>>

Если вы собираетесь заменять методы "на лету", вам может потребоваться использовать подход getattr(weakref.proxy(object), 'name_of_attribute_as_string'). getattr - довольно быстрый поиск, так что это не буквальная наихудшая вещь в мире, но в зависимости от того, что вы делаете, YMMV.