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

В Python есть два типа последовательностей для необработанных символьных данных:

  • str – последовательность кодовых точек Unicode. Используется для всего удобочитаемого текста: путей к файлам, сообщений журнала, полезной нагрузки JSON.

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

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

Строка str из кодовых точек Unicode слева и последовательность bytes из необработанных октетов справа, со стрелками encode и decode между ними.

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, должны записываться как шестнадцатеричные escape-последовательности \xHH.

2.6.2. Кодирование и декодирование

  • str.encode() преобразует строку в bytes, используя именованную кодировку (по умолчанию "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 обычные срезы вполне подходят.