Python中的memoryview到底有什么作用?

127

查看关于memoryview的文档:

memoryview对象允许Python代码在不复制的情况下访问支持缓冲区协议的对象的内部数据。

class memoryview(obj)

创建一个引用obj的memoryview。 obj必须支持缓冲区协议。 支持缓冲区协议的内置对象包括bytes和bytearray。

然后我们有以下示例代码:

>>> 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(或旧的缓冲区对象)。如果我们正在进行大量的小切片或者进行大量次数的小切片,则存在更好的效率需求。

在上述方案中,除非有人能够向我解释我错过了什么,否则我不认为它对任何一种情况都有用。

编辑1:

我们有一大段数据,我们想通过从头到尾逐步处理它,例如从字符串缓冲区的开头提取标记,直到消耗缓冲区。在C术语中,这是通过缓冲区推进指针,并且可以将该指针传递给期望缓冲区类型的任何函数。如何在Python中实现类似的功能?

人们建议变通方法,例如许多字符串和正则表达式函数带有可以用于模拟推进指针的位置参数。这里有两个问题:首先它是一个变通方法,你被迫改变你的编码风格以克服缺点,其次:并非所有函数都有位置参数,例如正则表达式函数和startswith没有,encode()/decode()也没有。

其他人可能建议分块加载数据,或者在大于最大标记的小段中处理缓冲区。好的,我们意识到这些可能的变通方法,但是我们应该以更自然的方式在Python中工作,而不是试图弯曲编码风格来适应语言——难道不是吗?

编辑2:

一段代码示例可以使事情更清晰明了。这就是我想做的事情,也是我乍一看认为memoryview会让我做的事情。让我们使用pmview(proper memory view)来表示我正在寻找的功能:

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? - zr.
12
参考问题的答案缺乏详细说明,而且该问题也没有从学习者的角度触及潜在问题。 - Basel Shishani
6个回答

119
< p > memoryview 有用的一个原因是,它们可以被切片而不需要复制底层数据,这与 bytes/str 不同。

例如,考虑以下玩具示例。

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

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

在我的电脑上,我得到

     bytes 100000 0.211
     bytes 200000 0.826
     bytes 300000 1.953
     bytes 400000 3.514
memoryview 100000 0.021
memoryview 200000 0.052
memoryview 300000 0.043
memoryview 400000 0.077

您可以清楚地看到重复字符串切片的二次复杂度。即使只有400000次迭代,它已经难以控制。与此同时,memoryview版本具有线性复杂度,并且速度非常快。

编辑:请注意,这是在CPython中完成的。Pypy 4.0.1之前存在一个错误,导致内存视图具有二次性能。


13
这个回答并没有解决提问者所说的问题,即要想做任何“有用”的事情,必须使用bytes()来复制对象... - ragardner
5
正如我的例子所示,即使你最终将其复制到一个字节对象中,使用它进行中间操作也是高效的。 - Antimony
不复制底层数据,所以 b[1:] 就像返回一个从索引 1 开始的指针/引用(无论你如何命名它)? - Rick
1
没错。实际上有点像返回一个(指针,长度)对。 - Antimony
1
正如我的例子展示的那样,这对于高效地进行中间操作非常有用。你的例子并未展示出这点,你只是使用了不同于零的偏移量进行索引,没有进行任何操作。 - user9413641
显示剩余3条评论

83

memoryview对象在需要支持索引的二进制数据子集时非常有用。与需要获取切片(并创建新的、潜在大的)对象以传递给另一个API不同,您可以使用memoryview对象。

一个这样的API示例是struct模块。您不必传入大的bytes对象的切片来解析出打包的C值,而是传入仅从中提取值所需的区域的memoryview对象。

事实上,memoryview对象本身支持struct解压缩;您可以针对底层bytes对象的某个区域进行定位,然后使用.cast()将底层字节“解释”为长整数、浮点数或n维整数列表。这使得二进制文件格式解释非常高效,而无需创建更多字节的副本。


