🐍💾 Твой Python-код жрет память? Вот 11 способов это исправить

Невозможно добиться высокой производительности и масштабируемости, если приложение неэффективно использует оперативную память. Расскажем, как это исправить.
🐍💾 Твой Python-код жрет память? Вот 11 способов это исправить

Правильное управление памятью – ключ к созданию эффективного и надежного кода. В распоряжении Python-разработчиков есть встроенные механизмы, сторонние инструменты и кастомные подходы, которые помогают сократить расход памяти.

Объектный пул

Объектный пул
Объектный пул

Объектный пул – стратегия оптимизации памяти и производительности, которая позволяет переиспользовать объекты:

  • Вместо создания нового объекта программа берет его из пула.
  • Новый объект создается только в том случае, если в пуле нет доступных объектов.
  • После использования объект возвращается в пул, а не уничтожается.

Если программа постоянно создает и удаляет ресурсоемкие объекты, то этот подход помогает:

  • Снизить затраты на выделение и освобождение памяти.
  • Уменьшить нагрузку на сборщик мусора.
  • Повысить производительность кода, работающего с затратными объектами в реальном времени.

Вот пример простейшего объектного пула:

        class ObjectPool:
    def __init__(self, create_func):
        self.create_func = create_func  
        self.pool = []  

    def acquire(self):
        """Получаем объект из пула или создаем новый"""
        if self.pool:
            obj = self.pool.pop()
            print("Взяли объект из пула")
            return obj
        print("Создан новый объект")
        return self.create_func()   

    def release(self, obj):
        """Возвращаем объект обратно в пул"""
        self.pool.append(obj)
        print("Объект возвращен в пул")

def create_expensive_object():
    print("Создаем новый ресурсоемкий объект")
    return [0] * 1000000  

pool = ObjectPool(create_expensive_object)
obj1 = pool.acquire()
pool.release(obj1) 
"""Берем объект снова (переиспользуем obj1)"""
obj2 = pool.acquire()


    

Результат:

        Создан новый объект
Создаем новый ресурсоемкий объект
Объект возвращен в пул
Взяли объект из пула
    

Слабые ссылки

Слабые ссылки
Слабые ссылки

В Python используется сборщик мусора, который удаляет объекты, когда на них не осталось ссылок. Но иногда нам нужно создать ссылку на объект, не мешая его автоматическому удалению. Слабые ссылки позволяют ссылаться на объект без увеличения его счетчика ссылок, и тем самым помогают избежать избыточного потребления памяти – объекты будут автоматически удаляться, когда они больше не нужны. Это полезно:

  • Для кэшей – чтобы объекты автоматически удалялись, когда они больше не нужны.
  • Для избежания циклических ссылок – если объект A ссылается на B, а B на A, обычный сборщик мусора может их не удалить.

Пример использования слабых ссылок:

        import weakref

class ExpensiveObject:
    def __init__(self, value):
        self.value = value

def on_delete(ref):
    print("Объект удален")

obj = ExpensiveObject(555_555_555)
weak_ref = weakref.ref(obj, on_delete) 
del obj
print(weak_ref())  

    

Результат:

        Объект удален
None
    

Слоты

Слоты
Слоты

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

Если мы заранее знаем, какие атрибуты будут у объекта, мы можем использовать __slots__, чтобы экономить память. Это позволяет Python использовать массив фиксированного размера вместо словаря. В приведенном ниже примере в RegularClass все объекты хранят атрибуты в словаре, что делает их гибкими, но затратными по памяти, а SlottedClass вместо словаря использует компактный массив для снижения потребления памяти:

        class RegularClass:
    def __init__(self, x, y):
        self.x = x
        self.y = y

class SlottedClass:
    __slots__ = ['x', 'y']
    def __init__(self, x, y):
        self.x = x
        self.y = y

import sys

regular = RegularClass(1, 2)
slotted = SlottedClass(1, 2)
"""Разница зависит от платформы и версии Python"""
print(sys.getsizeof(regular))  
print(sys.getsizeof(slotted))  
    

Результат:

        56
48
    

Отображение файла в память

Отображение файла в память
Отображение файла в память

Загрузка очень больших файлов в память может сделать работу программы неэффективной или даже невозможной. Решить эту проблему помогает техника отображения файлов в память – она позволяет работать с файлом, как с обычным массивом, без необходимости загружать содержимое полностью. В результате можно быстро читать и записывать данные в файл, обрабатывая только нужные фрагменты. Python предоставляет модуль mmap, который позволяет отобразить файл в память и работать с ним, как с байтовым массивом:

        import mmap

with open('large_file.bin', 'rb') as f:
    mm = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)  
    # Читаем 100 байт, начиная с позиции 1000
    data = mm[1000:1100]
    mm.close()  # Закрываем отображение

    

