Python中的memoryview的确切含义是什么


84

查看memoryview上的文档

memoryview对象允许Python代码无需复制即可访问支持缓冲区协议的对象的内部数据。

memoryview(obj)

创建引用obj的memoryview。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(或旧的缓冲区对象),并且切片也可能很大。如果我们要制作大片或多次制作小片,那么就需要更高的效率。

使用上述方案,除非有人可以向我解释我在这里缺少的内容,否则我看不出它对任何一种情况都有用。

编辑1:

我们有大量数据,我们想通过从头到尾进行处理来处理它,例如从字符串缓冲区的开头提取标记直到缓冲区被消耗。在C术语中,这是通过缓冲区,并且指针可以传递给任何需要缓冲区类型的函数。如何在python中完成类似的操作?

人们提出了解决方法,例如,许多字符串和正则表达式函数采用位置参数,可用于模拟前进的指针。这样做有两个问题:首先是一种解决方法,您不得不更改编码样式以克服缺点,其次,并非所有函数都具有位置参数,例如regex函数,而startswithdo,encode()/ decode()not则没有。

其他人可能建议分块加载数据,或在大于最大令牌的小段中处理缓冲区。好的,我们知道了这些可能的解决方法,但是我们应该在python中以更自然的方式工作,而不必尝试弯曲编码样式以适应语言-是不是?

编辑2:

代码示例将使事情变得更清楚。这就是我想要做的,并且我以为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


9
所引用问题的答案未提供详细信息。这个问题也没有从学习者的角度触及潜在的问题。
巴塞尔Shishani 2014年

Answers:


81

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 3中不起作用TypeError: memoryview: a bytes-like object is required, not 'str'
微积分

@Joseprint作为语句在Python 3中也不起作用。该代码是为Python 2编写的,尽管python 3所需的更改相当琐碎。

@Yumi Tada,str在python3中与在python2中定义的完全不同。
hcnhcn012

3
这个答案没有解决这样一个事实,即如提问者所说要做任何“有用的”事情,您必须使用bytes()复制对象……
ragardner

1
@ citizen2077如我的示例所示,即使最终将其复制到bytes对象,它对于有效地进行中间操作也很有用。

58

memoryview当您需要只需要支持索引的二进制数据的子集时,对象就很棒。无需获取切片(并创建可能更大的新对象)以传递给另一个API,您只需获取一个memoryview对象即可。

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

memoryview实际上,对象支持struct本机解压缩;您可以bytes使用切片将基础对象的区域作为目标,然后用于.cast()将基础字节“解释”为长整数,浮点值或n维整数列表。这使得非常有效的二进制文件格式解释,而不必创建更多的字节副本。


1
当您需要支持索引以外的子集时,该怎么办?
巴塞尔Shishani 2013年

2
@BaselShishani:不使用memoryview。您正在处理文本,而不是二进制数据。
马丁·彼得斯

是的,处理文本。因此,我们不使用memoryview,还有其他选择吗?
巴塞尔Shishani 2013年

您想解决什么问题?您需要测试的子字符串大吗?
马丁·彼得斯

6
@BaselShishani:切片memoryview返回一个仅覆盖该区域的新memoryview。
马丁·彼得斯

4

让我明确指出理解此处的故障所在。

像我一样,提问者希望能够创建一个内存视图,该视图选择现有数组的一个切片(例如,字节或字节数组)。因此,我们期望这样的事情:

desired_slice_view = memoryview(existing_array, start_index, end_index)

las,没有这样的构造函数,文档也没有指出该怎么做。

关键是您必须首先创建一个覆盖整个现有阵列的memoryview。从该memoryview中,您可以创建第二个memoryview,它覆盖现有阵列的一部分,如下所示:

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

简而言之,第一行的目的仅仅是提供一个对象,其切片实现(dunder-getitem)返回一个memoryview。

这看似很麻烦,但是可以通过以下两种方法使其合理化:

  1. 我们期望的输出是一个内存视图,它是某些东西的一部分。通常,我们通过使用切片运算符[10:20]从相同类型的对象中获得切片的对象。因此,有一些理由可以期望我们需要从memoryview中获取desired_slice_view,因此第一步就是获取整个基础数组的memoryview。

  2. 带有开始和结束参数的memoryview构造函数的幼稚期望不能认为分片规范确实需要通常的分片运算符的所有表现力(包括[3 :: 2]或[:-4]等)。在那种单行构造函数中,无法仅使用现有的(并理解的)运算符。您不能将其附加到existing_array参数,因为它将构成该数组的一个切片,而不是告诉memoryview构造函数一些切片参数。而且您不能将运算符本身用作参数,因为它是运算符而不是值或对象。

可以想象,一个memoryview构造函数可以采用一个slice对象:

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

...但这不是很令人满意,因为当用户已经以slice运算符的方式思考时,他们将不得不了解slice对象及其构造函数的参数含义。


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)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.