2.29. Struct y datos binarios

El módulo struct empaqueta valores de Python en una disposición binaria de tamaño fijo y desempaqueta bytes de vuelta a valores de Python. Recurre a él cuando trabajes con un formato de archivo binario, un protocolo de red o un dispositivo que intercambia registros de tamaño fijo.

Dos funciones cubren la mayoría de los casos:

  • struct.pack() – toma valores de Python y una cadena de formato, y devuelve un objeto bytes con la disposición exacta.

  • struct.unpack() – toma una cadena de formato y un objeto bytes, y devuelve una tupla de valores de Python.

2.29.1. Cadenas de formato

Una cadena de formato enumera un código por cada campo del registro. Los códigos describen tanto el tamaño como la interpretación de cada campo.

El tipo int de Python no tiene un tamaño fijo: crece para acomodar cualquier valor que le asignes. Los formatos binarios tienen tamaños fijos: cada campo entero usa un número acordado de bytes. struct convierte entre los enteros de Python sin límite y estas representaciones de tamaño fijo.

El ancho de un entero es el número de bits que utiliza. Un byte son ocho bits. El código en minúscula es la variante con signo; el código en mayúscula es la variante sin signo (solo valores no negativos):

  • b / B8 bits (un byte). -128..127 con signo, 0..255 sin signo.

  • h / H16 bits (dos bytes). -32768..32767 con signo, 0..65535 sin signo.

  • i / I32 bits (cuatro bytes). Aproximadamente ±dos mil millones con signo, cuatro mil millones sin signo.

  • q / Q64 bits (ocho bytes). En la práctica, sin límite para el uso cotidiano.

Elige un ancho que cubra cómodamente el rango que esperas. Empaquetar un valor fuera del rango declarado o bien da la vuelta silenciosamente o bien lanza struct.error, según la compilación.

Los demás códigos comunes son para números de punto flotante y cadenas de bytes:

  • f – número de punto flotante de 32 bits (precisión simple; alrededor de siete dígitos decimales). El tipo float habitual de Python en MicroPython ya tiene este tamaño, por lo que empaquetar uno en f no causa pérdidas.

  • d – número de punto flotante de 64 bits (precisión doble; alrededor de quince dígitos decimales). Empaquetar un float de 32 bits de MicroPython en d lo amplía a ocho bytes, pero no añade precisión.

  • s – cadena de bytes de longitud fija, precedida por un recuento (8s para un campo de ocho bytes).

2.29.2. Orden de bytes

Un entero de varios bytes puede almacenarse en memoria de dos maneras. El número 0x12345678 en un campo de 32 bits se dispone así:

  • Little-endian – primero el byte menos significativo: 78 56 34 12.

  • Big-endian – primero el byte más significativo: 12 34 56 78.

Ambos codifican el mismo valor; solo difieren en qué extremo del campo contiene el byte bajo. Un archivo escrito por un sistema se lee corrupto en otro si el orden de bytes no coincide.

El primer carácter de la cadena de formato selecciona el orden:

  • < – little-endian. Común en x86 y ARM.

  • > – big-endian. Común en protocolos de red.

  • ! – orden de red, equivalente a >.

Sin un carácter inicial, se usan el orden de bytes nativo y la alineación nativa; establecer < o > explícitamente elimina esa ambigüedad y suele ser lo que quieres al leer un archivo o comunicarte con otra máquina.

Nota

La OpenMV Cam es little-endian, igual que su PC anfitrión. Usa < en las cadenas de formato para archivos locales de la cámara y para datos binarios que viajan hacia o desde un equipo de escritorio. Usa > (o !) para protocolos de red y para cualquier formato cuya especificación requiera big-endian.

Seis bytes dispuestos en una fila, con los dos primeros bytes agrupados como un campo "H" (16 bits sin signo) y los siguientes cuatro como un campo "I" (32 bits sin signo), cada uno etiquetado con su orden de bytes little-endian.

"<HI" empaqueta un valor de 16 bits seguido de un valor de 32 bits en seis bytes little-endian.

2.29.3. Empaquetado

import struct

blob = struct.pack("<HI", 320, 1000000)
print(blob, len(blob))

Salida:

b'@\x01@B\x0f\x00' 6

El formato <HI produce seis bytes: dos para el campo H y cuatro para el campo I, todos en little-endian. Pasa exactamente el número de valores que el formato espera; una discrepancia lanza struct.error.

2.29.4. Desempaquetado

width, count = struct.unpack("<HI", blob)
print(width, count)

Salida:

320 1000000

struct.unpack() siempre devuelve una tupla, incluso cuando el formato describe un único campo. Desempáquetala en la misma línea para mayor legibilidad.

2.29.5. Cadenas de bytes de longitud fija

El código s lee o escribe un bloque de bytes literalmente. El recuento va antes de la s: 4s significa «cuatro bytes tratados como una sola cadena de bytes». Esta es la manera habitual de incrustar un valor mágico, una etiqueta de tamaño fijo o un campo de nombre con relleno en un registro:

header = struct.pack("<4sHI", b"OMV0", 320, 1000000)
print(header)

Salida:

b'OMV0@\x01@B\x0f\x00'

Los primeros cuatro bytes son el valor mágico literal b"OMV0"; los dos siguientes son el campo H (320); los últimos cuatro son el campo I (1000000). El desempaquetado devuelve los bytes como un objeto bytes:

magic, width, count = struct.unpack("<4sHI", header)
print(magic, width, count)

Salida:

b'OMV0' 320 1000000

Si el valor de origen es más corto que el recuento declarado, el resultado se rellena por la derecha con \x00; si es más largo, los bytes sobrantes se descartan silenciosamente:

struct.pack("4s", b"hi")        # b'hi\x00\x00'
struct.pack("4s", b"toolong")   # b'tool'

El recuento es una longitud en bytes, no un número de caracteres: s trabaja con bytes en bruto, por lo que una cadena UTF-8 con caracteres de varios bytes debe primero pasarse por .encode() y contarse en bytes.

2.29.6. Tamaño y lecturas parciales

struct.calcsize() devuelve el número de bytes que consume una cadena de formato:

struct.calcsize("<HI")     # 6

Al leer un flujo de registros de un archivo, lee exactamente esa cantidad de bytes por registro:

record_size = struct.calcsize("<HI")
with open("data.bin", "rb") as f:
    while True:
        chunk = f.read(record_size)
        if len(chunk) < record_size:
            break
        width, count = struct.unpack("<HI", chunk)
        print(width, count)

Una lectura corta al final del archivo produce un bloque más pequeño que record_size; trátalo como la condición de fin de flujo en lugar de intentar desempaquetar un registro parcial.