2.6. Texto frente a bytes

Python tiene dos tipos de secuencia para datos de caracteres en bruto:

  • str – una secuencia de puntos de código Unicode. Se usa para todo el texto legible por humanos: rutas de archivo, mensajes de registro, cargas útiles JSON.

  • bytes – una secuencia de enteros en el rango 0 – 255. Se usa para datos binarios en bruto: tramas UART, búferes de imagen, paquetes de red, valores de registro.

No se pueden mezclar sin una conversión explícita. Pasar un str a un método write de hardware lanza TypeError, y lo inverso también se rechaza.

Un str de puntos de código Unicode a la izquierda y una secuencia de bytes de octetos en bruto a la derecha, con flechas de codificación y decodificación entre ambos.

Un str almacena caracteres Unicode; un bytes almacena octetos en bruto. Pasar de uno a otro es codificar (str → bytes) y decodificar (bytes → str).

2.6.1. literales de bytes

Un literal de bytes es un literal con forma de cadena precedido del prefijo b:

header  = b"OMV"
crlf    = b"\r\n"
payload = b"\x01\x02\x03"

Dentro de un literal de bytes solo se permiten caracteres ASCII directamente; los valores no ASCII deben escribirse como escapes hexadecimales \xHH.

2.6.2. Codificación y decodificación

  • str.encode() convierte una cadena en bytes usando una codificación con nombre (por defecto "utf-8").

  • bytes.decode() hace lo inverso.

>>> "hello".encode()
b'hello'
>>> "héllo".encode()
b'h\xc3\xa9llo'              # é is two bytes in UTF-8
>>> b"hello".decode()
'hello'

UTF-8 es el valor por defecto y la opción correcta para cualquier cosa que pueda contener caracteres no ASCII. Usa "ascii" solo cuando se garantice que los datos son ASCII puro; de esa forma un byte no ASCII descarriado lanza UnicodeError en lugar de pasar inadvertido.

2.6.3. Indexación y segmentación

Un valor de bytes se comporta como una secuencia de enteros cuando se indexa, no como una secuencia de cadenas de un byte:

>>> data = b"abc"
>>> data[0]
97                           # the int 97, not 'a'
>>> data[0:1]
b'a'                         # slicing returns bytes

Un error habitual es comparar data[0] == "a" y sorprenderse de que sea False: data[0] es un entero, no una cadena de un solo carácter, así que los dos valores nunca pueden coincidir.

2.6.4. ord y chr – puente entre caracteres y enteros

Dado que indexar un bytes devuelve un entero pero el resto del programa probablemente razona en términos de caracteres, Python proporciona dos funciones integradas para moverse entre ambos:

  • ord() – toma una cadena de un solo carácter y devuelve su punto de código entero.

  • chr() – lo inverso: dado un entero, devuelve la cadena de un solo carácter de ese punto de código.

>>> ord("a")
97
>>> chr(97)
'a'
>>> ord("A"), chr(0x41)
(65, 'A')

Para los caracteres ASCII el punto de código coincide con el valor del byte, por lo que ord("a") y b"a"[0] dan ambos 97. Eso hace que las comparaciones de bytes se lean en términos del carácter que realmente te interesa:

>>> data = b"abc"
>>> data[0] == ord("a")          # instead of the magic number 97
True

Y chr() resulta útil para registrar o depurar cuando quieres ver la forma imprimible de un byte:

>>> chr(data[0])
'a'

Para los caracteres no ASCII, ord() devuelve el punto de código Unicode, que no es lo mismo que ningún byte individual de la forma codificada; la representación en bytes depende de la codificación.

2.6.5. bytearray para búferes mutables

bytes es inmutable: cada «modificación» devuelve un objeto nuevo y deja el original intacto. Para datos que pretendas modificar, ampliar o rellenar pieza a pieza, usa bytearray. Contiene el mismo contenido que bytes pero admite la mutación en el sitio:

