Python

Профилирование памяти при разработке на Python

Время прочтения: 4 мин.

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

Немного о сборщике мусора

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

Для подсчета количества ссылок на объект в Python может использоваться функция sys.getrefcount(). Следует учитывать, что функция создает временную ссылку на объект, увеличивая счетчик ссылок на 1.

import sys
my_list = [1, 2, 3]
result = my_list
print('Количество ссылок на объект my_list: ', sys.getrefcount(my_list)) 
# ---> 'Количество ссылок на объект my_list: 3

# Счетчик можно уменьшить вручную, вызвав инструкцию del.
del my_list
print(my_list)

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

Получаем конкретное значение объема потребляемой памяти в коде

  1. Модуль memory_profiler

Модуль позволяет построчно проследить потребление памяти процессором. Модуль psutil ускоряет работу модуля memory_profiler. Модуль прост в использовании: мы декорируем функцию при помощи декоратора @profiler, в качестве результата работы модуля мы видим структурированный отчет.

import copy
from memory_profiler import profile

@profile
def fn_1():
	a = {i: i*i for i in range(10000)}
	b = copy.deepcopy(a)
	return b
fn_1()	

Line #    Mem usage    Increment     Line Contents
============================================================
    22     42.4 MiB     42.4 MiB           	@profile
    23                                        		def qq():
    24     43.4 MiB      1.1 MiB        	a = {i: i * i for i in range(1000)}
    25     43.7 MiB      0.3 MiB           	b = copy.deepcopy(a) 
    26     43.7 MiB      0.0 MiB           	return b

Столбец line – номер строки. Столбец mem usage – Использование памяти после выполнения строки. Столбец increment – прирост по памяти текущей строки относительно последней. Столбец line contents – сам профилируемый код.

from timeit import default_timer
import memory_profiler

def check(fn):
    """
    Декоратор для замера скорости исполнения кода и потребляемой памяти
    (с ипользованием memory_profiler)
    """
    def wrapper():
        memory = memory_profiler.memory_usage()
        start_time = default_timer()
        result = fn()
        finish_time = default_timer()
        memory_2 = memory_profiler.memory_usage()
        print(f'Память: {memory_2[0] - memory[0]}, скорость: {finish_time  - start_time}')
        return result
    return wrapper

2. Функции sys.getsizeof(), pympler.asizeof()

sys.getsizeof() – возвращает размер объекта, но не включает в себя размер элементов  сложного класса, на который ссылается объект. pympler.asizeof() – рекурсивно ищет все вложенные элементы и отображает общий размер файла.

# Numbers (числа)
print(sys.getsizeof(2)) # —> 28
# Tuples (кортежи)
print(sys.getsizeof((2))) # —> 28
# Strings (строки)
print(sys.getsizeof(‘2’)) # —> 50
# Sets (множества)
print(sys.getsizeof(set([2]))) # —> 216
# Lists (списки)
print(sys.getsizeof([2])) # —> 64
# Boolean
print(sys.getsizeof(True)) # —> 28
# Dictionaries (словари)
print(sys.getsizeof({2: 4})) # —> 232

Способы уменьшения расхода памяти

  1. Использование генераторов.
@check
def fn():
    a = []
    for i in range(10000):
        yield a.append(i)
fn() #  ---> Память: 0.00390625, скорость: 6e-06

2. Конструкция __slots__ при определении классов в ООП.

Использование слотов позволяет сохранить атрибуты в менее затратном по памяти контейнере – списке, кортеже. При этом добавить новые атрибуты невозможно.

import sys
from pympler import asizeof

class TestClass:
    __slots__ = ['a', 'b']
    def __init__(self, a, b): 
        self.a = a
        self.b = b
object = TestClass(2, 7)

print(object.__slots__) #  ---> ['a', 'b']
print(asizeof.asizeof(object)) #  ---> 120
print(sys.getsizeof(object)) #  ---> 56

3. Использование NumPy, Pandas для обработки больших данных.

Использование многомерных массивов может занимать много памяти, поэтому стоит рассмотреть возможности NumPy, поскольку пакет специализирован на экономное потребление памяти при обработке больших данных.

import numpy as np 
import sys
from pympler import asizeof

obj = [i for i in range(100000)]
print(asizeoff.asizeoff(obj)) #  ---> 4024456
print(sys.getsizeoff(obj)) #  ---> 824464

obj_num = np.array([i for i in range(100000)])
print(asizeoff.asizeof(obj_num)) #  ---> 400112
print(sys.getsizeof(obj_num)) #  ---> 400096

4. Использовать возможности модуля recordclass.

Модуль похож на namedtuple, кроме одного: он изменяемый. При этом переменные экономнее в потреблении памяти.

import sys
from recordclass import recordclass
from collections import namedtuple

def test():
	test_1 = recordclass('test_1', ['a', 'b', 'c', 'd'])
	test_2 = namedtuple('test_2', ['a', 'b', 'c', 'd'])

	test_rc = test_1(a=1, b=7, c=4, d=9)
	test_nt = test_2(a=1, b=7, c=4, d=9)

	print(sys.getsizeof(test_rc))  #  ---> 48
	print(sys.getsizeof(test_nt)) #  --->  80

test()

5. Использовать возможности специализированных функций, экономящих память, таких как map.

6. Если существует необходимость использовать словари для хранения объектов, то стоит выполнить их сериализацию в json-формат.

import json
import sys
from pympler import asizeof

test_dict = {i: i ** i for i in range(100)}
json_dict = json.dumps(test_dict)

print(asizeof.asizeof(test_dict)) #  ---> 14496
print(sys.getsizeof(test_dict)) #  ---> 4704
print(asizeof.asizeof(json_dict)) #  ---> 9712
print(sys.getsizeof(json_dict)) #  ---> 9710

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

Советуем почитать