Published: Mo 10 April 2023
By Denis Obrezkov
In Python .
Мотивация
Иногда бывает необходимо поделиться сырыми данными с другой программой. К примеру, мы работаем с изображением и хотим отправить его в специализированную библиотеку, написанную на 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 позволяют сэкономить и память, и время, однако, из-за своей узконаправленности и низкоуровневости используются достаточно редко.