2.6. Tekst a bajty

Python ma dwa typy sekwencyjne dla surowych danych znakowych:

  • str – sekwencja punktów kodowych Unicode. Używana do wszelkiego tekstu czytelnego dla człowieka: ścieżek plików, komunikatów dziennika, ładunków JSON.

  • bytes – sekwencja liczb całkowitych z zakresu od 0 do 255. Używana do surowych danych binarnych: ramek UART, buforów obrazu, pakietów sieciowych, wartości rejestrów.

Nie można ich mieszać bez jawnej konwersji. Przekazanie str do sprzętowej metody write zgłasza TypeError, a operacja odwrotna również jest odrzucana.

Po lewej str złożony z punktów kodowych Unicode, a po prawej sekwencja bytes z surowych oktetów, z strzałkami encode i decode pomiędzy nimi.

str przechowuje znaki Unicode; bytes przechowuje surowe oktety. Przejście między nimi to kodowanie (str → bytes) oraz dekodowanie (bytes → str).

2.6.1. Literały bytes

Literał bytes to literał podobny do ciągu znaków, poprzedzony znakiem b:

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

Wewnątrz literału bytes dozwolone są bezpośrednio tylko znaki ASCII; wartości spoza ASCII muszą być zapisane jako sekwencje szesnastkowe \xHH.

2.6.2. Kodowanie i dekodowanie

  • str.encode() konwertuje ciąg znaków na bytes przy użyciu nazwanego kodowania (domyślnie "utf-8").

  • bytes.decode() wykonuje operację odwrotną.

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

UTF-8 jest domyślnym i właściwym wyborem dla wszystkiego, co może zawierać znaki spoza ASCII. Używaj "ascii" tylko wtedy, gdy masz gwarancję, że dane są czystym ASCII; w ten sposób przypadkowy bajt spoza ASCII zgłosi UnicodeError zamiast po cichu przejść dalej.

2.6.3. Indeksowanie i wycinki

Wartość bytes przy indeksowaniu zachowuje się jak sekwencja liczb całkowitych, a nie jak sekwencja jednobajtowych ciągów:

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

Częstym błędem jest porównywanie data[0] == "a" i zdziwienie, że wynik to Falsedata[0] jest liczbą całkowitą, a nie jednoznakowym ciągiem, więc te dwie wartości nigdy nie mogą być równe.

2.6.4. ord i chr – pomost między znakami a liczbami całkowitymi

Ponieważ indeksowanie bytes zwraca liczbę całkowitą, podczas gdy reszta programu prawdopodobnie operuje na znakach, Python udostępnia dwie funkcje wbudowane do przechodzenia między nimi:

  • ord() – przyjmuje jednoznakowy ciąg i zwraca jego całkowity punkt kodowy.

  • chr() – operacja odwrotna: dla danej liczby całkowitej zwraca jednoznakowy ciąg odpowiadający temu punktowi kodowemu.

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

Dla znaków ASCII punkt kodowy jest równy wartości bajtu, więc zarówno ord("a"), jak i b"a"[0] dają 97. Dzięki temu porównania bajtów można zapisywać w odniesieniu do znaku, na którym faktycznie nam zależy:

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

Z kolei chr() przydaje się przy logowaniu lub debugowaniu, gdy chcesz zobaczyć drukowalną postać bajtu:

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

Dla znaków spoza ASCII ord() zwraca punkt kodowy Unicode, który nie jest tożsamy z żadnym pojedynczym bajtem w postaci zakodowanej; reprezentacja bajtowa zależy od kodowania.

2.6.5. bytearray dla zmiennych buforów

bytes jest niezmienny – każda „modyfikacja” zwraca nowy obiekt i pozostawia oryginał bez zmian. Do danych, które zamierzasz modyfikować, dopisywać lub wypełniać kawałek po kawałku, użyj bytearray. Przechowuje tę samą zawartość co bytes, ale obsługuje modyfikację w miejscu:

>>> 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. Tworzenie bytearray

Konstruktor bytearray przyjmuje kilka rodzajów danych wejściowych:

  • bytearray(8) – bufor 8 bajtów zerowych.

  • bytearray(b"hello") – modyfikowalna kopia wartości bytes.

  • bytearray("hello", "utf-8") – bytearray utworzony z ciągu znaków przy użyciu podanego kodowania.

  • bytearray([72, 73, 74]) – bytearray utworzony z sekwencji liczb całkowitych z zakresu od 0 do 255 (tutaj 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. Modyfikowanie bytearray

Przypisanie indeksowane i przez wycinki działa tak samo jak w przypadku 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')

Poszczególne bajty muszą być liczbami całkowitymi z zakresu od 0 do 255; przypisanie dowolnego innego typu zgłasza TypeError lub ValueError.

Przypisanie przez wycinek może zmienić długość bufora. Zastąpienie wycinka dłuższą wartością powiększa bytearray; zastąpienie krótszą wartością zmniejsza go. Zastąpienie wartością b"" całkowicie usuwa wycinek:

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

Metody bytearray.append() i bytearray.extend() dodają bajty na końcu bez każdorazowej realokacji całego bufora:

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

2.6.5.3. Odczyt z bytearray

Indeksowanie, wycinki, iteracja oraz metody inspekcji bytes (bytes.startswith(), bytes.find(), bytes.strip() itd.) działają tak samo jak na wartości bytes. Indeksowanie zwraca liczbę całkowitą; wycinek zwraca kolejny bytearray:

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

2.6.5.4. Konwersja między bytes a bytearray

bytes i bytearray konwertują się wzajemnie za pomocą swoich konstruktorów. Używaj tego, gdy API wymaga konkretnie jednej z postaci:

>>> 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 dla wycinków bez kopiowania

Wycinanie bytes lub bytearray zwykle kopiuje bajty do nowego bufora. memoryview udostępnia te same bajty bez kopiowania:

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

Widok na bytearray jest także zapisywalny – modyfikacja widoku modyfikuje bufor bazowy:

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

Sięgaj po memoryview, gdy kopiowanie wycinka byłoby marnotrawstwem – zazwyczaj wtedy, gdy ten sam duży bufor jest przekazywany dalej lub przetwarzany kawałkami. Do codziennej pracy w stylu ciągów znaków na małych bytes zwykłe wycinanie jest w zupełności wystarczające.