Синтаксис, переменные и 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:
- Конструируется (или достаётся из small-int cache, см. урок 2) объект PyLong со значением 5.
- В словаре локального namespace (
locals()или модульного) создаётся запись'x' → ptr_к_PyLong. - Refcount объекта инкрементируется.
Команда y = x:
- Достаётся ptr из имени
x. - В namespace создаётся запись
'y' → тот_же_ptr. - Refcount того же объекта инкрементируется ещё раз. Объект не копируется.
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)
Никогда не используйте 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 снова
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 и RecordBatchCross-course → ClickHouse: 02/01 optimal-types — типы Python (
int,float,str) против ClickHouseInt64/Float64/String/LowCardinality(String). Различие в representation: Python — heap-allocated PyObject (28+ байт даже для нуля), ClickHouse — packed primitives (8 байтInt64). Identity-семантикаisvs==имеет смысл только в языках с reference semantics; в SQL=— всегда equality.
Ключевые выводы
- Имена — это биндинги к
PyObject*, а не контейнеры значений. Присваивание не копирует объект, оно создаёт ещё одну ссылку. isсравнивает identity,==— equality. Их совпадение для int/str — следствие small-int cache и interning, а не language semantics.- CPython считает refcount автоматически, плюс generational GC ловит циклы. Это объясняет, почему
delне всегда сразу освобождает память (если refcount > 0) и почему__del__запускается детерминированно для не-циклических объектов.