В чем именно заключается смысл memoryview в Python

86

Проверка документации по memoryview:

Объекты memoryview позволяют коду Python получать доступ к внутренним данным объекта, который поддерживает протокол буфера, без копирования.

класс memoryview (объект)

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

Затем нам дается пример кода:

>>> v = memoryview(b'abcefg')
>>> v[1]
98
>>> v[-1]
103
>>> v[1:4]
<memory at 0x7f3ddc9f4350>
>>> bytes(v[1:4])
b'bce'

Цитата окончена, теперь давайте рассмотрим подробнее:

>>> b = b'long bytes stream'
>>> b.startswith(b'long')
True
>>> v = memoryview(b)
>>> vsub = v[5:]
>>> vsub.startswith(b'bytes')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'memoryview' object has no attribute 'startswith'
>>> bytes(vsub).startswith(b'bytes')
True
>>> 

Итак, что я понял из вышесказанного:

Мы создаем объект memoryview для предоставления внутренних данных объекта буфера без копирования, однако, чтобы сделать что-нибудь полезное с объектом (путем вызова методов, предоставленных объектом), мы должны создать копию!

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

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

Edit1:

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

Люди предлагают обходные пути, например, многие строковые и регулярные функции принимают аргументы позиции, которые можно использовать для имитации продвижения указателя. С этим есть две проблемы: во-первых, это обходной путь, вы вынуждены изменить свой стиль кодирования, чтобы преодолеть недостатки, и, во-вторых, не все функции имеют аргументы позиции, например функции регулярных выражений и startswithdo, encode()/ decode()not.

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

Edit2:

Образец кода проясняет ситуацию. Это то, что я хочу сделать, и то, что я предполагал, с первого взгляда позволит мне сделать memoryview. Давайте использовать pmview (правильное представление памяти) для функциональности, которую я ищу:

tokens = []
xlarge_str = get_string()
xlarge_str_view =  pmview(xlarge_str)

while True:
    token =  get_token(xlarge_str_view)
    if token: 
        xlarge_str_view = xlarge_str_view.vslice(len(token)) 
        # vslice: view slice: default stop paramter at end of buffer
        tokens.append(token)
    else:   
        break
Базель Шишани
источник
возможный дубликат Когда следует использовать memoryview?
зр.
9
Ответ на указанный вопрос не содержит подробностей. Вопрос также не касается потенциальных проблем с точки зрения учащегося.
Basel Shishani

Ответы:

83

Одна из причин, по которой memoryviews полезны, заключается в том, что их можно разрезать без копирования базовых данных, в отличие от bytes/ str.

Например, возьмем следующий игрушечный пример.

import time
for n in (100000, 200000, 300000, 400000):
    data = 'x'*n
    start = time.time()
    b = data
    while b:
        b = b[1:]
    print 'bytes', n, time.time()-start

for n in (100000, 200000, 300000, 400000):
    data = 'x'*n
    start = time.time()
    b = memoryview(data)
    while b:
        b = b[1:]
    print 'memoryview', n, time.time()-start

На моем компьютере я получаю

bytes 100000 0.200068950653
bytes 200000 0.938908100128
bytes 300000 2.30898690224
bytes 400000 4.27718806267
memoryview 100000 0.0100269317627
memoryview 200000 0.0208270549774
memoryview 300000 0.0303030014038
memoryview 400000 0.0403470993042

Вы можете ясно видеть квадратичную сложность повторяющейся нарезки строки. Даже после 400000 итераций это уже невозможно. Между тем, версия memoryview имеет линейную сложность и молниеносно.

Изменить: обратите внимание, что это было сделано в CPython. В Pypy до 4.0.1 была ошибка, из-за которой просмотры памяти имели квадратичную производительность.

Сурьма
источник
Пример не работает в python 3TypeError: memoryview: a bytes-like object is required, not 'str'
Calculus
@Jose printкак оператор также не работает в Python 3. Этот код был написан для Python 2, хотя изменения, необходимые для Python 3, довольно тривиальны.
Antimony
@Yumi Tada, strв python3 совершенно другое определение в python2.
hcnhcn012
5
Этот ответ не касается того факта, что, как заявляет спрашивающий, чтобы сделать что-нибудь «полезное», вам нужно использовать bytes (), который копирует объект ...
ragardner
1
@ Citizen2077 Как показывает мой пример, он полезен для эффективного выполнения промежуточных манипуляций, даже если вы в конечном итоге скопируете его в объект байтов.
Antimony
59

