1

前面的问题:

标准库中是否有一种 Pythonic 方法可以使用for ... in ...语法(即/ )解析原始二进制文件,该语法(即__iter__/ __next__)产生尊重buffersize参数的块,无需子类IOBase或其子类?

详细解释

我想打开一个原始文件进行解析,利用for ... in ...语法,我希望该语法能够产生可预测的形状对象。对于我正在处理的问题,这并没有按预期发生,因此我尝试了以下测试(import numpy as np必需):

In [271]: with open('tinytest.dat', 'wb') as f:
     ...:     f.write(np.random.randint(0, 256, 16384, dtype=np.uint8).tobytes())
     ...:

In [272]: np.array([len(b) for b in open('tinytest.dat', 'rb', 16)])
Out[272]:
array([  13,  138,  196,  263,  719,   98,  476,    3,  266,   63,   51,
    241,  472,   75,  120,  137,   14,  342,  148,  399,  366,  360,
     41,    9,  141,  282,    7,  159,  341,  355,  470,  427,  214,
     42, 1095,   84,  284,  366,  117,  187,  188,   54,  611,  246,
    743,  194,   11,   38,  196, 1368,    4,   21,  442,  169,   22,
    207,  226,  227,  193,  677,  174,  110,  273,   52,  357])

我不明白为什么会出现这种随机行为,为什么不尊重这个buffersize论点。使用read1给出了预期的字节数:

In [273]: with open('tinytest.dat', 'rb', 16) as f:
     ...:     b = f.read1()
     ...:     print(len(b))
     ...:     print(b)
     ...:
16
b'M\xfb\xea\xc0X\xd4U%3\xad\xc9u\n\x0f8}'

它就是:第一个块末尾附近的换行符。

In [274]: with open('tinytest.dat', 'rb', 2048) as f:
     ...:     print(f.readline())
     ...:
b'M\xfb\xea\xc0X\xd4U%3\xad\xc9u\n'

果然,readline被调用以生成文件的每个块,并且它在换行值(对应于 10)上跳闸。我通过代码验证了这个阅读,IOBase 定义中的行

571    def __next__(self):
572    line = self.readline()
573    if not line:
574        raise StopIteration
575    return line

所以我的问题是这样的:是否有一些更 Pythonic 的方式来实现buffersize- 尊重原始文件行为,允许for ... in ...语法,不必子类IOBase或其子类(因此,不是标准库的一部分)?如果不是,这种意外行为是否需要 PEP?(或者它是否值得学习去期待这种行为?:)

4

1 回答 1

1

这种行为并不出人意料,据记录,所有派生自IOBaseiterate over lines的对象。二进制与文本模式之间唯一变化的是行终止符的定义方式,它始终定义为b"\n"二进制模式。

文档:_

IOBase(及其子类)支持迭代器协议,这意味着可以迭代 IOBase 对象以产生流中的行。行的定义略有不同,具体取决于流是二进制流(产生字节)还是文本流(产生字符串)。见readline()下文。

问题在于,过去在类型系统中文本和二进制数据之间存在歧义,这是 Python 2 -> 3 过渡破坏向后兼容性的主要动机因素。

我认为让迭代器协议尊重 Python 3 中以二进制模式打开的文件对象的缓冲区大小当然是合理的。为什么决定保留旧行为是我只能推测的。

在任何情况下,您都应该定义自己的迭代器,这在 Python 中很常见。迭代器是一个基本的构建块,就像内置类型一样。

您实际上可以使用 2 参数iter(callable, sentinel)形式来构造一个超级基本的包装器:

>>> from functools import partial
>>> def iter_blocks(f, n):
...     return iter(partial(f.read, n), b'')
...
>>> np.array([len(b) for b in iter_blocks(open('tinytest.dat', 'rb'), 16)])
array([16, 16, 16, ..., 16, 16, 16])

当然,您可以只使用生成器:

def iter_blocks(bin_file, n):
    result = bin_file.read(n)
    while result:
        yield result
        result = bin_file.read(n)

有很多方法可以解决这个问题。同样,迭代器是编写惯用 Python的核心类型。

Python 是一种非常动态的语言,“鸭子打字”是游戏的名称。通常,您的第一直觉不应该是“如何继承某些内置类型以扩展功能”。我的意思是,这通常是可能的,但你会发现有很多语言特性是为了不必这样做,而且通常情况下,最好以这种方式开始,至少通常在我看来.

于 2020-12-02T21:57:48.807 回答