GIL, подсчёт ссылок, циклический GC, weakref, __slots__
Python управляет памятью автоматически, но понимание механизмов помогает избежать утечек и оптимизировать производительность.
В CPython каждый объект Python является структурой в C:
// Упрощённо:
typedef struct {
Py_ssize_t ob_refcnt; // счётчик ссылок
PyTypeObject *ob_type; // тип объекта
// ... данные объекта
} PyObject;Heap — вся динамически выделяемая память. Python имеет свой аллокатор поверх системного malloc.
import sys
x = [1, 2, 3]
print(id(x)) # адрес объекта в памяти
print(sys.getsizeof(x)) # 120 байт (только объект list, не данные!)
# getsizeof — поверхностный размер:
import sys
a = [1, 2, 3]
print(sys.getsizeof(a)) # 120 — размер list-объекта без элементов
# Для полного размера используйте рекурсивную функцию:
def deep_getsizeof(obj, seen=None):
if seen is None:
seen = set()
obj_id = id(obj)
if obj_id in seen:
return 0
seen.add(obj_id)
size = sys.getsizeof(obj)
if isinstance(obj, (list, tuple, set)):
size += sum(deep_getsizeof(item, seen) for item in obj)
elif isinstance(obj, dict):
size += sum(deep_getsizeof(k, seen) + deep_getsizeof(v, seen)
for k, v in obj.items())
return sizeОсновной механизм управления памятью в CPython.
import sys
a = [1, 2, 3]
print(sys.getrefcount(a)) # 2: одна для 'a', одна для аргумента getrefcount
b = a # refcount = 3
c = [a, b] # refcount = 5 (a, b, c[0], c[1] + аргумент)
del b # refcount уменьшается
del c # refcount уменьшается ещё на 2
# Когда refcount == 0, объект удаляется немедленноКак работает:
ob_refcnt = 1ob_refcnt += 1ob_refcnt -= 1ob_refcnt == 0: деструктор + освобождение памятиПлюсы:
Минусы:
Reference counting не может удалить циклы:
import gc
class Node:
def __init__(self, value):
self.value = value
self.ref = None
a = Node(1)
b = Node(2)
a.ref = b # a ссылается на b
b.ref = a # b ссылается на a — цикл!
del a, b
# refcount(a) = 1 (от b.ref), refcount(b) = 1 (от a.ref)
# Никогда не достигнут 0 без GC!
gc.collect() # явный вызов GC — находит и удаляет цикл
print(gc.collect()) # число удалённых объектовimport gc
# GC работает с тремя поколениями:
# Поколение 0: новые объекты (чаще всего собирается)
# Поколение 1: пережили 1 сборку поколения 0
# Поколение 2: пережили несколько сборок (редко собирается)
print(gc.get_threshold()) # (700, 10, 10) по умолчанию
# Поколение 0 собирается когда в нём 700+ объектов
# Поколение 1 собирается каждые 10 сборок поколения 0
# Поколение 2 собирается каждые 10 сборок поколения 1
gc.set_threshold(1000, 15, 15) # настройка порогов
# Для производительности можно отключить GC (только если нет циклов):
gc.disable()
gc.enable()
# Принудительная сборка:
gc.collect() # все поколения
gc.collect(0) # только поколение 0
gc.collect(1) # поколение 0 и 1
# Найти объекты в GC:
for obj in gc.get_objects():
if isinstance(obj, MyClass):
print(id(obj), obj)GIL — мьютекс, который позволяет только одному потоку выполнять Python-байткод в любой момент.
import threading
import time
counter = 0
def increment():
global counter
for _ in range(1_000_000):
counter += 1 # НЕ атомарная операция без GIL!
# Без GIL counter += 1 не был бы безопасным:
# 1. LOAD_GLOBAL counter
# 2. LOAD_CONST 1
# 3. BINARY_ADD
# 4. STORE_GLOBAL counter
# GIL гарантирует что никакой другой поток не влезет между этими шагами
t1 = threading.Thread(target=increment)
t2 = threading.Thread(target=increment)
t1.start(); t2.start()
t1.join(); t2.join()
print(counter) # 2_000_000 — корректно, благодаря GILПочему GIL не мешает I/O:
# GIL освобождается при системных вызовах:
# - socket.recv(), socket.send()
# - file.read(), file.write()
# - time.sleep()
# - C extensions (если они явно освобождают GIL)
# Поэтому threading работает для I/O-bound задач:
import requests
def fetch(url):
return requests.get(url).text # освобождает GIL во время HTTP-запроса
with ThreadPoolExecutor(max_workers=10) as pool:
results = list(pool.map(fetch, urls)) # параллельно!Python 3.13 — Free-threading:
# Экспериментальная сборка python3.13t
# Позволяет потокам выполняться параллельно для CPU-bound задач
# Пока нестабильно для productionweakref)Слабая ссылка не увеличивает ob_refcnt — объект может быть удалён, даже если слабые ссылки на него существуют.
import weakref
class Cache:
pass
obj = Cache()
weak = weakref.ref(obj) # слабая ссылка
print(weak()) # <Cache object> — объект ещё жив
del obj
print(weak()) # None — объект удалён, weak ссылка стала пустой
# Callback при удалении объекта:
def on_destroy(ref):
print(f"Объект удалён! Ref: {ref}")
obj = Cache()
weak = weakref.ref(obj, on_destroy)
del obj # выведет: "Объект удалён! Ref: <weakref ...>"weakref.WeakValueDictionary — кэш без удержания объектовimport weakref
class ExpensiveObject:
def __init__(self, name):
self.name = name
print(f"Создан: {name}")
def __del__(self):
print(f"Удалён: {self.name}")
# Обычный dict удерживает объекты:
cache = {}
cache['key'] = ExpensiveObject("obj1")
# При удалении из cache объект остаётся если есть другие ссылки
# WeakValueDictionary: объекты удаляются когда нет других ссылок
cache = weakref.WeakValueDictionary()
obj1 = ExpensiveObject("obj1")
cache['key'] = obj1
print(cache['key'].name) # obj1 — объект есть
del obj1 # удаляем последнюю сильную ссылку
# Теперь cache['key'] автоматически удаляется из словаря
print('key' in cache) # Falseweakref.WeakSet и weakref.WeakKeyDictionary# WeakSet — набор слабых ссылок на объекты
listeners = weakref.WeakSet()
class EventListener:
def handle(self, event): ...
listener = EventListener()
listeners.add(listener)
# Когда listener удаляется — автоматически пропадает из listeners
# WeakKeyDictionary — слабые ключи
properties = weakref.WeakKeyDictionary()
obj = SomeObject()
properties[obj] = {"color": "red", "size": 42}
# При удалении obj запись из properties автоматически удалится__slots__ — экономия памятиimport sys
class WithDict:
def __init__(self, x, y):
self.x = x
self.y = y
class WithSlots:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
d = WithDict(1, 2)
s = WithSlots(1, 2)
print(sys.getsizeof(d)) # 48 bytes (сам объект)
print(sys.getsizeof(d.__dict__)) # 104 bytes (словарь атрибутов)
print(sys.getsizeof(s)) # 56 bytes (с дескрипторами, без __dict__)
# Нет __dict__ — экономия 104 bytes на экземпляр!
# При 1 млн объектов: ~104 МБ экономииОграничения __slots__:
s = WithSlots(1, 2)
s.z = 3 # AttributeError: 'WithSlots' object has no attribute 'z'
# Наследование: если базовый класс без __slots__, подкласс имеет __dict__
class Base:
__slots__ = ('a',)
class Child(Base):
__slots__ = ('b',) # правильно: добавляем только новые слоты
# Для pickle совместимости нужен __getstate__/__setstate__
class Slotted:
__slots__ = ('x', 'y')
def __getstate__(self): return {'x': self.x, 'y': self.y}
def __setstate__(self, state): self.x = state['x']; self.y = state['y']tracemalloc — встроенное отслеживаниеimport tracemalloc
tracemalloc.start(10) # 10 фреймов в стеке трассировки
# ... ваш код ...
result = [list(range(1000)) for _ in range(100)]
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
print("Топ 3 аллокаторов:")
for stat in top_stats[:3]:
print(stat)
# Сравнение двух снимков:
snapshot1 = tracemalloc.take_snapshot()
# ... больше кода ...
snapshot2 = tracemalloc.take_snapshot()
top_stats = snapshot2.compare_to(snapshot1, 'lineno')memory_profiler — построчное профилированиеfrom memory_profiler import profile
@profile
def memory_heavy():
a = [0] * 1_000_000 # большой список
b = {i: i for i in range(100_000)} # словарь
del a # освобождение
return b
memory_heavy()
# Выводит потребление памяти построчноobjgraph — граф объектовimport objgraph
# Найти самые частые типы объектов
objgraph.show_most_common_types(limit=10)
# Найти утечки (новые объекты с последнего вызова)
objgraph.show_growth(limit=5)
# Визуализировать ссылки
objgraph.show_backrefs(my_object, max_depth=3)CPython использует собственный аллокатор (pymalloc) поверх системного malloc:
Арена (arena): 256 KB — выделяется через mmap
└── Пулы (pool): 4 KB каждый
└── Блоки (block): 8–512 байт (размер кратен 8)
Важное следствие:
# Кэш целых чисел: -5 до 256 создаются при старте интерпретатора
a = 42
b = 42
print(a is b) # True — один и тот же объект!
a = 1000
b = 1000
print(a is b) # False — разные объекты (число больше 256)
# Интернирование строк:
a = "hello"
b = "hello"
print(a is b) # True — Python кэширует короткие "идентификаторные" строки
a = "hello world!"
b = "hello world!"
print(a is b) # Зависит от реализации — не полагайтесь на это!# 1. Используйте del для явного удаления больших объектов
large_data = load_gigabyte_file()
process(large_data)
del large_data # намекаем GC что объект можно удалить
gc.collect() # принудительно если нужно немедленно
# 2. Генераторы вместо списков для больших данных
# Плохо:
all_data = [parse(line) for line in open('huge_file.csv')]
# Хорошо:
all_data = (parse(line) for line in open('huge_file.csv'))
# 3. __slots__ для классов с миллионами экземпляров
class Point:
__slots__ = ('x', 'y')
# 4. WeakRef для кэшей
import functools
@functools.cached_property
def expensive_prop(self):
return compute() # вычисляется один раз, хранится в self.__dict__
# 5. array.array для однотипных данных
import array
floats = array.array('d', range(1_000_000)) # вместо list float
# 6. numpy для матриц и массивов
import numpy as np
matrix = np.zeros((1000, 1000), dtype=np.float32) # 4 MB вместо ~400 MB# 1. Глобальные списки/словари накапливают объекты
_event_handlers = [] # никогда не очищается!
def register(handler):
_event_handlers.append(handler) # объект удерживается вечно
# Решение: weakref
import weakref
_event_handlers = weakref.WeakSet()
# 2. Замыкания захватывают большие объекты
def make_handler(large_data):
def handler(event):
# large_data захвачен в замыкании!
return process(event, large_data)
return handler
# Решение: захватывать только нужное
def make_handler(large_data):
summary = extract_summary(large_data) # только что нужно
def handler(event):
return process(event, summary)
return handler
# 3. Циклические ссылки с __del__
class Node:
def __init__(self):
self.child = None
def __del__(self): # __del__ + цикл = GC не может удалить!
print("deleted")
# GC ≥3.4 умеет удалять циклы с __del__, но finalize порядок не гарантированtracemallocimport tracemalloc
import linecache
def display_top(snapshot, key_type='lineno', limit=10):
stats = snapshot.statistics(key_type)
print(f"Top {limit} lines")
for index, stat in enumerate(stats[:limit], 1):
frame = stat.traceback[0]
print(f"#{index}: {frame.filename}:{frame.lineno}: "
f"{stat.size/1024:.1f} KiB")
line = linecache.getline(frame.filename, frame.lineno).strip()
if line:
print(f" {line}")
other = stats[limit:]
if other:
size = sum(stat.size for stat in other)
print(f"{len(other)} other: {size/1024:.1f} KiB")
total = sum(stat.size for stat in stats)
print(f"Total allocated size: {total/1024:.1f} KiB")
# Использование:
tracemalloc.start(25)
# ... ваш код с подозрением на утечку ...
run_application()
snapshot = tracemalloc.take_snapshot()
display_top(snapshot)gc.get_referrers() — кто держит объектimport gc
class LeakedObject:
pass
obj = LeakedObject()
ref = obj # вторая ссылка
referrers = gc.get_referrers(obj)
for r in referrers:
print(type(r), r)
# <class 'dict'> {'obj': ..., 'ref': ...} — frame locals__del__ и финализация объектовclass Resource:
def __init__(self, name):
self.name = name
print(f"Открыт: {name}")
def __del__(self):
# ⚠️ НЕ полагайтесь на __del__ для освобождения ресурсов!
# Может не вызваться при завершении интерпретатора
print(f"Закрыт: {self.name}")
# Лучше: используйте контекстные менеджеры
class Resource:
def __enter__(self):
return self
def __exit__(self, *args):
self.close()
# Или weakref.finalize — надёжная альтернатива __del__
import weakref
class Resource:
def __init__(self, name):
self.name = name
self._finalizer = weakref.finalize(self, Resource._cleanup, name)
@staticmethod
def _cleanup(name):
print(f"Cleaned up: {name}") # вызывается при сборке мусора
r = Resource("conn")
del r # → "Cleaned up: conn"gc.is_tracked() и gc.get_referents().weakref.WeakValueDictionary — убедитесь что объекты удаляются при отсутствии внешних ссылок.__slots__ и без через sys.getsizeof().tracemalloc чтобы найти где в вашем коде аллоцируется больше всего памяти.sys.getrefcount(x) всегда возвращает как минимум 2.@track_memory который логирует потребление памяти до и после вызова функции.weakref.finalize для автоматической очистки истёкших записей.Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.