来自生成器

时间:2017-09-20 09:40:15

标签: python-3.x

假设我有一个像

这样的生成器
gen = (i*2 for i in range(100))

我现在想要创建一个包含生成器产生的所有值的字节对象。我可以做到以下几点:

b = bytes(gen)

我现在的问题是:由于bytes对象是不可变的,在这种情况下内存分配如何工作?我是否必须假设对于生成器生成的每个元素,都会创建一个新的bytes对象,并将先前的内容加上另一个元素复制到其中?这对于较大长度的发电机来说效率非常低。由于生成器不提供任何长度信息,似乎没有任何其他方式在内部预先分配所需的内存。

然后,在尽可能少的内存使用情况下,实现这一目标的更好方法是什么?如果我先使用(可变)bytearray并将其转换为bytes对象?

b = bytes(bytearray(gen))

甚至是一个清单?

b = bytes(list(gen))

但这看起来有些奇怪且反直觉......

背景:我已经通过另一个模块(.pyd)的C-API一次一个地读取特定的生成器(在0..255中作为Python整数),并且事先已知序列的总长度,最多2 ** 25个字节。我的readout函数应该收集它们并返回一个我认为合适的bytes对象,因为数据是只读的。

3 个答案:

答案 0 :(得分:5)

bytes(iterator) 使用内部 C-API _PyBytes_FromIterator 函数从迭代器创建字节对象,该函数使用特殊的 _PyBytes_Writer 协议。它内部使用一个缓冲区,当它溢出时使用规则调整大小:

bufsize += bufsize  / OVERALLOCATE_FACTOR

Linux OVERALLOCATE_FACTOR=4,Windows OVERALLOCATE_FACTOR=2。

那些。这个过程看起来像写入 RAM 中的文件。最后,缓冲区的内容返回。

答案 1 :(得分:2)

这可能是一个评论或讨论开始而不是答案,但我认为最好像这样格式化它。 我只是挂钩,因为我觉得这个话题也非常有意思。

我建议粘贴真正的调用和生成器模拟。由于imho,生成器表达式示例不适合您的问题。 您粘贴的示例代码无效。

通常你有一个这样的生成器:(在你的情况下调用模块而不是生成数字当然..) 从dabeaz略微修改的示例 更新:删除了明确的字节creatinon。

def genbytes():
    for i in range(100):
         yield i**2

你可能会用这样的东西来称呼它:

for newbyte in genbytes():
    print(newbyte)
    print(id(newbyte))
    input("press key...")

利用:

  

我必须假设发电机产生的每个元素都存在   是一个新创建的字节对象

我绝对会说是的。这就是收益率的方式。我们可以在上面看到。字节总是有一个新的id。

通常情况下这不应该是一个问题,因为你想逐个使用字节,然后将它收集到你建议使用bytearray

bytearray append之类的内容中

但即使产生了新的字节,我认为这至少不会立即在输入中产生33 MB并返回它们。

我从PEP 289添加了这个exerpt,其中指出了gen表达式和“function”样式的生成器的等价:

  

生成器表达式的语义等同于创建   匿名生成器函数并调用它。例如:

g = (x**2 for x in range(10))
print g.next()

相当于:

def __gen(exp):
    for x in exp:
        yield x**2
g = __gen(iter(range(10)))
print g.next()

所以bytes(gen)也会调用gen.next()(在这种情况下为yields整数,但在实际情况下可能是字节数),同时在gen上进行调整。

答案 2 :(得分:2)

从任意大小的可迭代对象创建不可变类型是 Python 的一项常见任务。最值得注意的是,tuple 是一种无处不在的不可变类型。这使得高效创建这样的实例成为必要——不断地创建和丢弃中间实例是没有效率的。

使用生成器作为 bytes 的源会相当快。但是,直接将数据作为固定的 tuplelist 提供会更快,而且 readable buffer 甚至可以直接复制到新的 bytes 对象。如果数据是作为生成器提供的,中间 tuplelistbytearray 将不会提供任何好处。


从可迭代对象创建固定大小实例的一般任务并不是不可变类型所独有的。即使是像 list 这样的可变类型在任何特定时间点都有固定的大小。在迭代期间通过 listing 元素填充 .append 时,Python 无法提前知道最终大小。
在这种情况下查看 list 的大小揭示了将任意大小的可迭代对象转换为特定大小的容器的一般策略:

>>> items = []
>>> for i in (i*2 for i in range(10)):
>>>     items.append(i)
>>>     print(i // 2, "\t", sys.getsizeof(items))
0    88
1    88
2    88
3    88
4    120
5    120
6    120
7    120
8    184
9    184

可以看出,Python 不会为每个项目增加 list。相反,当 list 太小时,会添加一整块空间,可以在再次需要调整大小之前容纳几个项目。

当出现任意大小的可迭代对象时,bytes 的简单实现将首先将其参数转换为 list,然后将内容复制到新的 bytes 实例。这已经只创建一个 bytes 实例和一个临时的辅助 list - 比为每个元素创建一个新实例效率更高。


但是,可以更进一步:bytes 是一个不可变整数的不可变序列,因此它不必保留其项目的身份:因为每个 bytes 的值item完全适合一个字节/C char,items可以直接写入内存。
CPython 3.9 implementation, constructing bytes from iterables 中有一些直接访问缓冲区的特殊情况,保证大小的 listtuple,以及用于any 其他不是字符串的可迭代对象。生成器的情况也属于后一种路径。

This generic path 使用 a struct as a primitive, low-level "list" for chars – 它使用 char 数组进行存储,并保存元数据,例如当前大小和最大大小。从迭代器/生成器中获取的 wach 项的字节值被简单地直接复制到 char 数组的当前末尾;如果 char 数组变得太小,它将被一个新的更大的 char 数组替换。一旦 iterable/generator 耗尽,char 数组可以直接用作新的 bytes 对象的内部存储。


在查看时间时,重要的部分是数据是否已经来自生成器,或者是否可以选择其他格式。

当已经有一个生成器时,直接将它提供给 bytes 是最快的——虽然不是很多。

%timeit bytes(i*2 % 256 for i in range(2**20))
107 ms ± 411 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit bytes(bytearray(i*2 % 256 for i in range(2**20)))
111 ms ± 946 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit bytes(tuple(i*2 % 256 for i in range(2**20)))
114 ms ± 1.13 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit bytes(list(i*2 % 256 for i in range(2**20)))
115 ms ± 741 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

但是,如果可以自由选择初始数据结构,list 会以中间存储为代价更快。

%timeit bytes([i*2 % 256 for i in range(2**20)])
94.5 ms ± 1.11 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

在 C/Cython 模块中直接创建数据时,提供 bytes/bytesarray 是迄今为止最快且开销低的。