Время прочтения: 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, задачей которого является удаление циклических ссылок на объекты.
Получаем конкретное значение объема потребляемой памяти в коде
- Модуль 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 |
Способы уменьшения расхода памяти
- Использование генераторов.
@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 имеет механизм для контроля потребляемой памяти, как правило, вмешательство программиста в этот процесс не требуется, тем не менее, важно понимать аспекты по уменьшению затрат памяти.