1
当您需要支持更多内容的子集时,该怎么办?! - Basel Shishani
2
@BaselShishani:不要使用memoryview。因为你正在处理文本,而不是二进制数据。 - Martijn Pieters
7
对一个 memoryview 进行切片会返回一个新的 memoryview,该 memoryview 只包含被切片的那部分数据。 - Martijn Pieters
1
@MartijnPieters,你的意思是说,逐个转换memoryview对象的元素比切片bytes本身或将整个memoryview切片转换为bytes更快? 你有一点测试数据吗? - Marco Sulla
1
我阅读了Antimony的回答。它只对切片进行了基准测试,而没有对cast()进行测试。切片bytes与切片memoryview并不相同,您必须像您所说的那样进行cast() - Marco Sulla
显示剩余8条评论

9

让我清楚地解释一下这里的理解问题所在。

像我这样的问答者希望能够创建一个memoryview,选择现有数组的一个切片(例如bytes或bytearray)。我们因此期望有这样一种做法:

desired_slice_view = memoryview(existing_array, start_index, end_index)

遗憾的是,没有这样的构造函数,并且文档没有明确说明应该怎么做。

关键是你必须先创建一个覆盖整个现有数组的memoryview。从那个memoryview中,你可以创建第二个memoryview,覆盖现有数组的一个切片,就像这样:

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

简而言之,第一行的目的仅是提供一个对象,其切片实现(dunder-getitem)返回一个memoryview。
这可能看起来有些凌乱,但可以有几种理性化的方式:
1. 我们想要的输出是一个memoryview,它是某个东西的切片。通常情况下,我们通过在该类型的对象上使用切片操作符[10:20]来从该对象中获取切片对象。因此,有理由期望我们需要从一个memoryview中获取我们想要的切片视图,因此第一步是获取整个底层数组的memoryview。 2. 一个带有开始和结束参数的memoryview构造函数的天真期望未考虑到切片规范确实需要通常的切片操作符的所有表现力(包括诸如[3::2]或[:-4]等)。在那个一行代码的构造函数中没有办法只使用现有的(并且已知的)操作符。你不能将它附加到现有的数组参数上,因为那样会创建该数组的一个切片,而不是告诉memoryview构造函数一些切片参数。而且你也不能将操作符本身用作参数,因为它是一个操作符,而不是一个值或对象。 可以想象,memoryview构造函数可以接受一个切片对象:
desired_slice_view = memoryview(existing_array, slice(1, 5, 2) )

...但是这并不是很令人满意,因为用户需要学习有关切片对象以及它的构造函数的参数含义,而他们已经按照切片操作符号的符号来思考了。


3

以下是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))

2

Antimony提供了一个非常棒的例子。 实际上,在Python3中,您可以通过data = bytes(n)来替换data = 'x'*n,并在print语句中加括号,如下所示:

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)

1
以下代码可能更好地解释了这个问题。假设您无法控制foreign_func的实现方式。您可以直接使用bytes调用它,也可以使用这些字节的memoryview进行调用:
from pandas import DataFrame
from timeit import timeit


def foreign_func(data):
    def _foreign_func(data):
        # Did you know that memview slice can be compared to bytes directly?
        assert data[:3] == b'xxx'
    _foreign_func(data[3:-3])


# timeit
bytes_times = []
memoryview_times = []
data_lens = []
for n in range(1, 10):
    data = b'x' * 10 ** n
    data_lens.append(len(data))
    bytes_times.append(timeit(
        'foreign_func(data)', globals=globals(), number=10))
    memoryview_times.append(timeit(
        'foreign_func(memoryview(data))', globals=globals(), number=10))


# output
df = DataFrame({
    'data_len': data_lens,
    'memoryview_time': memoryview_times,
    'bytes_time': bytes_times
})
df['times_faster'] = df['bytes_time'] / df['memoryview_time']
print(df)
df[['memoryview_time', 'bytes_time']].plot()

结果:

     data_len  memoryview_time  bytes_time   times_faster
0          10         0.000019    0.000012       0.672033
1         100         0.000016    0.000011       0.690320
2        1000         0.000016    0.000013       0.833314
3       10000         0.000016    0.000037       2.387100
4      100000         0.000016    0.000086       5.300594
5     1000000         0.000018    0.001134      63.357466
6    10000000         0.000009    0.028672    3221.528855
7   100000000         0.000009    0.258822   28758.547214
8  1000000000         0.000009    2.779704  292601.789177

calling with bytes gets exponentially slower


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接