Почему python «gc.collect ()» работает не так, как ожидалось?

#python #python-3.x #garbage-collection

#python #python-3.x #сбор мусора

Вопрос:

Вот мой тестовый код:

 #! /usr/bin/python3
import gc
import ctypes

name = "a" * 50
name_id = id(name)
del name
gc.collect()
print(ctypes.cast(name_id, ctypes.py_object).value)
  

вывод:

 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
  

На мой взгляд, gc.collect() следует очистить переменную name и ее значение,
но почему я могу получить значение с name_id помощью after gc.collect() ?

Ответ №1:

Вы не должны ожидать gc.collect() , что здесь что-то будет сделано. gc просто управляет циклическим сборщиком мусора, который является вспомогательным сборщиком мусора, потому что CPython использует подсчет ссылок для своей стратегии управления основной памятью. Циклический сборщик мусора обрабатывает ссылочные циклы, здесь нет ссылочных циклов, поэтому gc.collect ничего не будет делать.

На мой взгляд, gc.collect() должен очистить имя переменной и ее значение,

Это просто не так, как работает Python. Переменная перестала существовать del name , но объект продолжает существовать, в данном случае, из-за оптимизации компилятора. Переменные Python не похожи на переменные C, они не являются фрагментами памяти, это имена, которые ссылаются на объекты в определенном пространстве имен.

В любом случае, разборка кода даст вам некоторое представление об этом:

 In [1]: import dis

In [2]: dis.dis("""
   ...: import gc
   ...: import ctypes
   ...:
   ...: name = "a" * 50
   ...: name_id = id(name)
   ...: del name
   ...: gc.collect()
   ...: print(ctypes.cast(name_id, ctypes.py_object).value)
   ...: """)
  2           0 LOAD_CONST               0 (0)
              2 LOAD_CONST               1 (None)
              4 IMPORT_NAME              0 (gc)
              6 STORE_NAME               0 (gc)

  3           8 LOAD_CONST               0 (0)
             10 LOAD_CONST               1 (None)
             12 IMPORT_NAME              1 (ctypes)
             14 STORE_NAME               1 (ctypes)

  5          16 LOAD_CONST               2 ('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')
             18 STORE_NAME               2 (name)

  6          20 LOAD_NAME                3 (id)
             22 LOAD_NAME                2 (name)
             24 CALL_FUNCTION            1
             26 STORE_NAME               4 (name_id)

  7          28 DELETE_NAME              2 (name)

  8          30 LOAD_NAME                0 (gc)
             32 LOAD_METHOD              5 (collect)
             34 CALL_METHOD              0
             36 POP_TOP

  9          38 LOAD_NAME                6 (print)
             40 LOAD_NAME                1 (ctypes)
             42 LOAD_METHOD              7 (cast)
             44 LOAD_NAME                4 (name_id)
             46 LOAD_NAME                1 (ctypes)
             48 LOAD_ATTR                8 (py_object)
             50 CALL_METHOD              2
             52 LOAD_ATTR                9 (value)
             54 CALL_FUNCTION            1
             56 POP_TOP
             58 LOAD_CONST               1 (None)
             60 RETURN_VALUE
  

Итак, когда ваш блок кода был скомпилирован, компилятор CPython заметил, что "a"*50 его можно превратить в константу, и так оно и было. Он хранит константы для объектов кода до тех пор, пока этот объект кода больше не будет существовать (в данном случае, когда интерпретатор существует). Поскольку этот объект code будет поддерживать ссылку на этот строковый объект, он будет существовать все время.

Итак, более явно:

 In [4]: code = compile("""name = "a" * 50""", filename='foo', mode='exec')

In [5]: code
Out[5]: <code object <module> at 0x7ff7c12495d0, file "foo", line 1>

In [6]: code.co_consts
Out[6]: ('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', None)
  

Обратите внимание также, что управление памятью Python является сложным и довольно непрозрачным. Все объекты обрабатываются в частной куче. То, что объект «выпущен», не означает, что среда выполнения не будет просто повторно использовать этот бит памяти для объектов того же типа (или других подходящих типов) по мере необходимости. Посмотрите на это:

 In [1]: class Foo: pass

In [2]: import ctypes

In [3]: foo = Foo()

In [4]: id(foo)
Out[4]: 140559250737552

In [5]: del foo

In [6]: foo2 = Foo()

In [7]: id(foo2)
Out[7]: 140559250737680

In [8]: ctypes.cast(140559250737552, ctypes.py_object).value
Out[8]: <prompt_toolkit.lexers.pygments.RegexSync at 0x7fd68035c990>

In [9]: id(foo2)
Out[9]: 140559250737680

In [10]: del foo2

In [11]: ctypes.cast(140559250737680, ctypes.py_object).value
Out[11]: <prompt_toolkit.lexers.pygments.PygmentsLexer at 0x7fd68035ca10>
  

Обратите внимание, как вы можете восстановить некоторые объекты в этих случаях, потому что интерактивная оболочка ipython постоянно создает объекты, а внутренняя куча рада повторно использовать эту память.

Посмотрите, что происходит в более простой версии:

 (base) juanarrivillaga@50-254-139-253-static% python
Python 3.7.9 (default, Aug 31 2020, 07:22:35)
[Clang 10.0.0 ] :: Anaconda, Inc. on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import ctypes
>>> class Foo: pass
...
>>> foo = Foo()
>>> i = id(foo)
>>> del foo
>>> ctypes.cast(i, ctypes.py_object).value
zsh: segmentation fault  python
  

Так что да. Более того, чего можно было ожидать, мы попытались получить доступ к части памяти, которая была не только восстановлена внутренней кучей, но и освобождена процессом Python, и, таким образом, мы получили ошибку сегментации.

Комментарии:

1. Я пробовал так: n = 50, name = «a» * n . В этом случае name отсутствует в co_consts, но я также могу получить значение по name_id

2. @olivetree123 да, и что ? Интерпретатор Python может повторно использовать память, если это необходимо или нет. В этом случае этот идентификатор был отправлен обратно в кучу, и он не был освобожден. Это полностью зависит от множества различных оптимизаций, которые существуют для встроенных типов в частной куче. Повторите попытку через 10 минут после выделения строк аналогичного размера или других объектов, и вы можете получить ошибку segfault. Например, я получаю сообщение об ошибке шины, когда я только что попробовал это. Что вы пытаетесь понять, в принципе?