Почему недавно созданная переменная в Python имеет счетчик ссылок четыре?

Я работаю над презентацией для коллег, чтобы объяснить основное поведение и аргументацию за GIL, и нашел то, что я не мог объяснить, собрав краткое объяснение подсчета ссылок. Похоже, что новые объявленные переменные имеют четыре ссылки, а не ту, которую я ожидал бы. Например, следующий код:

the_var = 'Hello World!'
print('Var created: {} references'.format(sys.getrefcount(the_var)))

Результаты этого вывода:

Var created: 4 references

Я подтвердил, что вывод был таким же, если я использовал целое число > 100 (< 100 предварительно создано и имеет большее количество ссылок) или float, и если я объявил переменную в пределах области действия или в петля. Результат был тот же. Поведение также, по-видимому, одинаково в 2.7.11 и 3.5.1.

Я попытался отлаживать sys.getrefcount, чтобы узнать, не создает ли он дополнительные ссылки, но не смог войти в функцию (я предполагаю, что это прямой удар до уровня C).

Я знаю, что я собираюсь получить хотя бы один вопрос об этом, когда я представляю, и в любом случае я действительно озадачен выходом. Может ли кто-нибудь объяснить это поведение мне?

Ответ 1

Существует несколько сценариев, которые приведут к другому количеству ссылок. Самая простая консоль REPL:

>>> import sys
>>> the_var = 'Hello World!'
>>> print(sys.getrefcount(the_var))
2

Понимание этого результата довольно прямолинейно - в локальном стеке есть одна ссылка и другая временная/локальная функция sys.getrefcount() (даже документация предупреждает об этом - The count returned is generally one higher than you might expect). Но когда вы запускаете его как автономный script:

import sys

the_var = 'Hello World!'
print(sys.getrefcount(the_var))
# 4

как вы заметили, вы получите 4. Так что же дает? Хорошо, давайте исследовать... Существует очень полезный интерфейс для сборщика мусора - модуль gc - поэтому, если мы запустим его в консоли REPL:

>>> import gc
>>> the_var = 'Hello World!'
>>> gc.get_referrers(the_var)
[{'__builtins__': <module '__builtin__' (built-in)>, '__package__': None, 'the_var': 'Hello 
World!', 'gc': <module 'gc' (built-in)>, '__name__': '__main__', '__doc__': None}]

Там нет чудес, - это по существу только текущее пространство имен (locals()), поскольку переменная не существует нигде. Но что происходит, когда мы запускаем это как автономный script:

import gc
import pprint

the_var = 'Hello World!'
pprint.pprint(gc.get_referrers(the_var))

это выдает (YMMV, на основе вашей версии Python):

[['gc',
  'pprint',
  'the_var',
  'Hello World!',
  'pprint',
  'pprint',
  'gc',
  'get_referrers',
  'the_var'],
 (-1, None, 'Hello World!'),
 {'__builtins__': <module '__builtin__' (built-in)>,
  '__doc__': None,
  '__file__': 'test.py',
  '__name__': '__main__',
  '__package__': None,
  'gc': <module 'gc' (built-in)>,
  'pprint': <module 'pprint' from 'D:\Dev\Python\Py27-64\lib\pprint.pyc'>,
  'the_var': 'Hello World!'}]

Конечно, у нас есть еще две ссылки в списке, как сказал нам sys.getrefcount(), но что, черт возьми, это? Ну, когда интерпретатор Python анализирует ваш script, он сначала должен compile его на байт-код - и пока он это делает, он сохраняет все строки в списке, который, так как он упоминает вашу переменную, также объявляется ссылкой на нее.

Вторая более загадочная запись ((-1, None, 'Hello World!')) происходит из оптимизатора peep-hole и есть только оптимизация доступа (ссылка на строку в этом случай).

Оба из них являются чисто временными и необязательными - консоль REPL выполняет разделение контекста, поэтому вы не видите эти ссылки, если вы должны "перенаправить" свои компиляции из вашего текущего контекста:

import gc
import pprint

exec(compile("the_var = 'Hello World!'", "<string>", "exec"))
pprint.pprint(gc.get_referrers(the_var))

вы получите:

[{'__builtins__': <module '__builtin__' (built-in)>,
  '__doc__': None,
  '__file__': 'test.py',
  '__name__': '__main__',
  '__package__': None,
  'gc': <module 'gc' (built-in)>,
  'pprint': <module 'pprint' from 'D:\Dev\Python\Py27-64\lib\pprint.pyc'>,
  'the_var': 'Hello World!'}]

и если вы вернетесь к первоначальной попытке получить счетчик ссылок через sys.getreferencecount():

import sys

exec(compile("the_var = 'Hello World!'", "<string>", "exec"))
print(sys.getrefcount(the_var))
# 2

как в консоли REPL, и так же, как ожидалось. Дополнительная ссылка, связанная с оптимизацией peep-hole, поскольку она происходит на месте, может быть немедленно отброшена путем принудительного сбора мусора (gc.collect()) перед подсчетом ваших ссылок.

Однако список строк, созданный во время компиляции, не может быть выпущен до тех пор, пока весь файл не будет проанализирован и не скомпилирован, поэтому, если вы хотите импортировать script в другой script, а затем подсчитать ссылки на the_var из него вы получите 3 вместо 4 только тогда, когда думаете, что больше не можете вас смутить;)