Строки глубже: Unicode, bytes и кодировки

⏱ 25 мин · 🧩 заданий: 5

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))
encode("utf-8") str ─────────────────▶ bytes (текст) ◀───────────────── (байты для файла / сети) decode("utf-8")

Правило «бутерброда»

Декодируйте байты в 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

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Один вариант ответа

Чему равно len("ё".encode("utf-8"))?

Задание 2 Программа на Python

Напишите функцию 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)
Задание 3 Программа на Python

Напишите функцию 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"
Задание 4 Один вариант ответа

Что произойдёт при b"abc" + "def"?

Задание 5 Несколько вариантов

Какие утверждения верны?

Войдите, чтобы отмечать пройденные уроки.