Строки глубже: Unicode, bytes и кодировки
Unicode и кодовые точки
Unicode присваивает каждому символу номер — кодовую точку: A = U+0041, ж = U+0436, 😀 = U+1F600. В Python 3 str — последовательность кодовых точек.
for ch in "Aж€😀":
print(ch, ord(ch), hex(ord(ch)))
print(chr(1046), "ж", "\N{GREEK SMALL LETTER ALPHA}", "\U0001F600")
Кодировка: из символов в байты
В файлах и сети передаются байты. Кодировка определяет, как кодовые точки превращаются в байты. Почти везде используется UTF-8:
| Диапазон | Байт в UTF-8 | Примеры |
|---|---|---|
| U+0000–U+007F | 1 | латиница, цифры (совместимо с ASCII) |
| U+0080–U+07FF | 2 | кириллица, греческий |
| U+0800–U+FFFF | 3 | €, китайские иероглифы |
| U+10000 и выше | 4 | эмодзи |
for s in ["hi", "привет", "€", "😀"]:
b = s.encode("utf-8")
print(f"{s!r:10} символов: {len(s)} байт: {len(b)} {b}")
bytes и bytearray
data = b"hello" # литерал bytes — только ASCII
print(data[0], list(data), data.upper(), data.hex())
raw = "Москва".encode("utf-8")
print(raw)
print(raw.decode("utf-8")) # обратно в str
buf = bytearray(b"abc") # изменяемый вариант
buf[0] = ord("X")
buf.extend(b"!!")
print(buf, bytes(buf))
Правило «бутерброда»
Декодируйте байты в str как можно раньше (на входе: файл, сеть), работайте внутри программы только со str, кодируйте обратно как можно позже (на выходе). И всегда указывайте кодировку явно: open(path, encoding="utf-8").
Ошибки кодирования
raw = "Привет".encode("cp1251") # старая кодировка Windows
print(raw)
try:
raw.decode("utf-8")
except UnicodeDecodeError as e:
print("Ошибка:", e)
print(raw.decode("utf-8", errors="replace")) # � вместо непонятных байтов
print(raw.decode("cp1251")) # правильная кодировка
print("€".encode("ascii", errors="ignore"), "€".encode("ascii", errors="xmlcharrefreplace"))
«Кракозябры» (Привет) — классический результат декодирования UTF-8 как cp1251:
print("Привет".encode("utf-8").decode("cp1251"))
Нормализация
Один и тот же видимый символ можно закодировать по-разному:
import unicodedata
a = "é" # é — одна кодовая точка
b = "é" # e + комбинируемый акцент
print(a, b, a == b, len(a), len(b))
print(unicodedata.normalize("NFC", a) == unicodedata.normalize("NFC", b))
print(unicodedata.name("ж"), unicodedata.category("ж"))
Нормализуйте строки (NFC) перед сравнением и сохранением, если они приходят из разных источников (например, имена файлов в macOS хранятся в NFD).
Сравнение без учёта регистра
print("Straße".lower(), "Straße".casefold())
print("STRASSE".casefold() == "Straße".casefold())
casefold() — «агрессивный» lower() для сравнения строк на разных языках.
Как CPython хранит строки
С Python 3.3 (PEP 393) строка хранится компактно: 1, 2 или 4 байта на символ — по самому «широкому» символу в строке.
import sys
for s in ["a" * 100, "ж" * 100, "😀" * 100]:
print(f"{s[0]!r}×100 → {sys.getsizeof(s)} байт")
print(sys.getsizeof("a" * 100 + "😀")) # один эмодзи «расширяет» всю строку
Интернирование
Короткие строки, похожие на идентификаторы, CPython хранит в одном экземпляре — интернирует. Поэтому is иногда «работает» для строк, но полагаться на это нельзя:
a = "hello"
b = "hello"
print(a is b) # True — интернировано (деталь реализации)
c = "".join(["hel", "lo"])
print(a == c, a is c) # True False — создано во время выполнения
import sys
print(sys.intern(c) is a) # явное интернирование
Строки сравнивают через ==
is для строк — ошибка, даже если «на моей машине работает». Начиная с Python 3.8 сравнение литерала через is даже вызывает SyntaxWarning.
Итог
str— кодовые точки Unicode;ord/chr— номер ↔ символ.bytes— байты 0–255;encode/decodeпереводят между ними; кодировку указывайте явно (UTF-8).- UTF-8: 1–4 байта на символ, длина в байтах ≠ длине в символах.
- Нормализация
unicodedata.normalize("NFC", s)иcasefold()для корректного сравнения. - CPython хранит строки по 1/2/4 байта на символ; интернирование — деталь реализации, строки сравнивают через
==.
Практика решено 0 из 5
Чему равно len("ё".encode("utf-8"))?
Напишите функцию utf8_size(text), которая возвращает кортеж (символов, байт_в_utf8).
Какие тесты проверяют код (4)
assert utf8_size("abc") == (3, 3)
assert utf8_size("Привет") == (6, 12)
assert utf8_size("€") == (1, 3)
assert utf8_size("😀") == (1, 4)
def utf8_size(text):
return len(text), len(text.encode("utf-8"))
Напишите функцию caesar(text, shift) — шифр Цезаря для латинских строчных букв a–z:
каждая буква сдвигается на shift позиций по кругу, остальные символы не меняются.
Используйте ord и chr.
Какие тесты проверяют код (4)
assert caesar("abc", 1) == "bcd"
assert caesar("xyz", 3) == "abc"
assert caesar("hello, world!", 13) == "uryyb, jbeyq!"
assert caesar(caesar("python", 5), -5) == "python"
def caesar(text, shift):
result = []
for ch in text:
if "a" <= ch <= "z":
result.append(chr((ord(ch) - ord("a") + shift) % 26 + ord("a")))
else:
result.append(ch)
return "".join(result)
Что произойдёт при b"abc" + "def"?
Какие утверждения верны?
Войдите, чтобы отмечать пройденные уроки.