2.6. Текст и байты¶
В Python есть два типа последовательностей для необработанных символьных данных:
str– последовательность кодовых точек Unicode. Используется для всего удобочитаемого текста: путей к файлам, сообщений журнала, полезной нагрузки JSON.bytes– последовательность целых чисел в диапазоне 0 – 255. Используется для необработанных двоичных данных: кадров UART, буферов изображений, сетевых пакетов, значений регистров.
Их нельзя смешивать без явного преобразования. Передача str в аппаратный метод write вызывает TypeError, и обратное также отклоняется.
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" и удивляться, что результат False – data[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 обычные срезы вполне подходят.