На senior-собесе спрашивают, как Python управляет памятью и как вы ищете узкие места. Ответ «Python медленный» не принимается: нужно знать, где именно он медленный и что с этим делать.
Подсчёт ссылок
У каждого объекта в CPython есть счётчик ссылок. Присваивание, добавление в список, передача аргументом увеличивают его, выход из области и del уменьшают. Когда счётчик падает до нуля, объект освобождается сразу. Поэтому файлы, открытые без with, в CPython обычно закрываются быстро, но полагаться на это нельзя: в PyPy и других реализациях сборка отложенная.
sys.getrefcount(x) показывает счётчик (на 1 больше из-за аргумента самой функции).
Сборщик циклов
Подсчёт ссылок не справляется с циклами: a.other = b; b.other = a. Для этого есть модуль gc: сборщик с поколениями периодически ищет недостижимые группы объектов-контейнеров и удаляет их. Его можно отключить (gc.disable()) или заморозить старые объекты (gc.freeze()), так иногда делают в веб-воркерах после форка, чтобы не портить copy-on-write страницы.
weakref создаёт ссылку, которая не увеличивает счётчик: кеши (WeakValueDictionary), обратные ссылки на родителя, подписчики.
Утечки памяти в Python
Сборщик есть, а утечки бывают:
- Растущие глобальные структуры: кеш без ограничения, список «на всякий случай».
lru_cacheбезmaxsizeили на методе (держитself).- Обработчики и подписки, которые не удаляются.
- Ссылки на большие объекты из traceback (
sys.exc_infoв долгоживущей переменной). - Фрагментация: процесс освободил объекты, но память ОС не вернул (аллокатор держит арены).
Инструменты: tracemalloc (снимки и сравнение, где выделялось), objgraph, memray, gc.get_objects().
Сколько весит объект
Python-объекты тяжёлые: даже маленький int занимает десятки байт, у каждого объекта заголовок со счётчиком и указателем на тип. Список хранит указатели на объекты, а не сами числа. Отсюда приёмы экономии:
__slots__в классах с миллионами экземпляров.array.arrayи numpy для однородных чисел: значения лежат подряд без объектов.- Генераторы вместо списков для потоковой обработки.
- Кортежи вместо списков для неизменяемых записей.
sys.getsizeof показывает только сам объект, без того, на что он ссылается.
Как искать узкие места
- Измерить, а не гадать.
cProfile(python -m cProfile -s cumtime app.py) показывает, где тратится время по функциям.py-spyснимает профиль работающего процесса без остановки.timeitдля микробенчмарков. - Найти алгоритмическую проблему. Чаще всего тормозит
x in listв цикле, повторные запросы к БД в цикле (N+1), конкатенация строк в цикле, лишние копии. - Только потом микрооптимизации.
Что ускоряет Python-код
- Правильная структура данных:
setиdictдля поиска,dequeдля очереди,heapqдля топ-k. - Встроенные функции и стандартная библиотека написаны на C:
sum,sorted,max,str.join,collections.Counter,itertools. ''.join(parts)вместоs += partв цикле.- Локальные переменные быстрее глобальных; в горячем цикле можно сохранить метод в локальное имя.
- Векторизация через numpy, перенос горячего кода в Cython, Rust (PyO3) или C.
- Кеширование результатов (
functools.cache). - Новые версии CPython: 3.11 дал заметный прирост скорости за счёт специализирующего интерпретатора, в 3.13 появился экспериментальный JIT.
Частые ошибки
- Оптимизация без профилирования.
- Ожидание, что
del xосвободит память, когда на объект есть другие ссылки. - Чтение огромного файла целиком через
read()вместо построчного обхода. - Кеш без ограничения размера в долгоживущем процессе.
Что спросят на собесе
- Как Python управляет памятью: подсчёт ссылок и сборщик циклов.
- Откуда берутся утечки памяти в Python и как их искать.
- Как найти, почему код медленный: какими инструментами.
- Какие приёмы ускорения вы применяли.
- Что дают
__slots__, генераторы,weakref.