Мотивация
Иногда бывает необходимо поделиться сырыми данными с другой программой. К примеру, мы работаем с изображением и хотим отправить его в специализированную библиотеку, написанную на C++. При этом пересылка всего изображения может быть слишком излишней и мы хотим обработать его по частям. В этом случае мы можем использовать специальную структуру, которая позволяет эффективно работать с массивами сырых данных без создания копий объекта — memoryview.
Об объектах memoryview
Объекты memoryview позволяют делиться участками памяти интерпретатора. Объекты memoryview можно получить из объектов, которые поддерживают протокол для работы с программами на языке C (buffer protocol). В python — это bytes и bytearray. Работа с memoryview строится следующим образом: взять подходящий объект, создать из него объект memoryview, создавать срезы из memoryview без привлечения новой памяти.
Пример использования:
>>> v = memoryview(b'abcefg')
>>> print(type(v))
<class 'memoryview'>
>>> print(v[2])
99
>>> print(v[1:3])
<memory at 0x7f454a259000>
>>> print(bytes(v[1:3]))
b'bc'
Пример сохранения памяти:
Давайте теперь убедимся, что memoryview не выделяет дополнительную память под срезы. Для того, чтобы увидеть, сколько памяти используется, мы будет использовать стандартную библиотеку tracemalloc (https://docs.python.org/3/library/tracemalloc.html):
Для начала создадим объект bytes и убедимся, что при создании из него среза типа bytes, новая память выделяется:
tracemalloc.start()
# create a byte sequence of 1000000 bytes
data = bytes(1000000)
# take a snapshot before making a slice
snapshot1 = tracemalloc.take_snapshot()
# slice! type(new_data) == 'bytes', its size is around 500 KB
new_data = data[:500000]
# take a snapshot after making the slice
snapshot2 = tracemalloc.take_snapshot()
tracemalloc.stop()
top_stats = snapshot2.compare_to(snapshot1, 'lineno')
print("[ Top 10 ]")
for stat in top_stats[:5]:
print(stat)
получаем вывод (cpython):
<stdin>:1: size=1465 KiB (+488 KiB), count=2 (+1), average=732 KiB
/usr/lib/python3.10/tracemalloc.py:423: size=88 B (+88 B), count=2 (+2), average=44 B
/usr/lib/python3.10/tracemalloc.py:560: size=48 B (+48 B), count=1 (+1), average=48 B
/usr/lib/python3.10/tracemalloc.py:315: size=40 B (+40 B), count=1 (+1), average=40 B
<unknown>:0: size=9220 B (+0 B), count=10 (+0), average=922 B
Можно увидеть, что у нас было выделено дополнительно примерно 488 Кибибайт (488 * 1024 байта). Посмотрим, что будет, если использовать memoryview:
tracemalloc.start()
# create a byte sequence of 1000000 bytes
data = bytes(1000000)
# take a snapshot before making a slice
snapshot1 = tracemalloc.take_snapshot()
# create memoryview
mview = memoryview(data)
# slice! type(new_data) == 'memoryview', shouldn't consume much memory
new_mdata = mview[:500000]
# take a snapshot after making the slice
snapshot2 = tracemalloc.take_snapshot()
tracemalloc.stop()
top_stats = snapshot2.compare_to(snapshot1, 'lineno')
print("[ Top 10 ]")
for stat in top_stats[:5]:
print(stat)
получаем вывод (cpython):
<stdin>:1: size=977 KiB (+496 B), count=4 (+3), average=244 KiB
/usr/lib/python3.10/tracemalloc.py:423: size=88 B (+88 B), count=2 (+2), average=44 B
/usr/lib/python3.10/tracemalloc.py:560: size=48 B (+48 B), count=1 (+1), average=48 B
/usr/lib/python3.10/tracemalloc.py:315: size=40 B (+40 B), count=1 (+1), average=40 B
<unknown>:0: size=9220 B (+0 B), count=10 (+0), average=922 B
К исходному расходу памяти у нас прибавилось всего около 500 байт, несмотря на создание memoryview размером в 500 тысяч байт. Таким образом, новая память под элементы среза в memoryview не была выделена, мы буквально получаем ссылку на кусок уже существующей памяти.
Заключение
Мы рассмотрели класс memoryview для предоставления доступа к сырой памяти. Обычно он используется, когда необходимо передать в более низкоуровневую библиотеку набор сырых байт, как они представлены в памяти, либо когда мы хотим работать со срезами байтовых объектов, но хотим предотвратить их многократное копирование. Срезы memoryview позволяют сэкономить и память, и время, однако, из-за своей узконаправленности и низкоуровневости используются достаточно редко.