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

pre-backref-graph.png

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

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 показывает, что в