Измерение памяти списка Python: как узнать размер и оптимизировать использование

0
25

Краткая памятка по измерению памяти списков

  1. Используйте sys.getsizeof() для быстрой оценки базового размера списка.
  2. Для полного размера с элементами применяйте deep_getsizeof().
  3. Помните, что sys.getsizeof() не учитывает вложенные объекты.
  4. Для однотипных данных используйте array.array или NumPy.
  5. Избегайте хранения больших данных в списках — используйте генераторы.
  6. Применяйте Memory Profiler для поиска утечек памяти.
  7. Следите за счетчиком ссылок и сборщиком мусора.
  8. Учитывайте накладные расходы на маленькие объекты.
  9. Используйте tracemalloc для детального анализа.
  10. Оптимизируйте структуры данных: кортежи легче списков, словари тяжелее.
  11. Проверяйте память до и после удаления больших списков.
  12. Используйте __slots__ в пользовательских классах для экономии памяти.

Практическое исследование использования памяти Python

Как определить общий объем памяти - изображение номер один
Как определить общий объем памяти — изображение номер один

Сначала давайте немного разберемся и получим конкретное представление о фактическом использовании памяти объектами Python.

Встроенная функция ()

12 - изображение номер два
12 — изображение номер два

Модуль sys стандартной библиотеки предоставляет функцию getsizeof (). Эта функция принимает объект (и необязательный параметр по умолчанию), вызывает метод sizeof () объекта и возвращает результат, поэтому вы также можете сделать ваши объекты инспектируемыми.

Измерение памяти объектов Python

Python - изображение номер три
Python — изображение номер три

Вау. 80 байт! Это действительно заставляет задуматься о том, хотите ли вы представлять большое количество вещественных чисел как числа с плавающей запятой или десятичные дроби.

OK. Пустая строка занимает 37 байтов, и каждый дополнительный символ добавляет еще один байт. Это многое говорит о компромиссе между сохранением нескольких коротких строк, когда вы будете платить 37 байтов за каждую, и одной длинной строкой, где вы платите только один раз.

Строки Unicode ведут себя аналогично, за исключением того, что служебные данные составляют 50 байтов, и каждый дополнительный символ добавляет 2 байта. Это нужно учитывать, если вы используете библиотеки, которые возвращают строки Unicode, но ваш текст может быть представлен в виде простых строк.

История похожа на кортежи. Накладные расходы пустого кортежа составляют 56 байтов против 72 списка. Опять же, эта разница в 16 байтов на последовательность — это низко висящий плод, если у вас есть структура данных с большим количеством небольших неизменяемых последовательностей.

Hands-On Exploration of Python Memory Usage

First, let’s explore a little bit and get a concrete sense of the actual memory usage of Python objects.

The () Built-in Function

Посчитать количество элементов в списке - изображение номер пять
Посчитать количество элементов в списке — изображение номер пять

The standard library’s sys module provides the getsizeof() function. That function accepts an object (and optional default), calls the object’s sizeof() method, and returns the result, so you can make your objects inspectable as well.

Measuring the Memory of Python Objects

Variable's memory size in - изображение номер шесть
Variable's memory size in — изображение номер шесть

Таблица №1

1
import sys
2

3
sys.getsizeof(5)
4
28

Таблица №2

1
sys.getsizeof(5.3)
2
24

Таблица №3

1
from decimal import Decimal
2
sys.getsizeof(Decimal(5.3))
3
104

Таблица №4

1
sys.getsizeof('')
2
49
3
sys.getsizeof('1')
4
50
5
sys.getsizeof('12')
6
51
7
sys.getsizeof('123')
8
52
9
sys.getsizeof('1234')
10
53

OK. An empty string takes 49 bytes, and each additional character adds another byte. That says a lot about the tradeoffs of keeping multiple short strings where you’ll pay the 49 bytes overhead for each one vs. a single long string where you pay the overhead only once.

Таблица №5

1
(bytes())
2
33

Таблица №6

1
sys.getsizeof([])
2
56
3
sys.getsizeof([1])
4
64
5
sys.getsizeof([1, 2])
6
72
7
sys.getsizeof([1, 2,3])
8
80
9
sys.getsizeof([1, 2, 3, 4])
10
88
11

12
sys.getsizeof(['a long longlong string'])
13
64

The answer is simple. The list doesn’t contain the int objects themselves. It just contains an 8-byte (on 64-bit versions of CPython) pointer to the actual int object. What that means is that the getsizeof() function doesn’t return the actual memory of the list and all the objects it contains, but only the memory of the list and the pointers to its objects. In the next section I’ll introduce the deep\_getsizeof() function, which addresses this issue.

Таблица №7

1
sys.getsizeof(())
2
40
3
sys.getsizeof((1,))
4
48
5
sys.getsizeof((1,2,))
6
56
7
sys.getsizeof((1,2,3,))
8
64
9
sys.getsizeof((1, 2, 3, 4))
10
72
11
sys.getsizeof(('a long longlong string',))
12
48

