2.6. Texto vs bytes

Python tem dois tipos de sequência para dados de carácter em bruto:

  • str – uma sequência de pontos de código Unicode. Usada para todo o texto legível por humanos: caminhos de ficheiros, mensagens de registo, payloads JSON.

  • bytes – uma sequência de inteiros no intervalo 0 – 255. Usada para dados binários em bruto: tramas UART, buffers de imagem, pacotes de rede, valores de registos.

Não podem ser misturados sem uma conversão explícita. Passar uma str a um método de hardware write lança TypeError, e o inverso também é rejeitado.

A str of Unicode codepoints on the left and a bytes sequence of raw octets on the right, with encode and decode arrows between them.

Uma str armazena caracteres Unicode; uma bytes armazena octetos em bruto. Passar de uma para a outra é codificação (str → bytes) e descodificação (bytes → str).

2.6.1. Literais bytes

Um literal bytes é um literal semelhante a uma string prefixado com b:

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

Apenas caracteres ASCII são permitidos diretamente dentro de um literal bytes; valores não-ASCII têm de ser escritos como escapes hexadecimais \xHH.

2.6.2. Codificação e descodificação

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

UTF-8 é o padrão e a escolha certa para tudo o que possa conter caracteres não-ASCII. Use "ascii" apenas quando os dados são garantidamente ASCII puro; assim um byte não-ASCII inesperado lança UnicodeError em vez de passar silenciosamente.

2.6.3. Indexação e fatiamento

Um valor bytes comporta-se como uma sequência de inteiros quando indexado, não como uma sequência de strings de um byte:

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

Um erro comum é comparar data[0] == "a" e ficar surpreendido por ser Falsedata[0] é um inteiro, não uma string de um carácter, pelo que os dois valores nunca podem ser iguais.

2.6.4. ord e chr – ligação entre caracteres e inteiros

Como a indexação de bytes devolve um inteiro mas o resto do programa provavelmente pensa em termos de caracteres, Python disponibiliza duas funções internas para transitar entre eles:

  • ord() – recebe uma string de um único carácter e devolve o seu ponto de código inteiro.

  • chr() – o inverso: dado um inteiro, devolve a string de um único carácter para esse ponto de código.

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

Para caracteres ASCII o ponto de código é igual ao valor do byte, pelo que ord("a") e b"a"[0] dão ambos 97. Isso permite que as comparações de bytes sejam lidas em termos do carácter que realmente nos interessa:

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

E chr() é útil para registos ou depuração quando se pretende ver a forma imprimível de um byte:

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

Para caracteres não-ASCII, ord() devolve o ponto de código Unicode, que não é o mesmo que qualquer byte individual na forma codificada; a representação em bytes depende da codificação.

2.6.5. bytearray para buffers mutáveis

bytes é imutável – cada «modificação» devolve um novo objeto e deixa o original intacto. Para dados que pretende modificar, acrescentar, ou preencher parte a parte, use bytearray. Contém o mesmo conteúdo que bytes mas suporta mutação no local:

>>> 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. Criar um bytearray

O construtor bytearray aceita várias entradas:

  • bytearray(8) – um buffer de 8 bytes a zero.

  • bytearray(b"hello") – uma cópia mutável de um valor bytes.

  • bytearray("hello", "utf-8") – um bytearray a partir de uma string, usando a codificação indicada.

  • bytearray([72, 73, 74]) – um bytearray a partir de uma sequência de inteiros em 0 – 255 (aqui, 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 um bytearray

A atribuição por índice e por slice funciona como numa 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')

Os bytes individuais têm de ser inteiros em 0 – 255; atribuir qualquer outro tipo lança TypeError ou ValueError.

A atribuição por slice pode alterar o comprimento do buffer. Substituir um slice por um valor mais longo aumenta o bytearray; substituir por um valor mais curto reduz-o. Substituir por b"" elimina o slice inteiramente:

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

Os métodos bytearray.append() e bytearray.extend() adicionam bytes no final sem realocar o buffer inteiro a cada vez:

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

2.6.5.3. Leitura a partir de um bytearray

A indexação, o fatiamento, a iteração e os métodos de inspeção de bytes (bytes.startswith(), bytes.find(), bytes.strip(), etc.) funcionam todos da mesma forma que num valor bytes. A indexação devolve um inteiro; o fatiamento devolve outro bytearray:

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

2.6.5.4. Conversão entre bytes e bytearray

bytes e bytearray convertem-se mutuamente com os seus construtores. Use isto quando uma API exige especificamente uma das 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 fatiamento sem cópia

Fatiar uma bytes ou bytearray normalmente copia os bytes para um novo buffer. memoryview expõe os mesmos bytes sem copiar:

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

Uma vista sobre um bytearray é também gravável – mutar a vista muta o buffer subjacente:

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

Recorra a memoryview quando copiar um slice seria dispendioso – tipicamente quando o mesmo buffer grande é passado entre funções ou processado em partes. Para trabalho quotidiano no estilo de strings com bytes pequenos, o fatiamento simples é suficiente.