2.6. Testo e byte

Python ha due tipi di sequenza per i dati grezzi di caratteri:

  • str – una sequenza di codepoint Unicode. Usato per tutto il testo leggibile dall’uomo: percorsi di file, messaggi di log, payload JSON.

  • bytes – una sequenza di interi nell’intervallo 0 – 255. Usato per i dati binari grezzi: frame UART, buffer di immagini, pacchetti di rete, valori di registro.

Non possono essere mischiati senza una conversione esplicita. Passare uno str a un metodo hardware write solleva TypeError, e anche l’inverso viene rifiutato.

Uno str di codepoint Unicode a sinistra e una sequenza bytes di ottetti grezzi a destra, con frecce di encode e decode tra di loro.

Uno str memorizza caratteri Unicode; un bytes memorizza ottetti grezzi. Il passaggio tra di loro è la codifica (str → bytes) e la decodifica (bytes → str).

2.6.1. Letterali bytes

Un letterale bytes è un letterale simile a una stringa con il prefisso b:

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

All’interno di un letterale bytes sono consentiti direttamente solo caratteri ASCII; i valori non ASCII devono essere scritti come escape esadecimali \xHH.

2.6.2. Codifica e decodifica

  • str.encode() converte una stringa in bytes utilizzando una codifica con nome (predefinita "utf-8").

  • bytes.decode() esegue l’operazione inversa.

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

UTF-8 è la codifica predefinita e la scelta giusta per qualsiasi cosa possa contenere caratteri non ASCII. Usa "ascii" solo quando i dati sono garantiti come puro ASCII; in questo modo un byte non ASCII fuori posto solleva UnicodeError invece di passare silenziosamente.

2.6.3. Indicizzazione e slicing

Un valore bytes si comporta come una sequenza di interi quando viene indicizzato, non come una sequenza di stringhe di un byte:

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

Un errore comune è confrontare data[0] == "a" e sorprendersi che sia False: data[0] è un intero, non una stringa di un carattere, quindi i due valori non possono mai coincidere.

2.6.4. ord e chr – collegare caratteri e interi

Poiché l’indicizzazione di un bytes restituisce un intero ma il resto del programma probabilmente ragiona in termini di caratteri, Python fornisce due funzioni built-in per passare tra i due:

  • ord() – prende una stringa di un carattere e restituisce il suo codepoint intero.

  • chr() – l’inverso: dato un intero, restituisce la stringa di un carattere per quel codepoint.

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

Per i caratteri ASCII il codepoint è uguale al valore del byte, quindi ord("a") e b"a"[0] danno entrambi 97. Questo fa sì che i confronti di byte si leggano in termini del carattere a cui sei effettivamente interessato:

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

E chr() è comodo per il logging o il debugging quando vuoi vedere la forma stampabile di un byte:

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

Per i caratteri non ASCII ord() restituisce il codepoint Unicode, che non è uguale a nessun singolo byte nella forma codificata; la rappresentazione in byte dipende dalla codifica.

2.6.5. bytearray per buffer modificabili

bytes è immutabile: ogni «modifica» restituisce un nuovo oggetto e lascia intatto l’originale. Per i dati che intendi modificare, aggiungere o riempire pezzo per pezzo, usa bytearray. Contiene lo stesso contenuto di bytes ma supporta la mutazione sul posto:

>>> 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. Creare un bytearray

Il costruttore bytearray accetta diversi input:

  • bytearray(8) – un buffer di 8 byte a zero.

  • bytearray(b"hello") – una copia modificabile di un valore bytes.

  • bytearray("hello", "utf-8") – un bytearray da una stringa, utilizzando la codifica indicata.

  • bytearray([72, 73, 74]) – un bytearray da una sequenza di interi in 0 – 255 (qui, 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. Modificare un bytearray

L’assegnazione indicizzata e tramite slice funzionano esattamente come per 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')

I singoli byte devono essere interi in 0 – 255; assegnare qualsiasi altro tipo solleva TypeError o ValueError.

L’assegnazione tramite slice può modificare la lunghezza del buffer. Sostituire una slice con un valore più lungo fa crescere il bytearray; sostituire con un valore più corto lo restringe. Sostituire con b"" elimina completamente la slice:

>>> 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')

I metodi bytearray.append() e bytearray.extend() aggiungono byte alla fine senza riallocare ogni volta l’intero buffer:

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

2.6.5.3. Leggere da un bytearray

L’indicizzazione, lo slicing, l’iterazione e i metodi di ispezione di bytes (bytes.startswith(), bytes.find(), bytes.strip(), ecc.) funzionano tutti allo stesso modo che su un valore bytes. L’indicizzazione restituisce un intero; lo slicing restituisce un altro bytearray:

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

2.6.5.4. Conversione tra bytes e bytearray

bytes e bytearray si convertono l’uno nell’altro con i loro costruttori. Usa questa tecnica quando un’API richiede specificamente una delle due forme:

>>> 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 per slicing senza copia

Lo slicing di un bytes o di un bytearray normalmente copia i byte in un nuovo buffer. memoryview espone gli stessi byte senza copiarli:

>>> 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 su un bytearray è anche scrivibile: mutare la vista muta il buffer sottostante:

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

Ricorri a memoryview quando copiare una slice sarebbe uno spreco, tipicamente quando lo stesso buffer di grandi dimensioni viene passato in giro o elaborato a pezzi. Per il lavoro quotidiano in stile stringa su piccole quantità di byte, lo slicing semplice va benissimo.