Learning Platform
Глоссарий Troubleshooting
Урок 02.01 · 20 мин
Средний
VariablesReferencesRefcountis vs ==id()
Требуемые знания:
  • Базовый Python (вы прошли M00)

Синтаксис, переменные и reference semantics

В Python нет «переменных» в том смысле, в котором они существуют в C. Нет ячейки в стеке, в которой лежит значение 5, и нет адреса этой ячейки в имени x. Вместо этого есть имена (names) — записи в namespace-словаре — которые ссылаются на объекты (PyObject) в куче. Команда x = 5 не создаёт box на стеке; она привязывает имя x к объекту PyLong со значением 5.

Это не педантичная мелочь. Из этой модели прямо вытекает поведение is/==, refcount, GC, мутабельности и того, почему a = []; b = a; b.append(1) неожиданно меняет a.


Имена → биндинги → объекты

Все объекты Python — это PyObject* (указатели на heap-allocated структуры, отслеживаемые garbage collector’ом). Каждый объект на уровне C имеет одинаковую «голову»:

  • ob_refcnt — счётчик ссылок (Py_ssize_t),
  • ob_type — указатель на объект типа (PyTypeObject),
  • type-specific payload (для int — массив digits, для str — массив codepoints, для list — массив указателей).

Команда x = 5:

  1. Конструируется (или достаётся из small-int cache, см. урок 2) объект PyLong со значением 5.
  2. В словаре локального namespace (locals() или модульного) создаётся запись 'x' → ptr_к_PyLong.
  3. Refcount объекта инкрементируется.

Команда y = x:

  1. Достаётся ptr из имени x.
  2. В namespace создаётся запись 'y' → тот_же_ptr.
  3. Refcount того же объекта инкрементируется ещё раз. Объект не копируется.
x = 5; y = x — обе ссылки на один PyLong
namespacelocals()Python's namespace — это dict от строковых имён к PyObject*. После y = x обе записи указывают на один и тот же объект.
PyLong (heap)ob_refcnt=2ob_refcnt инкрементирован с 1 до 2 при y = x. Сам объект не копируется — обе переменные смотрят на одну и ту же ячейку памяти.

is vs == — identity vs equality

Это два разных оператора:

  • is сравнивает identity — буквально id(a) == id(b), то есть указывают ли имена на один и тот же объект в памяти.
  • == сравнивает equality — вызывает __eq__ (метод протокола сравнения значений).

Для immutable примитивов (int, str) и интернированных значений эти операторы могут случайно совпасть — это implementation detail CPython, не language guarantee.

a = 1000
b = 1000
a is b   # implementation-defined; в CPython REPL обычно False, в одной expression может быть True
a == b   # always True

a = 256
b = 256
a is b   # True гарантированно — small-int cache (-5..256)
WARNING

Никогда не используйте is для проверки равенства значений. is зарезервирован для identity-проверок: x is None, x is True, x is False, sentinel-объекты. PEP 8 явно об этом говорит. Использование is для int/str — это баг, ждущий, когда значение выйдет из кэша.


id() и identity

Функция id(obj) возвращает «адрес» объекта — числовое представление, в CPython это (по сути) PyObject*, преобразованный в integer. Стабилен пока объект жив; после освобождения памяти этот же id может быть переиспользован для совершенно другого объекта.

Демонстрация small-int cache (значения сверены с baseline runtime cpython-3.12.7):

a = -5
b = -5
print(a is b)    # True — в small-int cache

a = 256
b = 256
print(a is b)    # True — верхняя граница cache

a = 257
b = 257
print(a is b)    # False — за пределами кэша, два отдельных PyLong

a = -6
b = -6
print(a is b)    # False — за пределами кэша

Cache range — [-5, 256] — закодирован в _PyLong_SMALL_INTS массиве (см. Objects/longobject.c в исходниках CPython). Подробнее — в следующем уроке.


Reference counting

Каждое присваивание имени или передача в функцию инкрементирует ob_refcnt; каждое выход из scope или rebinding декрементирует. Когда счётчик доходит до 0, CPython вызывает __del__ (если есть) и освобождает память. См. Include/object.h, макросы Py_INCREF/Py_DECREF.

Функция sys.getrefcount(x) возвращает текущее значение ob_refcnt — но +1, потому что сам аргумент функции тоже создаёт временную ссылку.

import sys

a = []
print(sys.getrefcount(a))  # 2: a + аргумент getrefcount

b = a
print(sys.getrefcount(a))  # 3: a + b + аргумент

del b
print(sys.getrefcount(a))  # 2 снова
TIP

sys.getrefcount(obj) всегда возвращает refcount + 1. Не пугайтесь начальной «двойки» для свежесозданного локального — это сам аргумент. Для отладки утечек смотрите на изменение значения, а не на абсолютное число.


GC и циклические ссылки

Refcount-only стратегия не справляется с циклами:

a = []
b = []
a.append(b)   # refcount(b) = 2 (b + a[0])
b.append(a)   # refcount(a) = 2 (a + b[0])
del a, b      # каждый refcount упал на 1, но не до 0 — обе ссылки внутри списков
              # без cycle GC память течёт

Чтобы ловить такие циклы, CPython дополняет refcount generational garbage collector (модуль gc, исходники в Modules/gcmodule.c). GC периодически обходит «достижимые из root» объекты и собирает то, что недостижимо, даже если refcount > 0. Для простых не-циклических случаев refcount достаточно — он O(1) и предсказуемый, в отличие от tracing GC.


Cross-course context

Arrow Memory Layout: буферы, bitmap и RecordBatch

Cross-course → ClickHouse: 02/01 optimal-types — типы Python (int, float, str) против ClickHouse Int64 / Float64 / String / LowCardinality(String). Различие в representation: Python — heap-allocated PyObject (28+ байт даже для нуля), ClickHouse — packed primitives (8 байт Int64). Identity-семантика is vs == имеет смысл только в языках с reference semantics; в SQL = — всегда equality.


Ключевые выводы

  1. Имена — это биндинги к PyObject*, а не контейнеры значений. Присваивание не копирует объект, оно создаёт ещё одну ссылку.
  2. is сравнивает identity, == — equality. Их совпадение для int/str — следствие small-int cache и interning, а не language semantics.
  3. CPython считает refcount автоматически, плюс generational GC ловит циклы. Это объясняет, почему del не всегда сразу освобождает память (если refcount > 0) и почему __del__ запускается детерминированно для не-циклических объектов.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. После выполнения `x = [1, 2, 3]; y = x` что происходит с объектами в памяти?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 7