>>> s = b"hello"
>>> s[0] = ord("H")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'bytes' object does not support item assignment

>>> s = bytearray(b"hello")
>>> s[0] = ord("H")
>>> s
bytearray(b'Hello')

2.6.5.1. Crear un bytearray

El constructor de bytearray acepta varias entradas:

  • bytearray(8) – un búfer de 8 bytes a cero.

  • bytearray(b"hello") – una copia mutable de un valor de bytes.

  • bytearray("hello", "utf-8") – un bytearray a partir de una cadena, usando la codificación indicada.

  • bytearray([72, 73, 74]) – un bytearray a partir de una secuencia de enteros en 0 – 255 (aquí, b"HIJ").

>>> bytearray(4)
bytearray(b'\x00\x00\x00\x00')
>>> bytearray(b"abc")
bytearray(b'abc')
>>> bytearray("café", "utf-8")
bytearray(b'caf\xc3\xa9')

2.6.5.2. Modificar un bytearray

La asignación indexada y por porciones funciona igual que en una list:

>>> buf = bytearray(8)        # 8 zero bytes
>>> buf[0] = 0xFF             # one byte at a time
>>> buf[1:4] = b"ABC"         # replace a slice
>>> buf
bytearray(b'\xffABC\x00\x00\x00\x00')

Los bytes individuales deben ser enteros en 0 – 255; asignar cualquier otro tipo lanza TypeError o ValueError.

La asignación por porciones puede cambiar la longitud del búfer. Reemplazar una porción con un valor más largo hace crecer el bytearray; reemplazar con uno más corto lo encoge. Reemplazar con b"" elimina la porción por completo:

>>> buf = bytearray(b"abcdef")
>>> buf[1:3] = b"XYZ"         # 2 bytes replaced with 3
>>> buf
bytearray(b'aXYZdef')
>>> buf[1:4] = b""            # delete the inserted run
>>> buf
bytearray(b'adef')

Los métodos bytearray.append() y bytearray.extend() añaden bytes al final sin tener que reasignar todo el búfer cada vez:

>>> buf = bytearray()
>>> buf.append(0x01)
>>> buf.extend(b"abc")
>>> buf
bytearray(b'\x01abc')

2.6.5.3. Leer de un bytearray

La indexación, la segmentación, la iteración y los métodos de inspección de bytes (bytes.startswith(), bytes.find(), bytes.strip(), etc.) funcionan todos igual que en un valor bytes. La indexación devuelve un entero; la segmentación devuelve otro bytearray:

>>> buf = bytearray(b"OpenMV")
>>> buf[0]
79
>>> buf[0:4]
bytearray(b'Open')
>>> buf.startswith(b"Open")
True

2.6.5.4. Conversión entre bytes y bytearray

bytes y bytearray se convierten entre sí mediante sus constructores. Usa esto cuando una API requiera específicamente una de las dos formas:

>>> ba = bytearray(b"hello")
>>> snapshot = bytes(ba)      # immutable copy
>>> ba[0] = ord("H")
>>> ba, snapshot
(bytearray(b'Hello'), b'hello')

2.6.5.5. memoryview para segmentación sin copia

Segmentar un bytes o un bytearray normalmente copia los bytes en un búfer nuevo. memoryview expone los mismos bytes sin copiarlos:

>>> buf = bytearray(b"OpenMV Cam")
>>> view = memoryview(buf)
>>> view[0:6]                 # shares storage with buf
<memoryview ...>
>>> bytes(view[0:6])          # materialise as bytes when needed
b'OpenMV'

Una vista sobre un bytearray también permite escritura: mutar la vista muta el búfer subyacente:

>>> view[0] = ord("o")
>>> buf
bytearray(b'openMV Cam')

Recurre a memoryview cuando copiar una porción sería un desperdicio, normalmente cuando el mismo búfer grande se pasa de un lado a otro o se procesa por partes. Para el trabajo cotidiano con cadenas de bytes pequeñas, la segmentación normal es suficiente.