Модуль mmap особенно полезен:

  • При работе с огромными файлами, которые не помещаются в оперативную память.
  • Для быстрого случайного доступа к данным в файле (например, обработка больших логов или бинарных данных).
  • При многократном чтении частей файла – mmap позволяет обращаться к файлу, как к массиву, без лишних дисковых операций.

Точное определение объема памяти, используемого объектами

Точное определение объема памяти, используемого объектами
Точное определение объема памяти, используемого объектами

Если программа использует слишком много памяти, важно выяснить, какие именно объекты в этом задействованы. Метод sys.getsizeof() позволяет узнать размер объекта, но не учитывает вложенные структуры. Например, у списка sys.getsizeof(my_list) покажет только его контейнер, но не элементы внутри. Для детального анализа потребления памяти лучше использовать более продвинутые инструменты, например, memory_profiler. Этот инструмент позволяет измерять использование памяти построчно, выявляя проблемные места в коде:

        from memory_profiler import profile

@profile
def memory_hungry_function():
    list_of_lists = [[i] * 1000 for i in range(1000)]
    return sum(sum(sublist) for sublist in list_of_lists)

memory_hungry_function()
    

Результат:

        Line #    Mem usage    Increment  Occurrences   Line Contents
=============================================================
     3     25.7 MiB     25.7 MiB           1   @profile
     4                                         def memory_hungry_function():
     5     33.4 MiB      7.7 MiB        1003       list_of_lists = [[i] * 1000 for i in range(1000)]  
     6     33.4 MiB      0.0 MiB        2003       return sum(sum(sublist) for sublist in list_of_lists)  


    
🐍 Библиотека питониста
Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека питониста»
🐍🎓 Библиотека Python для собеса
Подтянуть свои знания по Python вы можете на нашем телеграм-канале «Библиотека Python для собеса»
🐍🧩 Библиотека задач по Python
Интересные задачи по Python для практики можно найти на нашем телеграм-канале «Библиотека задач по Python»

Эффективная обработка больших коллекций

Эффективная обработка больших коллекций
Эффективная обработка больших коллекций

Обычный список загружает все данные сразу, и если файл очень большой, он может занять слишком много памяти:

        def load_all_lines(filename):
    with open(filename, 'r') as f:
        return [process_line(line) for line in f]  

    

Генератор и yield решают эту проблему – файл можно читать построчно, не загружая все содержимое целиком:

        def process_large_dataset(filename):
    with open(filename, 'r') as f:
        for line in f:
            yield process_line(line)

for result in process_large_dataset('large_file.txt'):
    print(result)
    

Генераторы стоит использовать при:

  • Обработке больших файлов (логов, CSV, JSON).
  • Работе с потоками данных (веб-скрапинг, работа с API).
  • При генерации последовательностей (например, бесконечные ряды чисел).

Например, так можно реализовать пословную обработку объемного текстового файла:

        def word_count_generator(file_path):
    with open(file_path, 'r') as file:
        for line in file:
            for word in line.split():
                yield word  

word_counts = {}
for word in word_count_generator('large_text_file.txt'):
    word_counts[word] = word_counts.get(word, 0) + 1

print(word_counts)

    

Кастомные структуры данных

Кастомные структуры данных
Кастомные структуры данных

При работе с очень большими данными, обычный список list может занять слишком много памяти. В таких случаях можно хранить часть данных на диске, а не в оперативной памяти. С этой целью можно создать кастомный список, который автоматически сохраняет данные на диск, если он становится слишком большим. В приведенном ниже примере DiskBackedList – это гибрид списка и файла, который помогает работать с огромными данными без переполнения памяти:

  • Хранит данные в оперативной памяти self.memory_list.
  • Если элементов становится слишком много max_memory_items, он сбрасывает их в файл.
  • Позволяет перебирать элементы, загружая их с диска при необходимости.
        import pickle

class DiskBackedList:
    def __init__(self, max_memory_items=1000):
        self.max_memory_items = max_memory_items  # Лимит хранения в памяти
        self.memory_list = []  
        self.disk_file = 'temp_list.pkl'  

    def append(self, item):
        """Добавляет элемент в список, выгружая на диск при превышении лимита"""
        self.memory_list.append(item)
        if len(self.memory_list) >= self.max_memory_items:
            self._write_to_disk()

    def _write_to_disk(self):
        """Записывает список на диск и очищает память"""
        with open(self.disk_file, 'ab') as f:  # 'ab' = добавить в файл (binary)
            pickle.dump(self.memory_list, f)
        self.memory_list.clear()  # Очищаем список из памяти

    def __iter__(self):
        """Итерируем сначала по памяти, затем по файлу"""
        yield from self.memory_list
        with open(self.disk_file, 'rb') as f:
            while True:
                try:
                    yield from pickle.load(f)  # Читаем сохраненные данные
                except EOFError:
                    break  # Достигли конца файла

    def __del__(self):
        """Удаляем временный файл при удалении объекта"""
        import os
        if os.path.exists(self.disk_file):
            os.remove(self.disk_file)

    

