← Назад к вопросам

Как обнаружить утечку памяти?

2.0 Middle🔥 81 комментариев
#Python Core

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Обнаружение утечек памяти в Python

Утечка памяти возникает, когда программа удерживает объекты в памяти, которые больше не используются. В Python с автоматической сборкой мусора это сложнее, чем в C++, но возможно.

Как возникают утечки в Python

Основные причины:

# ❌ Циклические ссылки
class Node:
    def __init__(self):
        self.next = None

a = Node()
b = Node()
a.next = b
b.next = a  # Циклическая ссылка
del a, b    # Не очищаются из памяти!

# ❌ Глобальные списки, которые растут
leak_list = []

def add_item():
    leak_list.append([0] * 1000000)  # Растёт бесконечно

# ❌ Замыкания с большими объектами
def create_function(large_data):
    def inner():
        return len(large_data)  # large_data остаётся в памяти
    return inner

big_list = [0] * 1000000
func = create_function(big_list)
del big_list  # Но func всё ещё удерживает ссылку

# ❌ Observer pattern без unsubscribe
class Observable:
    def __init__(self):
        self.observers = []
    
    def subscribe(self, observer):
        self.observers.append(observer)

observable = Observable()
for i in range(1000000):
    observable.subscribe(lambda: print(i))
# observers растёт бесконечно

Способ 1: memory_profiler (простой способ)

Установка:

pip install memory-profiler

Использование:

from memory_profiler import profile

@profile
def leaky_function():
    leak = []
    for i in range(1000):
        leak.append([0] * 10000)  # Растущая утечка
    return sum(len(x) for x in leak)

if __name__ == "__main__":
    leaky_function()

Запуск:

python -m memory_profiler script.py

Вывод:

Line #    Mem usage    Increment  Occurences   Line Contents
================================================
4  37.2 MiB    0.0 MiB        1   @profile
5  37.2 MiB    0.0 MiB        1   def leaky_function():
6  37.2 MiB    0.0 MiB        1       leak = []
7  167.4 MiB  130.2 MiB     1000       for i in range(1000):
8  167.4 MiB    0.0 MiB     1000           leak.append([0] * 10000)

Способ 2: tracemalloc (встроенный)

Встроённый модуль Python:

import tracemalloc

tracemalloc.start()

# Код, который может иметь утечку
leak = []
for i in range(1000):
    leak.append([0] * 10000)

# Снимок памяти
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')

print("[ Top 10 ]")
for stat in top_stats[:10]:
    print(stat)

Вывод:

script.py:7: 130.2 MiB
    leak.append([0] * 10000)

Более детальный анализ:

import tracemalloc

tracemalloc.start()

# Первый снимок
leak = []
for i in range(500):
    leak.append([0] * 10000)

snapshot1 = tracemalloc.take_snapshot()

# Добавляем ещё памяти
for i in range(500):
    leak.append([0] * 10000)

snapshot2 = tracemalloc.take_snapshot()

# Сравниваем разницу
top_stats = snapshot2.compare_to(snapshot1, 'lineno')

for stat in top_stats[:5]:
    print(stat)

Способ 3: objgraph (анализ объектов)

Установка:

pip install objgraph

Основное использование:

import objgraph
import gc

# Основные объекты в памяти
objgraph.show_most_common_types(limit=20)

# Вывод:
# MyClass                     1000
# dict                         500
# list                         300

Отслеживание роста объектов:

import objgraph

objgraph.show_growth()

# Код
leak = []
for i in range(1000):
    leak.append({})

objgraph.show_growth()  # Показывает, что появилось новое

# Вывод:
# dict                       1000      +1000
# list                          1        +1

Отслеживание утечки циклических ссылок:

import objgraph
import gc

class Node:
    def __init__(self):
        self.ref = None

# Создаём циклическую ссылку
a = Node()
b = Node()
a.ref = b
b.ref = a

del a, b
gc.collect()

# Ищем мусор с циклическими ссылками
objgraph.show_most_common_types()

Способ 4: Мониторинг в реальном времени

import psutil
import os

process = psutil.Process(os.getpid())

# Текущее использование памяти
info = process.memory_info()
print(f"RSS: {info.rss / 1024 / 1024:.2f} MB")  # Физическая память
print(f"VMS: {info.vms / 1024 / 1024:.2f} MB")  # Виртуальная память

# Детальная информация
mem_percent = process.memory_percent()
print(f"Memory: {mem_percent:.2f}%")

Мониторинг в цикле:

import psutil
import os
import time

process = psutil.Process(os.getpid())

for i in range(10):
    leak = []
    for j in range(1000):
        leak.append([0] * 10000)
    
    mem = process.memory_info().rss / 1024 / 1024
    print(f"Iteration {i}: {mem:.2f} MB")
    time.sleep(0.1)

Способ 5: Анализ графа объектов

import gc
import sys

class MyClass:
    pass

obj = MyClass()
obj.self_ref = obj  # Циклическая ссылка

# Получаем рефернцы на объект
referrers = gc.get_referrers(obj)
print(f"Object is referenced by {len(referrers)} objects")

for ref in referrers:
    print(f"  - {type(ref)}")

# Получаем объекты, на которые ссылается наш объект
referees = gc.get_referents(obj)
print(f"Object references {len(referees)} objects")

Способ 6: Тестирование утечек

import unittest
import gc
import tracemalloc

class TestMemoryLeak(unittest.TestCase):
    def setUp(self):
        tracemalloc.start()
        self.baseline = tracemalloc.take_snapshot()
    
    def tearDown(self):
        snapshot = tracemalloc.take_snapshot()
        top_stats = snapshot.compare_to(self.baseline, 'lineno')
        
        # Проверяем, что не выросла память больше чем на 1 МБ
        total = sum(stat.size_diff for stat in top_stats)
        self.assertLess(total, 1024 * 1024, f"Memory increased by {total / 1024 / 1024:.2f} MB")
    
    def test_no_leak(self):
        # Это должно быть без утечки
        for i in range(100):
            data = [1, 2, 3, 4, 5]
            del data

Инструменты и пакеты

ИнструментЦельУстановка
memory_profilerПострочный анализpip install memory-profiler
tracemallocВстроенный анализвстроенный
objgraphАнализ объектовpip install objgraph
pymplerГлубокий анализpip install pympler
psutilМониторинг процессаpip install psutil
guppy3Heap анализpip install guppy3

Исправление утечек

Циклические ссылки:

# ❌ Утечка
class Node:
    def __init__(self):
        self.next = None

# ✅ Исправление: используй weakref
import weakref

class Node:
    def __init__(self):
        self.next = None
    
    def set_next(self, node):
        self.next = weakref.ref(node)

Вывод из памяти:

# ✅ Явно удаляй большие объекты
big_data = [0] * 1000000
# работа с big_data
del big_data

# ✅ Закрывай ресурсы
with open("file.txt") as f:
    data = f.read()

# ✅ Отписывайся от событий
observable.unsubscribe(observer)

Лучшие практики

  • Регулярно мониторьте память в production
  • Избегайте циклических ссылок или используйте weakref
  • Тестируйте на утечки в CI/CD
  • Используйте контекстные менеджеры (with) для ресурсов
  • Профилируйте перед оптимизацией
  • Помните о замыканиях — они могут удерживать большие объекты

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