Почему в этом случае новый класс стиля и класс старого стиля имеют другое поведение?

Я нашел что-то интересное, вот фрагмент кода:

class A(object):
    def __init__(self):
        print "A init"

    def __del__(self):
        print "A del"

class B(object):
    a = A()

Если я запустил этот код, я получу:

A init

Но если я изменю class B(object) на class B(), я получу:

A init
A del

Я нашел примечание в __ del__ doc:

Не гарантируется, что для объектов вызываются del() которые все еще существуют, когда переводчик завершает работу.

Тогда, я думаю, это потому, что B.a по-прежнему ссылается (ссылка на класс B), когда существует интерпретатор.

Итак, я добавил del B до того, как интерпретатор существует вручную, а затем я нашел a.__del__().

Теперь я немного смущен. Почему a.__del__() вызывается при использовании класса старого стиля? Почему классы нового и старого стиля имеют другое поведение?

Я нашел аналогичный вопрос здесь, но я думаю, что ответы недостаточно ясны.

Ответ 1

TL; DR: это старая проблема в CPython, которая была окончательно исправлена ​​в CPython 3.4. Объекты, хранящиеся в режиме реального времени по ссылочным циклам, на которые ссылаются глобальные модули модуля, не были должным образом завершены на выходе интерпретатора в версиях CPython до 3.4. Классы нового стиля имеют неявные циклы в своих экземплярах type; классы старого стиля (типа classobj) не имеют неявных эталонных циклов.

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


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

>>> class A(object):
...     pass
>>> A.__mro__[0] is A
True

Это означает, что они не могут быть немедленно удалены *, но только при запуске сборщика мусора. Поскольку ссылка на них удерживается главным модулем, они остаются в памяти до выключения интерпретатора. В конце, во время очистки модуля, все глобальные имена модуля в основном настроены на None, и в зависимости от того, какие объекты имели счетчики ссылок, уменьшились до нуля (например, старый стиль), также были удалены, Тем не менее, классы нового стиля, имеющие ссылочные циклы, не будут выпущены/завершены этим.

Циклический сборщик мусора не будет запускаться на выходе интерпретатора (что разрешено документацией CPython:

Не гарантируется, что методы __del__() вызываются для объектов, которые все еще существуют, когда переводчик завершает работу.


Теперь классы старого стиля в Python 2 не имеют неявных циклов. Когда код очистки/завершения модуля CPython устанавливает глобальные переменные в None, остается только оставшаяся ссылка на класс B; то B удаляется, и последняя ссылка на a отбрасывается, а также завершается a.


Чтобы продемонстрировать тот факт, что классы нового стиля имеют циклы и требуют GC sweep, тогда как в классах старого стиля нет, вы можете попробовать следующую программу в CPython 2 (у CPython 3 нет классов старого стиля больше):

import gc
class A(object):
    def __init__(self):
        print("A init")

    def __del__(self):
        print("A del")

class B(object):
    a = A()

del B
print("About to execute gc.collect()")
gc.collect()

С B в качестве класса нового стиля, как указано выше, вывод

A init
About to execute gc.collect()
A del

С B в качестве класса старого стиля (class B:) вывод

A init
A del
About to execute gc.collect()

То есть класс нового стиля был удален только после gc.collect(), хотя последняя внешняя ссылка на него уже была отброшена; но класс старого стиля был удален мгновенно.


Значительная часть этого уже исправлена ​​ в Python 3.4: благодаря PEP 442, который включал процедуру отключения модуля на основе кода GC. Теперь даже при выходе интерпретатора глобалы модуля завершаются с помощью обычной сборки мусора. Если вы запустите свою программу под Python 3.4, программа напечатает

A init
A del

В то время как с Python <= 3.3, он будет печатать

A init

( Обратите внимание на, что другие реализации по-прежнему могут или не могут выполнить __del__ в данный момент, независимо от версии их выше, на или ниже, 3.4)