Эффективная работа с большими массивами NumPy

Эффективная работа с большими массивами NumPy
Эффективная работа с большими массивами NumPy

Аналитикам и дата-сайентистам нередко случается работать с массивами, занимающими солидную часть оперативной памяти. Решение – использовать np.memmap: он позволяет работать с массивом, как будто он находится в памяти (а на самом деле хранится на диске). С помощью этого метода можно работать с массивами, размер которых превышает объем оперативной памяти:

        import numpy as np

# Создаем отображение массива в памяти
shape = (10000, 10000)  
mm_array = np.memmap('mm_array.dat', dtype='float32', mode='w+', shape=shape)

# Используем массив, как если бы он был в памяти
mm_array[0, 0] = 1.0
mm_array[9999, 9999] = 100.0

# Изменения автоматически записываются на диск
del mm_array  # Закрываем файл

    

Кастомный кэш с автоматическим удалением устаревших данных

Кастомный кэш с автоматическим удалением устаревших данных
Кастомный кэш с автоматическим удалением устаревших данных

В долго работающих серверных приложениях хранение временных данных в памяти значительно ускоряет работу и уменьшает нагрузку на базу данных. Чтобы не столкнуться с утечкой памяти, стоит реализовать кастомный кэш с ограниченным временем жизни данных. В приведенном ниже примере TimedCache:

  • Хранит данные в словаре, чтобы обеспечивать быстрый доступ.
  • Запоминает время добавления каждого объекта.
  • Удаляет устаревшие объекты при каждом обращении.
        import time

class TimedCache:
    def __init__(self, expiration_time):
        self.cache = {}
        self.expiration_time = expiration_time

    def get(self, key):
        if key in self.cache:
            value, timestamp = self.cache[key]
            if time.time() - timestamp < self.expiration_time:
                return value
            else:
                del self.cache[key]
        return None

    def set(self, key, value):
        self.cache[key] = (value, time.time())

    def clean(self):
        current_time = time.time()
        self.cache = {k: v for k, v in self.cache.items() 
                      if current_time - v[1] < self.expiration_time}

# Usage
cache = TimedCache(expiration_time=60)  # Время жизни кэша
cache.set('user_1', {'name': 'Alice', 'age': 30})
print(cache.get('user_1'))  
time.sleep(61) # Данные устаревают
print(cache.get('user_1'))  

    

Результат:

        {'name': 'Alice', 'age': 30}
None
    

Использование контекстных менеджеров для управления временными объектами

Использование контекстных менеджеров для управления временными объектами
Использование контекстных менеджеров для управления временными объектами

Когда приложение создает много временных объектов, важно освобождать память после их использования. Контекстные менеджеры with помогают гарантированно очищать ресурсы, даже если происходит ошибка

        class TempResource:
    def __init__(self):
        self.data = [0] * 1000000  

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        del self.data  

with TempResource() as resource:
    # Используем объект
    pass  # После выхода из блока self.data удалится автоматически

    

Такой подход особенно полезен при работе с файлами, соединениями, временными объектами и другими ресурсами, которые нужно своевременно очищать, чтобы не возникали утечки памяти.

Обработка больших данных в pandas

Обработка больших данных в pandas
Обработка больших данных в pandas

При работе с большими таблицами, состоящими из миллионов строк, неэффективно загружать весь файл в память. Pandas позволяет обрабатывать данные порциями (чанками), чтобы снизить нагрузку на память:

        import pandas as pd

def process_large_csv(file_path, chunk_size=10000):
    for chunk in pd.read_csv(file_path, chunksize=chunk_size):
        # Process each chunk
        processed_chunk = chunk.apply(some_processing_function)
        yield processed_chunk

for processed_data in process_large_csv('large_dataset.csv'):
    # Обработка данных
    print(processed_data.head())
    

Этот подход идеален для работы с файлами размером в десятки гигабайт:

  • Загружается только одна часть данных за раз.
  • Можно выполнять любые операции с чанками по мере загрузки.
  • Можно использовать параллельную обработку для ускорения процесса.

В заключение

Эффективное управление памятью — это сочетание встроенных функций, внешних инструментов и оптимизаций. Если правильно подобрать технику под конкретную задачу, можно значительно ускорить работу программы и уменьшить потребление ресурсов.

***

При подготовке статьи использовалась публикация 6 Powerful Python Techniques for Efficient Memory Management.

Публикации по теме:

Комментарии

 
 

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

LIVE >

Подпишись

на push-уведомления