2.6. Текст і байти

Python має два типи послідовностей для символьних даних:

  • str – послідовність кодових точок Unicode. Використовується для всього людиночитабельного тексту: шляхів до файлів, повідомлень журналу, JSON-даних.

  • bytes – послідовність цілих чисел у діапазоні 0 – 255. Використовується для сирих бінарних даних: кадрів UART, буферів зображень, мережевих пакетів, значень регістрів.

Їх не можна змішувати без явного перетворення. Передача str до апаратного методу write викидає TypeError, а зворотне також відхиляється.

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.

str зберігає символи Unicode; bytes зберігає сирі октети. Перехід між ними — це кодування (str → bytes) і декодування (bytes → str).

2.6.1. Літерали bytes

Літерал bytes — це рядкоподібний літерал із префіксом b:

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

Усередині літерала bytes допускаються лише символи ASCII; значення не-ASCII мають бути записані як шістнадцяткові послідовності \xHH.

2.6.2. Кодування та декодування

  • str.encode() перетворює рядок на байти, використовуючи вказане кодування (за замовчуванням "utf-8").

  • bytes.decode() виконує зворотню операцію.

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

UTF-8 є типовим і правильним вибором для всього, що може містити символи не-ASCII. Використовуйте "ascii" лише тоді, коли дані гарантовано є чистим ASCII; тоді несподіваний байт не-ASCII викине UnicodeError замість того, щоб мовчки пройти.

2.6.3. Індексація та зрізи

Значення bytes поводиться як послідовність цілих чисел при індексації, а не як послідовність однобайтових рядків:

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

Поширена помилка — порівняння data[0] == "a" і подив від того, що результат Falsedata[0] є цілим числом, а не однобуквеним рядком, тому ці два значення ніколи не збігаються.

2.6.4. ord і chr – перехід між символами та цілими числами

Оскільки індексація bytes повертає ціле число, а решта програми, швидше за все, оперує символами, Python надає два вбудовані засоби для переходу між ними:

  • ord() – приймає однобуквений рядок і повертає його цілочисельну кодову точку.

  • chr() – зворотна операція: за цілим числом повертає однобуквений рядок для цієї кодової точки.

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

Для символів ASCII кодова точка збігається зі значенням байта, тому ord("a") і b"a"[0] обидва дають 97. Це дозволяє читати порівняння байтів у термінах потрібного символу:

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

chr() зручний для журналювання або налагодження, коли потрібно побачити друкований вигляд байта:

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

Для символів не-ASCII ord() повертає кодову точку Unicode, яка не збігається з жодним окремим байтом у закодованому вигляді; байтове представлення залежить від кодування.

2.6.5. bytearray для змінюваних буферів

bytes є незмінним — кожна «зміна» повертає новий об’єкт і не зачіпає оригінал. Для даних, які ви плануєте змінювати, доповнювати або заповнювати частинами, використовуйте bytearray. Він містить той самий вміст, що й bytes, але підтримує зміну на місці:

>>> 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. Створення bytearray

Конструктор bytearray приймає кілька типів вхідних даних:

  • bytearray(8) – буфер із 8 нульових байтів.

  • bytearray(b"hello") – змінна копія значення bytes.

  • bytearray("hello", "utf-8") – bytearray із рядка з використанням вказаного кодування.

  • bytearray([72, 73, 74]) – bytearray із послідовності цілих чисел у діапазоні 0 – 255 (тут 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. Зміна bytearray

Індексоване та зрізне присвоєння працюють так само, як у 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')

Окремі байти мають бути цілими числами у діапазоні 0 – 255; призначення будь-якого іншого типу викидає TypeError або ValueError.

Присвоєння зрізу може змінити довжину буфера. Заміна зрізу довшим значенням збільшує bytearray; заміна коротшим зменшує. Заміна на b"" повністю видаляє зріз:

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

Методи bytearray.append() і bytearray.extend() додають байти в кінець без перерозподілу всього буфера щоразу:

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

2.6.5.3. Читання з bytearray

Індексація, зрізи, ітерація та методи перевірки bytes (bytes.startswith(), bytes.find(), bytes.strip() тощо) працюють так само, як і для bytes. Індексація повертає ціле число; зріз повертає інший bytearray:

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

2.6.5.4. Перетворення між bytes і bytearray

bytes і bytearray перетворюються одне на одне за допомогою конструкторів. Використовуйте це, коли API вимагає конкретно одну з форм:

>>> 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 для зрізів без копіювання

Зріз bytes або bytearray зазвичай копіює байти в новий буфер. memoryview надає доступ до тих самих байтів без копіювання:

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

Представлення поверх bytearray також є записуваним — зміна представлення змінює базовий буфер:

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

Вдавайтеся до memoryview, коли копіювання зрізу було б марнотратством — зазвичай, коли один великий буфер передається або обробляється по частинах. Для повсякденної роботи зі рядками на малих bytes звичайні зрізи цілком підходять.