Достижение согласованного размера блока в вводе-выводе необработанных файлов Python
Вопрос заранее:
Есть ли в стандартной библиотеке питонический способ разбора необработанных двоичных файлов с использованием
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
Итак, мой вопрос таков: есть ли еще питонический способ достижения
buffersize
-соблюдение поведения сырого файла, которое позволяет
for ... in ...
синтаксис, без подкласса
IOBase
или его дочерние классы (и, следовательно, не являющиеся частью стандартной библиотеки)? Если нет, оправдывает ли такое неожиданное поведение PEP? (Или нужно научиться ожидать такого поведения?:)
1 ответ
Такое поведение не является неожиданным, поскольку все объекты, производные от
IOBase
перебирать строки. Единственное, что меняется между двоичным и текстовым режимами, - это то, как определяется терминатор строки, он всегда определяется как
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 - довольно динамичный язык, и "утиная печать" - это название игры. Как правило, ваш первый инстинкт не должен быть "как создать подкласс некоторого встроенного типа для расширения функциональности". Я имею в виду, что часто это возможно, но вы обнаружите, что есть много языковых функций, направленных на то, чтобы этого не делать, и часто это просто лучше выражается таким образом для начала, по крайней мере, обычно для моих глаз.