The story is similar for tuples. The overhead of an empty tuple is 40 bytes vs. the 56 of a list. Again, this 16 bytes difference per sequence is low-hanging fruit if you have a data structure with a lot of small, immutable sequences.

Таблица №8

1
sys.getsizeof(set())
2
216
3
sys.getsizeof(set([1))
4
216
5
sys.getsizeof(set([1, 2, 3, 4]))
6
216
7

8
sys.getsizeof({})
9
64
10
sys.getsizeof(dict(a=1))
11
232
12
sys.getsizeof(dict(a=1, b=2, c=3))
13
232

The bottom line is that Python objects have a huge fixed overhead. If your data structure is composed of a large number of collection objects like strings, lists and dictionaries that contain a small number of items each, you pay a heavy toll.

Основные способы определения размера объектов в Python

4 способа найти размер списка в - изображение номер семь
4 способа найти размер списка в — изображение номер семь

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

  • Встроенная функция () — базовый инструмент для прямого измерения
  • Библиотека pympler — расширенный анализ, включая вложенные структуры
  • Модуль objsize — удобное рекурсивное измерение объектов
  • Ручное отслеживание с использованием tracemalloc — для сложных случаев
  • Профилировщики памяти — memory_profiler и guppy для детального анализа

Выбор метода зависит от специфики задачи и глубины анализа, который вам требуется. Давайте сравним эти методы по основным характеристикам:

Таблица №9

Метод Точность Простота использования Подходит для рекурсивных структур Встроен в Python
() Средняя Высокая Нет Да
pympler Высокая Средняя Да Нет
objsize Высокая Средняя Да Нет
tracemalloc Очень высокая Низкая Да Да (с Python 3.4+)
memory_profiler Высокая Средняя Да Нет

import sys integer = 42 string = «Hello, Python!» empty_list = [] filled_list = [1, 2, 3, 4, 5] print(f»Целое число: {(integer)} байт») print(f»Строка: {(string)} байт») print(f»Пустой список: {(empty_list)} байт») print(f»Список с элементами: {(filled_list)} байт»)

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

Программирование (Python) - презентация онлайн - изображение номер восемь
Программирование (Python) — презентация онлайн — изображение номер восемь

Функция (): возможности и ограничения

Урок 11 - изображение номер девять
Урок 11 — изображение номер девять

Функция () — самый доступный и простой способ определения размера объекта в Python. Она возвращает размер объекта в байтах. Однако у этого метода есть серьезные ограничения, о которых необходимо знать каждому разработчику. 🧩

import sys int_val = 42 print(f»Целое число: {(int_val)} байт») str_short = «a» str_long = «a» * 1000 print(f»Короткая строка: {(str_short)} байт») print(f»Длинная строка: {(str_long)} байт») empty_list = [] list_with_ints = [1, 2, 3, 4, 5] list_with_strings = [«hello», «world», «python»] print(f»Пустой список: {(empty_list)} байт») print(f»Список с числами: {(list_with_ints)} байт») print(f»Список со строками: {(list_with_strings)} байт»)

  • Поверхностное измерение: функция возвращает только размер самого объекта без учета размера содержимого (для контейнеров)
  • Игнорирование ссылок: не учитывает память, занимаемую объектами, на которые ссылается измеряемый объект
  • Отсутствие рекурсии: не проходит автоматически по вложенным структурам данных
  • Служебные данные: включает в измерение служебную информацию объекта (reference count, type pointer и т.д.)
  • Реализационно-зависимые результаты: значения могут отличаться в разных версиях Python и реализациях (CPython vs PyPy)

Для получения более точных результатов при работе со сложными структурами данных необходимо использовать рекурсивный подход или специализированные библиотеки, такие как pympler. Ниже мы подробно разберем эти методы. 📊

Измерение памяти сложных структур данных и коллекций

Окончательно разбираем списки в питоне / - изображение номер десять
Окончательно разбираем списки в питоне / — изображение номер десять

Когда дело доходит до измерения памяти сложных структур данных в Python, простой вызов () не дает полной картины. Для списков, словарей, множеств и других контейнеров требуется учитывать не только размер самого контейнера, но и всех его элементов. 🧮

  1. Использование специализированных библиотек
  2. Написание собственных рекурсивных функций
  3. Комбинированный подход с учетом специфики структур данных

Рассмотрим использование популярной библиотеки pympler, которая значительно упрощает измерение «полного» размера объектов:

from pympler import asizeof import sys complex_data = { ‘users’: [{‘name’: ‘Alice’, ‘tags’: [‘developer’, ‘python’, ‘data science’]}, {‘name’: ‘Bob’, ‘tags’: [‘designer’, ‘ui/ux’, ‘web’]}], ‘statistics’: { ‘views’: [1024, 2048, 4096, 8192], ‘conversions’: [128, 256, 512] } } print(f»: {(complex_data)} байт») print(f»: {(complex_data)} байт»)

Разница будет существенной! Библиотека pympler рекурсивно обходит все вложенные объекты и учитывает их размер, давая более точное представление о реальном потреблении памяти.

Для различных типов коллекций характерны свои особенности использования памяти:

Таблица №10

Структура данных Базовый накладной расход Масштабирование Особенности
list ~64 байт (пустой) Линейное + избыточная аллокация Overallocation для оптимизации добавления
dict ~232 байт (пустой) Нелинейное (хеш-таблица) Sparse array + ~1/3 пустых слотов
set ~224 байт (пустой) Нелинейное (хеш-таблица) Аналогично dict, но без значений
tuple ~40 байт (пустой) Линейное Фиксированный размер, нет overallocation
str ~49 байт (пустая) Линейное Для ASCII: 1 байт/символ, Unicode: до 4 байт/символ

Интересная особенность: словари и множества в Python используют хеш-таблицы, что приводит к дополнительным накладным расходам на память, но обеспечивает быстрый доступ O(1). При этом Python резервирует дополнительное пространство для эффективного добавления новых элементов.

from pympler import asizeof sizes = {} for i in range(10): d = {j: j for j in range(i)} sizes[i] = (d) for i in range(1, 10): increase = sizes[i] – sizes[i-1] print(f»Добавление {i}-го элемента увеличило размер на {increase} байт»)

Вы заметите, что рост размера словаря не всегда линейный — периодически происходят «скачки», когда внутренняя хеш-таблица реорганизуется для размещения новых элементов.

При работе со сложными структурами данных важно также учитывать совместное использование объектов (object sharing). Python может использовать один и тот же объект в разных местах, что экономит память. Чтобы учесть этот факт при измерении, pympler предлагает функцию (), которая предоставляет более подробную информацию. 📝

Рекурсивный подсчёт памяти для вложенных объектов

Обработка массивов (Python) - изображение номер одиннадцать
Обработка массивов (Python) — изображение номер одиннадцать

Я работала с ML-системой обработки естественного языка, где модели генерировали сложные деревья синтаксического разбора. После нескольких часов работы приложение начинало потреблять гигабайты RAM и в конце концов падало. Стандартные профилировщики не выявляли причину. Решение пришло, когда я написала собственную рекурсивную функцию для анализа размера наших синтаксических деревьев. Оказалось, что некоторые деревья хранили огромные дубликаты текстовых данных на каждом уровне вложенности. После трех дней отладки, функция показала, что 87% памяти занимали повторяющиеся метаданные. Изменив структуру хранения на более эффективную с общими ссылками, мы уменьшили потребление памяти в 4.5 раза. Самое удивительное, что базовые методы типа () не показывали проблему, поскольку не учитывали глубину вложенности наших структур.

Когда стандартная функция () не справляется с задачей, а установка внешних библиотек нежелательна, на помощь приходят рекурсивные алгоритмы подсчета памяти. Они позволяют «погрузиться» в сложные структуры данных и просуммировать размер всех вложенных объектов. 🌲

Вот пример такой рекурсивной функции для подсчета полного размера объекта:

import sys import types from import Mapping, Container def get_size_recursive(obj, seen=None): «»»Рекурсивно вычисляет размер объекта в байтах.»»» size = (obj) if seen is None: seen = set() obj_id = id(obj) if obj_id in seen: return 0 (obj_id) if isinstance(obj, str) or isinstance(obj, bytes): pass elif isinstance(obj, Mapping): size += sum(get_size_recursive(k, seen) + get_size_recursive(v, seen) for k, v in ()) elif isinstance(obj, Container) and not isinstance(obj, (str, bytes,)): size += sum(get_size_recursive(x, seen) for x in obj) return size

Эта функция учитывает циклические ссылки, обрабатывает разные типы контейнеров и позволяет получить более точную оценку реального потребления памяти. Давайте протестируем её на разных структурах данных:

if __name__ == «__main__»: simple_list = [1, 2, 3, 4, 5] nested_dict = { «a»: [1, 2, 3], «b»: {«x»: 1, «y»: 2}, «c»: «hello world» * 100 } shared_obj = [«shared data» * 1000] obj_with_shared = [shared_obj, shared_obj, shared_obj] cycle_list = [] cycle_list.append(cycle_list) print(f»Размер простого списка: {get_size_recursive(simple_list)} байт») print(f»Размер вложенного словаря: {get_size_recursive(nested_dict)} байт») print(f»Размер с общими объектами: {get_size_recursive(obj_with_shared)} байт») print(f»Размер цикличной структуры: {get_size_recursive(cycle_list)} байт»)

ЧИТАТЬ ТАКЖЕ:  Как перевести число из двоичной системы счисления в восьмеричную с помощью Python

При реализации рекурсивного подсчета памяти важно учитывать следующие нюансы:

  • Циклические ссылки: необходимо отслеживать уже посещенные объекты, чтобы избежать бесконечной рекурсии
  • Разделяемые объекты: учитывать объекты только один раз, даже если на них ссылаются из разных мест
  • Специфические типы: некоторые типы (модули, классы, функции) требуют особой обработки
  • Глубина рекурсии: при работе с очень сложными структурами может потребоваться ограничение глубины
  • Производительность: рекурсивный обход может быть медленным для очень больших структур данных

Для повышения точности можно дополнительно учитывать специфику различных типов данных:

def get_detailed_size(obj, seen=None): «»»Улучшенная версия с учетом специфики типов.»»» if seen is None: seen = set() obj_id = id(obj) if obj_id in seen: return 0 size = (obj) (obj_id) if isinstance(obj, dict): size += sum(get_detailed_size(k, seen) + get_detailed_size(v, seen) for k, v in ()) elif isinstance(obj, (list, tuple, set, frozenset)): size += sum(get_detailed_size(i, seen) for i in obj) elif hasattr(obj, ‘__dict__’): size += get_detailed_size(obj.__dict__, seen) elif hasattr(obj, ‘__slots__’): size += sum(get_detailed_size(getattr(obj, s), seen) for s in obj.__slots__ if hasattr(obj, s)) return size

Рекурсивный подход особенно полезен для анализа собственных классов и структур данных. Он позволяет выявить «скрытые» утечки памяти и оптимизировать хранение информации. Особенно это актуально при работе с большими объемами данных или в системах с ограниченными ресурсами. 🔎

Функция deep_getsizeof ()

Публикация #1722 - изображение номер двенадцать
Публикация #1722 — изображение номер двенадцать

Теперь, когда я напугал вас до полусмерти и продемонстрировал, что () может только сказать вам, сколько памяти занимает примитивный объект, давайте посмотрим на более адекватное решение. Функция deep_getsizeof () рекурсивно выполняет детализацию и вычисляет фактическое использование памяти графом объектов Python.

«`python из импорта коллекций Mapping, контейнер из sys import getsizeof

У этой функции есть несколько интересных аспектов. Он учитывает объекты, на которые ссылаются несколько раз, и учитывает их только один раз, отслеживая идентификаторы объектов. Другая интересная особенность реализации заключается в том, что она в полной мере использует абстрактные базовые классы модуля коллекций. Это позволяет функции очень лаконично обрабатывать любую коллекцию, которая реализует базовые классы Mapping или Container, вместо непосредственного обращения к множеству типов коллекций, таких как: строка, Unicode, байты, список, кортеж, dict, frozendict, OrderedDict, set, frozenset и т. Д.,

Строка длиной 7 занимает 44 байта (37 служебных данных + 7 байтов для каждого символа).

Последний пример показывает, что deep_getsizeof () подсчитывает ссылки на один и тот же объект (строку x) только один раз, но подсчитывается указатель каждой ссылки.

The deep\_getsizeof() Function

Why is the memory usage of a - изображение номер тринадцать
Why is the memory usage of a — изображение номер тринадцать

Now that I’ve scared you half to death and also demonstrated that () can only tell you how much memory a primitive object takes, let’s take a look at a more adequate solution. The deep\_getsizeof() function drills down recursively and calculates the actual memory usage of a Python object graph.

Таблица №11

1
from  import Mapping, Container
2
from sys import getsizeof
3

4
def deep\_getsizeof(o, ids):
5
 """Find the memory footprint of a Python object
6

7
 This is a recursive function that drills down a Python object graph
8
 like a dictionary holding nested dictionaries with lists of lists
9
 and tuples and sets.
10

11
 The function does a shallow size of only. It counts each
12
 object inside a container as pointer only regardless of how big it
13
 really is.
14

15
:param o: the object
16
:param ids:
17
:return:
18
 """
19
 d = deep\_getsizeof
20
 if id(o) in ids:
21
 return 0
22

23
 r = getsizeof(o)
24
 ids.add(id(o))
25

26
 if isinstance(o, str) or isinstance(0, str):
27
 return r
28

29
 if isinstance(o, Mapping):
30
 return r + sum(d(k, ids) + d(v, ids) for k, v in o.iteritems())
31

32
 if isinstance(o, Container):
33
 return r + sum(d(x, ids) for x in o)
34

35
 return r 

There are several interesting aspects to this function. It takes into account objects that are referenced multiple times and counts them only once by keeping track of object ids. The other interesting feature of the implementation is that it takes full advantage of the collections module’s abstract base classes. That allows the function very concisely to handle any collection that implements either the Mapping or Container base classes instead of dealing directly with myriad collection types like: string, Unicode, bytes, list, tuple, dict, frozendict, OrderedDict, set, frozenset, etc.

Таблица №12

1
x = '1234567'
2
deep\_getsizeof(x, set())
3
56

Таблица №13

1
deep\_getsizeof([], set())
2
56

Таблица №14

1
deep\_getsizeof([x], set())
2
120

Таблица №15

1
deep\_getsizeof([x, x, x, x, x], set())
2
152

Reference Counting

Memory - изображение номер четырнадцать
Memory — изображение номер четырнадцать

Python manages memory using reference counting semantics. Once an object is not referenced anymore, its memory is deallocated. But as long as there is a reference, the object will not be deallocated. Things like cyclical references can bite you pretty hard.

Small Objects

Find - изображение номер пятнадцать
Find — изображение номер пятнадцать

CPython manages small objects (less than 256 bytes) in special pools on 8-byte boundaries. There are pools for 1-8 bytes, 9-16 bytes, and all the way to 249-256 bytes. When an object of size 10 is allocated, it is allocated from the 16-byte pool for objects 9-16 bytes in size. So, even though it contains only 10 bytes of data, it will cost 16 bytes of memory. If you allocate 1,000,000 objects of size 10, you actually use 16,000,000 bytes and not 10,000,000 bytes as you may assume. This 60% extra overhead is obviously not trivial.

Integers

Как проверить число на натуральность в - изображение номер шестнадцать
Как проверить число на натуральность в — изображение номер шестнадцать

CPython keeps a global list of all the integers in the range -5 to 256. This optimization strategy makes sense because small integers pop up all over the place, and given that each integer takes 28 bytes, it saves a lot of memory for a typical program.

It also means that CPython pre-allocates 266 * 28 = 7448 bytes for all these integers, even if you don’t use most of them. You can verify it by using the id() function that gives the pointer to the actual object. If you call id(x) for any x in the range -5 to 256, you will get the same result every time (for the same integer). But if you try it for integers outside this range, each one will be different (a new object is created on the fly every time).

Таблица №16

1
id(-3)
2
9788832
3

4
id(-3)
5
9788832 
6

7
id(-3)
8
9788832 
9

10
id(201)
11
9795360 
12

13
id(201)
14
9795360 
15

16
id(201)
17
9795360 

Таблица №17

1
id(257)
2
140276939034224 
3

4
id(301)
5
140276963839696 
6

7
id(301)
8
140276963839696 
9

10
id(-6)
11
140276963839696 
12

13
id(-6)
14
140276963839696 

Python Memory vs. System Memory

How to - изображение номер семнадцать
How to — изображение номер семнадцать

CPython is kind of possessive. In many cases, when memory objects in your program are not referenced anymore, they are not returned to the system (e.g. the small objects). This is good for your program if you allocate and deallocate many objects that belong to the same 8-byte pool because Python doesn’t have to bother the system, which is relatively expensive. But it’s not so great if your program normally uses X bytes and under some temporary condition it uses 100 times as much (e.g. parsing and processing a big configuration file only when it starts).

Now, that 100X memory may be trapped uselessly in your program, never to be used again and denying the system from allocating it to other programs. The irony is that if you use the processing module to run multiple instances of your program, you’ll severely limit the number of instances you can run on a given machine.

Подсчет ссылок

Одномерные массивы - презентация онлайн - изображение номер восемнадцать
Одномерные массивы — презентация онлайн — изображение номер восемнадцать

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

Счетчик ссылок

Как подсчитать количество - изображение номер девятнадцать
Как подсчитать количество — изображение номер девятнадцать

Каждый созданный объект имеет специальное поле — счетчик ссылок. Он хранит в себе количество ссылающихся на него объектов. Увеличивает свое значение, например, когда используется операция присваивания, или когда объект становится частью списка. При удалении переменной или же при использовании del счетчик ссылок уменьшается на 1. Например, при завершении работы функции, где эта переменная была объявлена.

В данном случае 1000 — это неизменяемый объект, один на всю программу. После инициализации двух переменных счетчик ссылок равен 5.

Почему 5? Потому что на объект 1000 ссылаются не только эти две переменные, а все переменные со значением 1000 во всех используемых модулях. Далее удаляем переменную b и счетчик ссылок меняет свое значение на 4. Как только счетчик достигает 0, объект освобождает блок.

Сборщик мусора

Сборка мусора (garbage collection) в - изображение номер двадцать
Сборка мусора (garbage collection) в — изображение номер двадцать

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

Специально для обработки таких случаев был создан модуль gc. Его работа заключается в том, чтобы периодически сканировать объекты контейнерного типа и определять наличие циклических ссылок.

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

Сканирование первого поколения начинается, когда количество созданных объектов контейнерного типа превышает количество удаленных на заданный порог. Например, сканирование второго поколения начнется, когда количество сканирований первого поколения превысит заданный порог. По умолчанию, пороги срабатывания — это 700, 10 и 10, соответственно.

Посмотреть их можно через gc.get_threshold(). Изменить через gc.set_threshold().

Все это актуально для list, dict, tuple и еще для классов. Не для простых типов. Базовый обзор типов данных — в нашем гайде для начинающих.

List

Обучение - изображение номер двадцать один
Обучение — изображение номер двадцать один

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

При обычном создании списка через перечисление элементов он всегда будет занимать: 64 байта пустой список + 8 байт на каждый элемент, т.к. список представляет из себя массив ссылок на объекты.

При создании через list comprehension размер будет уже 96. Что больше, чем размер пустого списка + 8 байт на каждый элемент. Работа этого механизма сводится к вызову метода append у создаваемого объекта списка. append работает следующим образом: в зависимости от уже присутствующего в списке количества элементов он заранее резервирует больше памяти при добавлении элемента. Дополнительно выделяемый объем памяти не всегда увеличивает список вдвое. Может быть выделено место всего под несколько элементов. Например, если к списку из 8-ми элементов добавляют еще один, будет зарезервировано место еще под восемь элементов. А при добавлении к списку из 16-ти элементов будет зарезервировано место всего под 9 элементов. Это позволяет избежать затрат на изменение размера списка при частых вызовах append. При этом неиспользуемая, но уже выделенная, память недоступна для чтения.

При создании списка на основе кортежа получившийся список будет занимать 112 байт. В данном случае заранее резервируется место под еще 3 элемента списка, помимо уже присутствующих в кортеже.

Итого получаем 64 байта, + 8 * 3 — это элементы из кортежа, + 8 * 3 зарезервированное место под новые элементы.

Tuple

#2 - изображение номер двадцать два
#2 — изображение номер двадцать два

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

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

ЧИТАТЬ ТАКЖЕ:  Для чего нужен язык программирования Python: сферы применения, преимущества и кому его знать

На примере видно, что адреса в памяти у двух кортежей совпадают. Аналогичное сравнение для списков вернуло бы False.

В случае непустого кортежа с памятью так же все просто. Объем блока будет равен 48 байтам + по 8 байт на каждый элемент.

С созданием кортежа из списка или другой коллекции тоже нет таких неожиданностей, как со списком. Т.к. не надо закладывать место под изменение количества элементов.

Dict

Day 97 - изображение номер двадцать три
Day 97 — изображение номер двадцать три

Как видно в примере, словарь занимает 240 байт. При создании словаря выделяется место под несколько элементов, а не только после добавления элемента, как это было со списком.

Если вызвать метод clear, очищаются не только все элементы. Изначально зарезервированная память тоже будет освобождена.

В итоге словарь стал занимать всего 72 байта. Гораздо меньше, чем при создании.

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

Целые

Использование памяти в - изображение номер двадцать четыре
Использование памяти в — изображение номер двадцать четыре

CPython хранит глобальный список всех целых чисел в диапазоне [-5, 256]. Эта стратегия оптимизации имеет смысл, потому что маленькие целые числа всплывают повсюду, и, учитывая, что каждое целое число занимает 24 байта, оно экономит много памяти для типичной программы.

Это также означает, что CPython предварительно выделяет 266 * 24 = 6384 байта для всех этих целых чисел, даже если вы не используете большинство из них. Вы можете проверить это с помощью функции id (), которая дает указатель на фактический объект. Если вы называете id (x) несколько для любого x в диапазоне [-5, 256], вы будете каждый раз получать один и тот же результат (для одного и того же целого числа). Но если вы попробуете это для целых чисел вне этого диапазона, каждый из них будет отличаться (новый объект создается на лету каждый раз).

Память Python против системной памяти

Python 使 用 memory_profiler 分 析 程 序 内 存 占 用 情 况 温 欣 爸 比 的 博 客 - изображение номер двадцать пять
Python 使 用 memory_profiler 分 析 程 序 内 存 占 用 情 况 温 欣 爸 比 的 博 客 — изображение номер двадцать пять

CPython является своего рода притяжательным. Во многих случаях, когда на объекты памяти в вашей программе больше нет ссылок, они не возвращаются в систему (например, небольшие объекты). Это хорошо для вашей программы, если вы выделяете и освобождаете много объектов (которые принадлежат одному и тому же 8-байтовому пулу), потому что Python не должен беспокоить систему, что относительно дорого. Но это не так хорошо, если ваша программа обычно использует X байтов и при некоторых временных условиях она использует в 100 раз больше (например, анализирует и обрабатывает большой файл конфигурации только при запуске).

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

Профилировщик памяти

Python code optimization tool -Memory_profiler - изображение номер двадцать шесть
Python code optimization tool -Memory_profiler — изображение номер двадцать шесть

Чтобы измерить и измерить фактическое использование памяти вашей программой, вы можете использовать модуль memory_profiler. Я немного поиграл с этим, и я не уверен, что доверяю результатам. Используя это очень просто. Вы декорируете функцию (может быть основной (функция 0)) с помощью декоратора @profiler, и, когда программа завершает работу, профилировщик памяти выводит на стандартный вывод удобный отчет, который показывает общее количество и изменения в памяти для каждой строки. Вот пример Программа, которую я запускал под профилировщиком:

Как вы можете видеть, 22,9 МБ дополнительной памяти занимают. Причина, по которой память не увеличивается при добавлении целых чисел как внутри, так и вне диапазона [-5, 256], а также при добавлении строки, заключается в том, что во всех случаях используется один объект. Непонятно, почему первый цикл диапазона (100000) в строке 8 добавляет 4,2 МБ, а второй в строке 10 добавляет всего 0,4 МБ, а третий цикл в строке 12 добавляет 0,8 МБ. Наконец, при удалении списков a, b и c освобождается -0.6MB для a и c, но для b добавляется 0.2MB. Я не могу иметь много смысла из этих результатов.

Memory Profiler

Memory profiling in - изображение номер двадцать семь
Memory profiling in — изображение номер двадцать семь

To gauge and measure the actual memory usage of your program, you can use the memory\_profiler module. I played with it a little bit and I’m not sure I trust the results. Using it is very simple. You decorate a function (could be the main function) with an @profiler decorator, and when the program exits, the memory profiler prints to standard output a handy report that shows the total and changes in memory for every line. Here is a sample program I ran under the profiler:

Таблица №18

1
from memory\_profiler import profile
2

3
@profile
4
def main():
5
 a = []
6
 b = []
7
 c = []
8
 for i in range(100000):
9
 a.append(5)
10
 for i in range(100000):
11
 b.append(300)
12
 for i in range(100000):
13
 c.append('123456789012345678901234567890')
14
 del a
15
 del b
16
 del c
17

18
 print('Done!')
19
 
20
if __name__ == '__main__':
21
 main()

Таблица №19

1
Filename: python_obj.py
2

3
Line # Mem usage Increment Occurrences Line Contents
4
=============================================================
5
 3 17.3 MiB 17.3 MiB 1 @profile
6
 4 def main():
7
 5 17.3 MiB 0.0 MiB 1 a = []
8
 6 17.3 MiB 0.0 MiB 1 b = []
9
 7 17.3 MiB 0.0 MiB 1 c = []
10
 8 18.0 MiB 0.0 MiB 100001 for i in range(100000):
11
 9 18.0 MiB 0.8 MiB 100000 (5)
12
 10 18.7 MiB 0.0 MiB 100001 for i in range(100000):
13
 11 18.7 MiB 0.7 MiB 100000 (300)
14
 12 19.5 MiB 0.0 MiB 100001 for i in range(100000):
15
 13 19.5 MiB 0.8 MiB 100000 ('123456789012345678901234567890')
16
 14 18.9 MiB -0.6 MiB 1 del a
17
 15 18.2 MiB -0.8 MiB 1 del b
18
 16 17.4 MiB -0.8 MiB 1 del c
19
 17
20
 18 17.4 MiB 0.0 MiB 1 print('Done!')

As you can see, there is 17.3 MB of memory overhead. The reason the memory doesn’t increase when adding integers both inside and outside the [-5, 256] range and also when adding the string is that a single object is used in all cases. It’s not clear why the first loop of range(100000) on line 9 adds 0.8MB while the second on line 11 adds just 0.7MB and the third loop on line 13 adds 0.8MB. Finally, when deleting the a, b and c lists, -0.6MB is released for a, -0.8MB is released for b, and -0.8MB is released for c.

Практические методы оптимизации использования памяти

Оптимизация использования памяти в - изображение номер двадцать восемь
Оптимизация использования памяти в — изображение номер двадцать восемь

Понимание того, как именно ваш код использует память — лишь первый шаг. Следующий логический этап — применение этих знаний для оптимизации. Рассмотрим наиболее эффективные методы снижения потребления памяти в Python-приложениях. 💡

  • Выбор подходящих структур данных с учетом особенностей использования
  • Использование генераторов вместо создания полных списков
  • Применение специализированных библиотек для работы с большими данными
  • Эффективное управление жизненным циклом объектов
  • Сжатие данных и использование более компактных представлений

from pympler import asizeof standard_list = list(range(100)) tuple_version = tuple(range(100)) set_version = set(range(100)) print(f»Список: {(standard_list)} байт») print(f»Кортеж: {(tuple_version)} байт») print(f»Множество: {(set_version)} байт») import array array_version = (‘i’, range(100)) print(f»Массив: {(array_version)} байт») import numpy as np numpy_version = (range(100), dtype=np.int32) print(f»NumPy массив: {(numpy_version)} байт»)

def process_large_list(size): large_list = [i * i for i in range(size)] result = sum(large_list) return result def process_with_generator(size): result = sum(i * i for i in range(size)) return result import tracemalloc size = 10_000_000 () process_large_list(size) list_memory = tracemalloc.get_traced_memory() () () process_with_generator(size) gen_memory = tracemalloc.get_traced_memory() () print(f»Память при использовании списка: {list_memory} байт») print(f»Память при использовании генератора: {gen_memory} байт») print(f»Разница: {list_memory/gen_memory:.1f}x»)

Результаты впечатляют — генератор может использовать в сотни раз меньше памяти для одной и той же задачи!

Стандартная библиотека Python и сторонние пакеты предлагают оптимизированные структуры:

Таблица №20

Структура Применение Экономия памяти
Легковесная замена классам с неизменяемыми атрибутами До 50% по сравнению с классами
Однотипные числовые последовательности До 60% по сравнению со списком
Числовые вычисления До 90% для крупных массивов
bytes/bytearray Бинарные данные До 60% по сравнению со строками
Табличные данные Зависит от типов, до 70% при использовании категорий
Подсчет частотности элементов ~10-15% по сравнению с обычным словарем

def process_huge_data(): huge_data = [random_complex_object() for _ in range(1000000)] result = analyze(huge_data) del huge_data import gc () return result

class RegularPerson: def __init__(self, name, age, address, phone, email): = name = age = address = phone = email class OptimizedPerson: __slots__ = (‘name’, ‘age’, ‘address’, ‘phone’, ’email’) def __init__(self, name, age, address, phone, email): = name = age = address = phone = email regular = RegularPerson(«John», 30, «123 Main St», «555-1234», «john@») optimized = OptimizedPerson(«John», 30, «123 Main St», «555-1234», «john@») print(f»Обычный класс: {(regular)} байт») print(f»Оптимизированный класс: {(optimized)} байт»)

Использование __slots__ может сократить потребление памяти на 30-50% для классов с множеством экземпляров.

  • Инструменты сжатия данных: модули gzip, bz2, lzma для эффективного хранения
  • Мемоизация: сохранение результатов вычислений для предотвращения повторных расчетов
  • Пул объектов: переиспользование объектов вместо создания новых
  • Интернирование строк: повторное использование одинаковых строк
  • Внешнее хранение: перемещение данных во внешние хранилища (БД, файлы, Redis)

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

Измерение и оптимизация размера объектов в Python — это не разовая задача, а постоянный процесс совершенствования кода. Контроль за потреблением памяти должен стать частью вашей повседневной практики разработки. Начните с понимания того, как устроена ваша структура данных, используйте подходящие инструменты для измерения, и применяйте проверенные техники оптимизации. Помните, что эффективное использование памяти не только улучшает производительность вашего приложения, но и делает его более масштабируемым, надёжным и экологичным с точки зрения вычислительных ресурсов. Грамотное управление памятью — признак зрелого инженерного мышления.

Выделение памяти

Программирование на языке - изображение номер двадцать девять
Программирование на языке — изображение номер двадцать девять

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

Интересно, что частично вернуть выделенную под арену память нельзя, пока в ней есть хоть один непустой пул, в котором есть хоть один непустой блок. Как блоки становятся пустыми, мы обсудим в следующем разделе.

Освобождение памяти

Telegram-канал \ - изображение номер тридцать
Telegram-канал \ — изображение номер тридцать

В Python нет необходимости в ручной очистке памяти. Если объект больше не используется, все это перекладывается на сам интерпретатор и два механизма: счетчик ссылок и сборщик мусора.

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

Лечит или хитрости

Введение в - изображение номер тридцать один
Введение в — изображение номер тридцать один

Оказывается, что у CPython есть несколько хитростей, поэтому числа, которые вы получаете из deep_getsizeof (), не полностью отражают использование памяти программой Python.

Выучить питон

Питон 3 - изображение номер тридцать два
Питон 3 — изображение номер тридцать два

Изучите Python с нашим полным руководством по питону, независимо от того, начинаете ли вы или начинающий программист, ищущий новые навыки.

Conclusion

CPython uses a lot of memory for its objects. It also uses various tricks and optimizations for memory management. By keeping track of your object’s memory usage and being aware of the memory management model, you can significantly reduce the memory footprint of your program.

Часто задаваемые вопросы о размере списков в Python

Вопрос: Как узнать, сколько памяти занимает пустой список в Python?
Ответ: Используйте функцию sys.getsizeof([]), она покажет базовый размер объекта списка (обычно около 56 байт).

Вопрос: Почему sys.getsizeof() не показывает полный размер списка с элементами?
Ответ: sys.getsizeof() учитывает только память самого списка (массив указателей), но не память, занимаемую вложенными объектами.

Вопрос: Как измерить полный размер списка, включая все его элементы?
Ответ: Используйте рекурсивную функцию deep_getsizeof(), которая суммирует размеры всех вложенных объектов.

Вопрос: Влияет ли тип элементов списка на его общий размер?
Ответ: Да, сильно. Список целых чисел будет занимать меньше памяти, чем список строк или словарей, так как каждый объект имеет свой размер.

Вопрос: Как оптимизировать память при работе с большими списками?
Ответ: Используйте array.array для однотипных данных, генераторы вместо списков, или библиотеку NumPy для числовых массивов.

Вопрос: Что такое Memory Profiler и как он помогает?
Ответ: Это инструмент для построчного анализа использования памяти в Python-скрипте, позволяющий выявить утечки и неэффективные участки кода.

Вопрос: Как сборщик мусора влияет на память списков?
Ответ: Сборщик мусора автоматически освобождает память, занятую объектами, на которые нет ссылок, включая элементы удаленных списков.

Вопрос: Почему маленькие объекты (до 256 байт) могут занимать больше памяти?
Ответ: Python использует пулы памяти для маленьких объектов, что может приводить к фрагментации и дополнительным накладным расходам.

Вопрос: Как проверить, сколько памяти занимает список в реальном времени?
Ответ: Используйте модуль tracemalloc для отслеживания выделений памяти или psutil для мониторинга системной памяти процесса.

Вопрос: В чем разница между памятью Python и системной памятью?
Ответ: Память Python — это память, выделенная интерпретатором для объектов, а системная память — общая память процесса, которая может включать накладные расходы ОС.