Зачем связанный метод в Python объект создается циклическая ссылка?
я работаю в Python 2.7, и мне нравится эта проблема, которая меня озадачивает.
это самый простой пример:
>>> class A(object):
def __del__(self):
print("DEL")
def a(self):
pass
>>> a = A()
>>> del a
DEL
это нормально, как и ожидалось... теперь я пытаюсь изменить a() метод объекта a и что происходит, что после его изменения я не могу удалить a больше:
>>> a = A()
>>> a.a = a.a
>>> del a
просто, чтобы сделать некоторые проверки я печатать a.a ссылка до и после задание
>>> a = A()
>>> print a.a
<bound method A.a of <__main__.A object at 0xe86110>>
>>> a.a = a.a
>>> print a.a
<bound method A.a of <__main__.A object at 0xe86110>>
наконец-то я использовал objgraph модуль, чтобы попытаться понять, почему объект не выпущен:
>>> b = A()
>>> import objgraph
>>> objgraph.show_backrefs([b], filename='pre-backref-graph.png')

>>> b.a = b.a
>>> objgraph.show_backrefs([b], filename='post-backref-graph.png')

как вы можете видеть в разделе post-backref-graph.png изображение есть __self__ ссылки в b, которые не имеют смысла для меня, потому что собственные ссылки метода экземпляра должны игнорироваться (как было до назначения).
кто-нибудь может объяснить, почему такое поведение и как я могу обойти это?
3 ответов
когда вы пишите a.a, Он эффективно работает:
A.a.__get__(a, A)
потому что вы не получаете доступ к предварительно привязанному методу, но класса что связаны во время выполнения.
когда вы
a.a = a.a
вы эффективно "кэшируете" акт привязки метода. Поскольку связанный метод имеет ссылку на объект (очевидно, как он должен пройти self функции), это создает циклическую ссылку.
так Я моделирую вашу проблему, как:
class A(object):
def __del__(self):
print("DEL")
def a(self):
pass
def log_all_calls(function):
def inner(*args, **kwargs):
print("Calling {}".format(function))
try:
return function(*args, **kwargs)
finally:
print("Called {}".format(function))
return inner
a = A()
a.a = log_all_calls(a.a)
a.a()
вы можете использовать слабые ссылки для привязки по требованию внутри lof_all_calls как:
import weakref
class A(object):
def __del__(self):
print("DEL")
def a(self):
pass
def log_all_calls_weakmethod(method):
cls = method.im_class
func = method.im_func
instance_ref = weakref.ref(method.im_self)
del method
def inner(*args, **kwargs):
instance = instance_ref()
if instance is None:
raise ValueError("Cannot call weak decorator with dead instance")
function = func.__get__(instance, cls)
print("Calling {}".format(function))
try:
return function(*args, **kwargs)
finally:
print("Called {}".format(function))
return inner
a = A()
a.a = log_all_calls_weakmethod(a.a)
a.a()
это действительно уродливо, поэтому я бы предпочел извлечь его, чтобы сделать weakmethod оформитель:
import weakref
def weakmethod(method):
cls = method.im_class
func = method.im_func
instance_ref = weakref.ref(method.im_self)
del method
def inner(*args, **kwargs):
instance = instance_ref()
if instance is None:
raise ValueError("Cannot call weak method with dead instance")
return func.__get__(instance, cls)(*args, **kwargs)
return inner
class A(object):
def __del__(self):
print("DEL")
def a(self):
pass
def log_all_calls(function):
def inner(*args, **kwargs):
print("Calling {}".format(function))
try:
return function(*args, **kwargs)
finally:
print("Called {}".format(function))
return inner
a = A()
a.a = log_all_calls(weakmethod(a.a))
a.a()
готово!
FWIW, Python 3.4 не только не имеет этих проблем, он также имеет WeakMethod встроенные для вас.
ответ Veedrac о связанном методе, содержащем ссылку на экземпляр, является только частью ответа. Сборщик мусора CPython знает, как обнаруживать и обрабатывать циклические ссылки - за исключением случаев, когда какой-то объект, являющийся частью цикла, имеет __del__ метод, как упоминалось здесь https://docs.python.org/2/library/gc.html#gc.garbage:
объекты
__del__()методы и являются частью исходного цикла вызвать всю ссылочную цикл безнадежной, в том числе объекты не обязательно в цикле, но достижимы только из него. Python не собирает такие циклы автоматически, потому что в целом, Python не может угадать безопасный порядок, в котором запускать__del__()методы. (...) Обычно лучше избегать проблемы, не создавая циклы, содержащие объекты с__del__()методы и в этом случае мусор можно проверить, чтобы убедиться, что такие циклы не сотворенный.
IOW : удалить __del__ метод, и вы должны быть в порядке.
EDIT: wrt / ваш комментарий:
я использую его на объекте как функцию
a.a = functor(a.a). Когда тест сделано я хотел бы заменить функтор оригинальным методом.
затем раствор проста:
a = A()
a.a = functor(a.a)
test(a)
del a.a
пока вы явно не свяжете его,a не имеет экземпляра "a", поэтому он посмотрел на класс и новый method экземпляр возвращается (cf https://wiki.python.org/moin/FromFunctionToMethod подробнее об этом). Это method экземпляр затем вызывается и (обычно) отбрасывается.
относительно того, почему Python делает это. Технически все объекты, содержащие циклические ссылки, если у них есть методы. Однако сбор мусора займет гораздо больше времени, если сборщику мусора придется выполнять явные проверки методов объектов, чтобы убедиться, что освобождение объекта не вызовет проблемы. Как таковой Python хранит методы отдельно от объекта __dict__. Поэтому, когда вы пишете a.a = a.a, вы затеняете метод с самим собой в a поле объекта. И таким образом, существует явная ссылка на метод, который препятствует правильному освобождению объекта.
решение вашей проблемы не беспокоит, чтобы сохранить " кэш " исходного метода и просто удалить затененную переменную, когда вы закончите с ней. Это unshadow метод и снова сделать его доступным.
>>> class A(object):
... def __del__(self):
... print("del")
... def method(self):
... print("method")
>>> a = A()
>>> vars(a)
{}
>>> "method" in dir(a)
True
>>> a.method = a.method
>>> vars(a)
{'method': <bound method A.method of <__main__.A object at 0x0000000001F07940>>}
>>> "method" in dir(a)
True
>>> a.method()
method
>>> del a.method
>>> vars(a)
{}
>>> "method" in dir(a)
True
>>> a.method()
method
>>> del a
del
здесь vars показывает, что в