У чому саме сенс memoryview у Python


85

Перевірка документації по пам'яті:

об'єкти memoryview дозволяють коду Python отримувати доступ до внутрішніх даних об'єкта, що підтримує буферний протокол, без копіювання.

клас memoryview (obj)

Створіть перегляд пам'яті, який посилається на 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 для викриття внутрішніх даних буферного об'єкта без копіювання, однак, щоб зробити щось корисне з об'єктом (викликаючи методи, передбачені об'єктом), нам потрібно створити копію!

Зазвичай перегляд пам'яті (або старий буферний об'єкт) буде потрібен, коли ми маємо великий об'єкт, і фрагменти також можуть бути великими. Потреба в кращій ефективності буде присутня, якщо ми робимо великі фрагменти або робимо невеликі фрагменти, але велику кількість разів.

З наведеною вище схемою я не бачу, як це може бути корисним для будь-якої ситуації, якщо хтось не може пояснити мені, чого я тут пропускаю.

Edit1:

У нас є велика частина даних, ми хочемо обробити їх, просуваючись по них від початку до кінця, наприклад, витягуючи токени з початку буфера рядків, поки буфер не буде спожитий. буфер, і вказівник може бути переданий будь-якій функції, що очікує типу буфера. Як можна зробити щось подібне в python?

Люди пропонують обхідні шляхи, наприклад, багато функцій рядків і регулярних виразів беруть аргументи позиції, які можна використовувати для емуляції просування вказівника. З цим виникають дві проблеми: по-перше, це обхід, ви змушені змінити стиль кодування, щоб подолати недоліки, і по-друге: не всі функції мають аргументи позиції, наприклад функції регулярного виразу і startswithроблять, encode()/ decode()не.

Інші можуть запропонувати завантажувати дані фрагментами або обробляти буфер невеликими сегментами, більшими за максимальний маркер. Гаразд, отже, ми знаємо про ці можливі обхідні шляхи, але ми повинні працювати більш природним чином у python, не намагаючись переробити стиль кодування відповідно до мови - чи не так?

Edit2:

Зразок коду зробить речі зрозумілішими. Це те, що я хочу зробити, і те, що, як я припускав, перегляд пам'яті дозволить мені зробити це на перший погляд. Давайте використовувати 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


9
Відповідь на запитання, на яке посилається, не містить деталей. Також питання не торкається потенційних проблем з боку учня.
Базель Шишані,

Відповіді:


82

Одна з причин memoryviewкорисна тому, що їх можна нарізати, не копіюючи базові дані, на відміну від 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'
Числення

@Jose printяк твердження також не працює в Python 3. Цей код був написаний для Python 2, хоча зміни, необхідні для python 3, є досить тривіальними.
Сурма

@Yumi Tada, strу python3 абсолютно інший, що визначається в python2.
hcnhcn012

5
Ця відповідь не стосується того факту, що для того, щоб зробити щось "корисне", як заявляє
запитувач,

1
@ citizen2077 Як показує мій приклад, це корисно для ефективного виконання проміжних маніпуляцій, навіть якщо ви в кінцевому підсумку скопіюєте їх у байт-об'єкт.
Сурма

59

memoryviewоб’єкти чудові, коли вам потрібні підмножини двійкових даних, які потребують лише підтримки індексації. Замість того, щоб брати фрагменти (і створювати нові, потенційно великі) об'єкти для передачі в інший API, ви можете просто взяти файлmemoryview об’єкт.

Одним з таких прикладів API може бути structмодуль. Замість того, щоб передавати фрагмент великого bytesоб'єкта для синтаксичного розбору упакованих значень C, ви передаєте memoryviewпросто область, з якої вам потрібно отримати значення.

memoryviewфактично об'єкти підтримують structрозпакування; Ви можете націлити на область базового bytesоб'єкта зрізом, а потім використовувати .cast()для "інтерпретації" базових байтів як довгих цілих чисел, значень з плаваючою комою або n-мірних списків цілих чисел. Це робить дуже ефективними інтерпретації формату двійкових файлів, не створюючи більше копій байтів.


1
І що ви робите, коли вам потрібні підмножини, які підтримують більше, ніж індексацію ?!
Базель Шишані,

2
@BaselShishani: не використовувати a memoryview. Тоді ви маєте справу з текстом, а не з двійковими даними.
Мартін Пітерс

Так, справу з текстом. Тож ми не використовуємо memoryview, чи є альтернатива?
Базель Шишані,

Яку проблему ви намагаєтесь вирішити? Чи такі великі підрядки, щоб перевірити?
Мартін Пітерс

6
@BaselShishani: нарізання перегляду пам'яті повертає новий перегляд пам'яті, що охоплює саме цю область.
Мартін Пітерс

5

Дозвольте мені пояснити, у чому полягає проблема у розумінні тут.

Допитувач, як і я, очікував, що зможе створити перегляд пам'яті, який вибирає фрагмент існуючого масиву (наприклад, байти або байтовий масив). Тому ми очікували приблизно такого:

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 з перегляду пам'яті, і що, отже, першим кроком є ​​отримання перегляду пам'яті всього базового масиву.

  2. Наївне очікування конструктора пам'яті з аргументами start і end не враховує, що специфікація зрізу дійсно потребує всій виразності звичайного оператора зрізу (включаючи такі речі, як [3 :: 2] або [: -4] тощо). Немає можливості просто використовувати існуючий (і зрозумілий) оператор у цьому однолінійному конструкторі. Ви не можете приєднати його до аргументу existing_array, оскільки це зробить зріз цього масиву, замість того, щоб повідомляти конструктору пам'яті деякі параметри зрізу. І ви не можете використовувати сам оператор як аргумент, оскільки це оператор, а не значення або об’єкт.

Можливо, конструктор memoryview може взяти об'єкт зрізу:

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

... але це не дуже задовільно, оскільки користувачам доведеться дізнатись про об'єкт зрізу та про те, що означають параметри його конструктора, коли вони вже думають з точки зору оператора зрізу.


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

Чудовий приклад Сурма. Насправді в 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)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.