memoryviewОбъекты отлично подходят, когда вам нужны подмножества двоичных данных, которые должны поддерживать только индексирование. Вместо того, чтобы делать срезы (и создавать новые, потенциально большие) объекты для передачи другому API, вы можете просто взять memoryviewобъект.

Одним из таких примеров API может быть structмодуль. Вместо того, чтобы передавать фрагмент большого bytesобъекта для анализа упакованных значений C, вы передаете memoryviewтолько ту область, из которой необходимо извлечь значения.

memoryviewобъекты фактически поддерживают structраспаковку изначально; вы можете нацелить область базового bytesобъекта с помощью среза, а затем использовать .cast()для «интерпретации» базовых байтов как длинных целых чисел, значений с плавающей запятой или n-мерных списков целых чисел. Это обеспечивает очень эффективную интерпретацию формата двоичного файла без необходимости создания дополнительных копий байтов.

Мартейн Питерс
источник
1
А что делать, когда вам нужны подмножества, которые поддерживают не только индексирование ?!
Basel Shishani
2
@BaselShishani: не используйте memoryview. Тогда вы имеете дело с текстом, а не с двоичными данными.
Мартейн Питерс
Да, дело с текстом. Итак, мы не используем memoryview, есть ли альтернатива?
Basel Shishani
Какую проблему ты пытаешься решить? Нужны ли вам подстроки для тестирования такого размера?
Мартейн Питерс
6
@BaselShishani: нарезка memoryview возвращает новое memoryview, охватывающее только этот регион.
Мартин Питерс
5

Позвольте мне объяснить, в чем заключается проблема понимания.

Спрашивающий, как и я, ожидал, что сможет создать представление памяти, которое выбирает часть существующего массива (например, bytes или bytearray). Поэтому мы ожидали чего-то вроде:

desired_slice_view = memoryview(existing_array, start_index, end_index)

Увы, такого конструктора нет, и вместо этого в документации не указывается, что делать.

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

whole_view = memoryview(existing_array)
desired_slice_view = whole_view[10:20]

Короче говоря, цель первой строки - просто предоставить объект, реализация среза которого (dunder-getitem) возвращает представление в памяти.

Это может показаться неряшливым, но можно рационализировать это несколькими способами:

  1. Наш желаемый результат - это просмотр памяти, который представляет собой кусок чего-то. Обычно мы получаем срезанный объект из объекта того же типа, используя для него оператор среза [10:20]. Так что есть основания ожидать, что нам нужно получить желаемый_slice_view из memoryview, и, следовательно, первым шагом будет получение memoryview всего базового массива.

  2. Наивное ожидание конструктора memoryview с аргументами start и end не учитывает, что спецификация среза действительно требует всей выразительности обычного оператора среза (включая такие вещи, как [3 :: 2] или [: -4] и т. Д.). Невозможно просто использовать существующий (и понятный) оператор в этом однострочном конструкторе. Вы не можете присоединить его к аргументу existing_array, так как это сделает часть этого массива, вместо того, чтобы сообщить конструктору memoryview некоторые параметры среза. И вы не можете использовать сам оператор в качестве аргумента, потому что это оператор, а не значение или объект.

Возможно, конструктор memoryview мог бы взять объект среза:

desired_slice_view = memoryview(existing_array, slice(1, 5, 2) )

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

Gwideman
источник
4

Вот код python3.

#!/usr/bin/env python3

import time
for n in (100000, 200000, 300000, 400000):
    data = b'x'*n
    start = time.time()
    b = data
    while b:
        b = b[1:]
    print ('bytes {:d} {:f}'.format(n,time.time()-start))

for n in (100000, 200000, 300000, 400000):
    data = b'x'*n
    start = time.time()
    b = memoryview(data)
    while b:
        b = b[1:]
    print ('memview {:d} {:f}'.format(n,time.time()-start))
джимаф
источник
1

Отличный пример от Antimony. Фактически, в Python3 вы можете заменить data = 'x' * n на data = bytes (n) и поставить скобки для операторов печати, как показано ниже:

import time
for n in (100000, 200000, 300000, 400000):
    #data = 'x'*n
    data = bytes(n)
    start = time.time()
    b = data
    while b:
        b = b[1:]
    print('bytes', n, time.time()-start)

for n in (100000, 200000, 300000, 400000):
    #data = 'x'*n
    data = bytes(n)
    start = time.time()
    b = memoryview(data)
    while b:
        b = b[1:]
    print('memoryview', n, time.time()-start)
user2494386
источник