На этом шаге мы закончим изучение этого вопроса.
Первый путь - написать класс, который просто представляет кусок (chunk) бинарных данных вместе с вспомогательной функцией для интерпретирования содержимого различными способами:
>>> class SizedRecord: def __init__(self, bytedata): self._buffer = memoryview(bytedata) @classmethod def from_file(cls, f, size_fmt, includes_size=True): sz_nbytes = struct.calcsize(size_fmt) sz_bytes = f.read(sz_nbytes) sz, = struct.unpack(size_fmt, sz_bytes) buf = f.read(sz - includes_size * sz_nbytes) return cls(buf) def iter_as(self, code): if isinstance(code, str): s = struct.Struct(code) for off in range(0, len(self._buffer), s.size): yield s.unpack_from(self._buffer, off) elif isinstance(code, StructureMeta): size = code.struct_size for off in range(0, len(self._buffer), size): data = self._buffer[off:off+size] yield code(data)
Метод класса SizedRecord.fmm_file() используется для чтения из файла куска (chunk) данных с префиксом, определяющим размер, что является обычным для многих форматов файлов. На вход он принимает код форматирования структуры, который содержит кодировку размера, который должен быть представлен в байтах. Необязательный аргумент includessize определяет, включает число байтов заголовок размера или нет. Вот пример того, как вы можете использовать этот код для прочтения отдельного многоугольника из файла с многоугольниками:
>>> f = open('polys.bin', 'rb') >>> phead = PolyHeader.from_file(f) >>> phead.num_polys 3 >>> polydata = [SizedRecord.from_file(f, '<i') for n in range(phead.num_polys)] >>> polydata [<__main__.SizedRecord object at 0x000001F2D9A4FFA0>, <__main__.SizedRecord object at 0x000001F2D9A286D0>, <__main__.SizedRecord object at 0x000001F2D9A58040>] >>>
Как показано выше, содержимое экземпляров SizeRecord пока еще не интерпретировано. Чтобы сделать это, используйте метод iter_as(), который принимает на вход код структуры формата или класс Structure. Это предоставляет вам немалую гибкость в том, как можно интерпретировать
>>> for n, poly in enumerate(polydata): print('Polygon', n) for p in poly.iter_as('<dd'): print(p) Polygon 0 (1.0, 2.5) (3.5, 4.0) (2.5, 1.5) Polygon 1 (7.0, 1.2) (5.1, 3.0) (0.5, 7.5) (0.8, 9.0) Polygon 2 (3.4, 6.3) (1.2, 0.5) (4.6, 9.2) >>>
>>> for n, poly in enumerate(polydata): print('Polygon', n) for p in poly.iter_as(Point): print(p.x, p.y) Polygon 0 1.0 2.5 3.5 4.0 2.5 1.5 Polygon 1 7.0 1.2 5.1 3.0 0.5 7.5 0.8 9.0 Polygon 2 3.4 6.3 1.2 0.5 4.6 9.2 >>>
Собирая все вместе, представим альтернативную реализацию функции read_ polys():
>>> class Point(Structure): _fields_ = [ ('<d', 'x'), ('d', 'y') ] >>> class PolyHeader(Structure): _fields_ = [ ('<i', 'file_code'), (Point, 'min'), (Point, 'max'), ('i', 'num_polys') ] >>> def read_polys(filename): polys = [] with open(filename, 'rb') as f: phead = PolyHeader.from_file(f) for n in range(phead.num_polys): rec = SizedRecord.from_file(f, '<i') poly = [(p.x, p.y) for p in rec.iter_as(Point)] polys.append(poly) return polys >>> polys = read_polys('polys.bin') >>> polys [[(1.0, 2.5), (3.5, 4.0), (2.5, 1.5)], [(7.0, 1.2), (5.1, 3.0), (0.5, 7.5), (0.8, 9.0)], [(3.4, 6.3), (1.2, 0.5), (4.6, 9.2)]] >>>
Этот рецепт предоставляет практическое применение различных продвинутых приемов программирования, в том числе дескрипторов, ленивых вычислений, метаклассов, переменных класса и представлений памяти (memoryviews). И все они служат очень четко определенной цели.
Главная идея этой реализации в том, что она полностью построена на идее ленивой распаковки. Когда экземпляр Structure создан, __init__() просто создает memoryview предоставленных байтовых данных и больше ничего не делает. В этот момент не производится распаковка или другие связанные со структурой операции. Причина использовать этот подход в том, что вы можете быть заинтересованы только в получении нескольких конкретных частей бинарной записи. Вместо распаковки файла целиком будут распакованы только участки, к которым осуществляется доступ.
Чтобы реализовать ленивую распаковку и упаковку значений, используется класс-дескриптор StructField. Каждый атрибут, который пользователь запишет в __fields__, конвертируется в дескриптор StructField, который сохраняет связанный код формата структуры и байтовый сдвиг в хранимый буфер. Метакласс StructureMeta - то, что автоматически создает эти дескрипторы при определении различных структурных классов. Главная причина использовать метакласс в том, что он очень сильно облегчает пользователю определение формата структуры, давая возможность высокоуровневого описания без необходимости волноваться о низкоуровневых деталях.
Тонкий момент использования метакласса StructureMeta в том, что он делает порядок байтов общим. Так что если любой атрибут определил порядок байтов (< для little endian или > для big endian), этот порядок будет применен ко всем последующим полям. Это помогает избежать излишнего ввода с клавиатуры, но также оставляет возможность переключиться на другой порядок в середине определения. Например, если у вас что- то сложное:
class ShapeFile(Structure): _fields_ = [('>i', 'file_code'), # Big endian ('20s', 'unused'), ('i', 'file_length'), ('<i', 'version'), # Little endian ('i', 'shape_type'), ('d', 'min_x'), ('d', 'min_y'), ('d', 'max_x'), ('d', 'max_y'), ('d', 'min_z'), ('d', 'max_z'), ('d', 'min_m'), ('d', 'max_m') ]
Как было отмечено, использование memoryview() в решении позволяет избавиться от копий в памяти. Когда структуры начинают вкладываться одна в другую, представления памяти (memoryviews) могут быть использованы для наложения разных частей определения структуры на одну и ту же область памяти. Этот аспект решения довольно тонкий, и он касается различий работы со срезами при использовании представлений памяти и обычных байтовых массивов. Если вы извлекаете срез из байтовой строки или массива, вы обычно получаете копию данных. А с представлением памяти это не так: срезы просто накладываются на существующую память. Поэтому этот подход эффективнее.
На следующем шаге мы рассмотрим суммирование данных и обсчет статистики.