Зачем связанный метод в 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
показывает, что в