2014-10-02 1 views
8

Я работаю в Python 2.7, и мне нравится эта проблема, которая меня озадачивает.Зачем устанавливать связанный метод для объекта python, создать круговую ссылку?

Это самый простой пример:

>>> 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, которые не имеют для меня никакого смысла, потому что self r ebooks метода instance следует игнорировать (как было до назначения).

Кто-нибудь может объяснить, почему это поведение и как я могу его обойти?

ответ

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, предварительно построенный для вас.

+0

Хорошо ... Есть ли способ избежать этого? Я должен кэшировать некоторые методы и позже восстановить метод: возможно ли это? –

+0

Это зависит от того, что вы пытаетесь сделать. – Veedrac

+0

OK Я нашел решение: aa = types.MethodType (Aa, a, A) –

4

Ответ 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 не имеет «а» экземпляр atribute, поэтому он посмотрел на класс и новый method экземпляр возвращается (cf https://wiki.python.org/moin/FromFunctionToMethod для получения дополнительной информации об этом). Этот экземпляр method затем вызывается и (обычно) отбрасывается.

+0

Это не решение .... Мне нужен метод __del__ (причины этого вне сферы действия здесь), и этот вопрос является единственным, что я не знаю, как решить, для других слабых ссылок хорошо работает –

+1

Well , он действительно отвечает на ваш вопрос: «Кто-нибудь может объяснить, почему это поведение и как я могу его обойти?» - вы не указали, что вам нужен '__del__', и ваш фрагмент не подразумевает, что он действительно используется;) Теперь, если вы посмотрите на ссылку, я указал вам тоже, есть еще немного по теме ... –

+0

Как писать в ссылке, которую вы опубликовали наилучшим образом. Не создавайте цикл, и я спросил: «Я не понимаю, почему родился этот цикл, и если есть способ не создавать подобные циклы» ... но удалите __del__ не удаляйте цикл, а просто побочные эффекты цикла :) –

1

Что касается того, почему Python делает это. Технически все объекты содержат круглые ссылки, если у них есть методы. Однако сбор мусора займет гораздо больше времени, если сборщик мусора должен был выполнить явные проверки методов объектов, чтобы убедиться, что освобождение объекта не вызовет проблемы. Поскольку такой Python хранит методы отдельно от объекта __dict__. Поэтому, когда вы пишете a.a = a.a, вы затеняете метод самим собой в поле a объекта. Таким образом, существует явная ссылка на метод, который не позволяет объекту правильно освобождаться.

Решение вашей проблемы не мешает хранить «кеш» исходного метода и просто удалять затененную переменную, когда вы закончите с ней. Это отменит метод и снова сделает его доступным.

>>> 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 показывает, что в атрибуте объекта __dict__. Обратите внимание, как __dict__ не содержит ссылки на себя, хотя a.__dict__ действителен. dir создает список всех атрибутов, доступных из данного объекта. Здесь мы можем увидеть все атрибуты и методы для объекта, а также все методы и атрибуты его классов и их баз.Это показывает, что связанный метод a хранится на месте отдельно от того, где хранятся атрибуты a.

+0

Спасибо за ваш ответ, но реальный вопрос не" почему объект не был уничтожен? " но почему 'a.a = a.a' создает круговую ссылку. Если вы удалите переопределение '__del__' и попробуйте построить график до и после назначения, вы увидите, что графики разные, а второй - круглые. –

+0

Непонятный вопрос. Я полностью изменил свой ответ с новой рекомендацией и сам научился этому процессу. – Dunes

+0

THX: это именно то, что я имел в виду: «Зачем устанавливать связанный метод для объекта python, создать круговую ссылку?». Veedrac дал мне способ работать с какой-то проблемой, связанной с этим, но с вашим ответом, который я действительно понимаю. –

Смежные вопросы