Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Чего вы не знали о строках в Python – Василий Р...

Чего вы не знали о строках в Python – Василий Рябов, PythoNN

Avatar for Sobolev Nikita

Sobolev Nikita

September 21, 2025
Tweet

More Decks by Sobolev Nikita

Other Decks in Programming

Transcript

  1. Чего вы не знали о строках в Python Василий Рябов

    Ведущий программист Positive Technologies
  2. Почему строки? Опять? • 2022-2024: работал над str в исследовательском

    интерпретаторе • Знаю о сторонних библиотеках (что можно улучшить в str) • Приколы о Юникоде, внутреннее представление str • Если внимательно читать доки… Почему не другие типы данных? • dict уже хорош (и нет идей): https://habr.com/ru/articles/432996/ • dict стал ordered в 3.6 (обнаружили и описали это в доках в 3.7) • set стал ordered в 3.11
  3. Стандарт Unicode (1/2) • У каждого символа (и у многих

    графем!) есть номер или codepoint • Диапазон: 0x0000 – 0x10FFFF (чаще пишут U+0000 – U+10FFFF) • Технически возможно 2^20+2^16−2^11 (1 112 064) символов как в UTF-16
  4. Стандарт Unicode (2/2) Стандарт содержит 2 части: • Universal Character

    Set (UCS): UCS-1, UCS-2 и UCS-4 (синхр. с ISO/IEC 10646) • Семейство кодировок: UTF-8, UTF-16 (BE и LE), UTF-32 (BE и LE) • UCS-4 == UTF-32 • UCS-2 ⸦ UTF-16, ибо каждая UTF-кодировка может покрыть весь UCS-4 • UCS-1 == ASCII ⸦ UTF-8 • UTF-7 не вошла в стандарт; есть первоапрельские UTF-9 и UTF-18 • Иногда говорят Юникод, подразумевают UTF-8 (но это не всегда так!)
  5. Стандарт Unicode: история • 1991: 16-битный Unicode 1.0 (7 161

    символ) • 1992: начата синхронизация с ISO/IEC 10646 • 1996: Unicode 2.0 (38 950 символов) технически мог > 2^16 • 2001: Unicode 3.1 (94 205 символов), появились первые символы > 2^16 • 2008: по данным Google UTF-8 стала самой популярной кодировкой (с выходом Unicode 5.1) • 2024: Unicode 16.0 (155 063 символа), UTF-8 используют 98,3% веб-сайтов
  6. Важные диапазоны символов (1/2) 17 плоскостей (planes) по 2^16 символов

    (разрывные диапазоны): • Плоскость 0 – Basic Multilingual Plane (BMP) • Текстовые символы большинства современных языков, упрощённые иероглифы • Плоскость 1 – Supplemental Multilingual Plane (SMP) • Плоскость 2 – Supplemental Ideographic Plane (SIP) • традиционные иероглифы • Плоскость 3 – Tertiary Ideographic Plane (TIP) • ещё более редкие и древние иероглифы • Плоскости 4-13 пока свободны • Плоскость 14 – Supplementary Special Purpose Plane (SSP) – смайлики и т.п. • Плоскости 15 и 16 – Private Use Planes
  7. Python: как узнать codepoint? (1/3) • и обратно • Windows

    11 консоль смогла это напечатать! • Из буфера обмена вставлялось '??' (май), но это починили
  8. Python: как узнать codepoint? (2/3) • и обратно а в

    Python 2.7.18 – старый Unicode 5.2 • Windows 11 консоль смогла это напечатать! • Из буфера обмена вставлялось '??' (май), но это починили
  9. Python: как узнать codepoint? (3/3) • и обратно а в

    Python 2.7.18 – старый Unicode 5.2 • Windows 11 консоль смогла это напечатать! • Из буфера обмена вставлялось '??' (май), но это починили
  10. BOM (Byte Order Mark) (1/3) В начале файла или представления

    в памяти • UTF-8: EF BB BF • UTF-16: FE FF (Big Endian) или FF FE (Little Endian) • UTF-32: 00 00 FE FF (Big Endian) или FF FE 00 00 (Little Endian)
  11. Кодировка UTF-16 • UTF-16 – двухбайтовая кодировка, переменной длины •

    UCS-4 символы представляются т.н. «суррогатной парой» (surrogate pair) или 2 code unit’а UTF-16 символы: • U+0000 – U+D7FF (простые символы, UCS-2) • U+D800 – U+DBFF (первые половинки суррогатных пар – high surrogate) • U+DC00 – U+DFFF (вторые половинки суррогатных пар – low surrogate) • U+E000 – U+FFFF («частное использование» / Private-Use) – никогда не будут определены, но можно договориться и юзать в своих приложениях
  12. Полпары нельзя (почти) • U+D800 – U+DBFF (первые половинки суррогатных

    пар) • U+DC00 – U+DFFF (вторые половинки суррогатных пар) • Некорректную строку создать можно! Похоже на баг. • И вообще, для str нет гарантии корректности!
  13. Баг ? • U+E000 – U+FFFF («частное использование» / Private-Use)

    – никогда не будут определены, но можно договориться и юзать в своих приложениях • Вероятно, не баг. Private Use == обычный символ, не суррогат
  14. Кодировка UTF-8 (1/3) • UTF-8 – однобайтовая кодировка, символы переменной

    длины • UCS-2 и UCS-4 символы представляются с помощью т.н. continuation bytes + финальный UTF-8 символы: • U+0000 – U+007F (== ASCII == UCS-1): длина в codepoint’ах == длина в байтах • Все остальные (2, 3 или 4 байта)
  15. Ладно, а как внутри Python? (2/5) Из чистого Python можно

    узнать, как str лежит в памяти (и сколько занимает)?
  16. Ладно, а как внутри Python? (3/5) • ASCII строки: точно

    не UTF-16 как в Java, C#, JS, Windows и mac OS
  17. Ладно, а как внутри Python? (5/5) • ASCII строки: точно

    не UTF-16 • UCS-2 строки: • UCS-2 + ASCII: точно не UTF-8
  18. Адаптивное представление • Файл ./cpython/include/cpython/unicodeobject.h • Latin-1 (U+00 – U+FF)

    != ASCII | UCS-1 (U+00 – U+7F) • Находится maxchar – это O(N) проход по всей строке!
  19. Крупные вехи в истории str Python 2.x • str –

    байтовая строка • 1999 (по копирайту) - 2002: тип unicode, только UCS-2 (== BMP) • 2.3 (2001): коммент с кодировкой .py файла Python 3.x • 3.0 (рокировка): str становится bytes, unicode становится str • 3.3: вернули префикс u для литералов (2/3 совместимость) • 3.3 (2010): текущее представление str (latin-1 | UCS-2 | UCS-4) • 3.6: добавлены f-строки (улучшались в 3.7, 3.8 и 3.12) • 3.12: компактнее на 8 байт для ASCII и на 16 байт для non-ASCII • 3.14: добавлены t-строки (отложенное форматирование)
  20. История методов str Python 3.x • 3.2: добавлен .format_map() •

    3.2: в метод .splitlines() добавлены разделители \v и \f • 3.7: метод .format() перестал быть thread safe! • из-за вызова setlocale() для float • 3.7: добавлен .isascii() • 3.9: добавлены .removeprefix() и .removesuffix() • 3.12: сняты большинство ограничений для f-строк • 3.13: метод .format() снова thread safe в no-GIL версии • были незначительные улучшения в форматировании
  21. А что кроме str? • Модуль string (cписки символов в

    виде строк): • string.ascii_letters • string.digits • string.whitespace • Функция string.capwords() полезнее str.title(): • Нет аналога str.istitle()! Идея уровня good first issue! • Классы string.Template и string.Formatter
  22. Где находится реализация str? (1/2) Файл ./Objects/unicodeobject.c содержит ~16 700

    LoC (lines of code) • Структура PyUnicodeObject и методы + атрибуты str • для всех трёх представлений • Модуль _string целиком там (он небольшой, но можно вынести) • Два разных итератора для str:
  23. Где находится реализация str? (2/2) Файл ./Objects/unicodeobject.c содержит ~16 700

    LoC (lines of code) • Структура PyUnicodeObject и методы + атрибуты str • для всех трёх представлений • Модуль _string целиком там (он небольшой, но можно вынести) • Два разных итератора для str: • Файл ./Objects/clinic/unicodeobject.c.h – почти 2k LoC • Там сгенерированные обёртки, обрабатывающие *args и **kwargs для методов • и вызовы unicode_rsplit_impl(self, sep, maxsplit); из unicodeobject.c
  24. И это ещё не всё! • Авто-генерированные файлы из файлов

    стандарта Unicode: • Скрипт ./Tools/unicode/makeunicodedata.py • генерирует ./Objects/unicodetype_db.h (6700+ строк) • это таблицы для методов типа .lower(), .upper() , .isdigit() и т.п. • Скрипт ./Tools/build/generate_global_objects.py • генерирует и ./Include/internal/pycore_unicodeobject_generated.h • это инициализация interned immortal static strings
  25. Не только символы? • Кроме символов, в Unicode есть комбинирующие

    метки! Например: • Диакритические знаки (умляуты, точки, крышки, перечеркивания (ять) и т.п.) • Ударения (несколько видов, в русском есть только «острое ударение») • … • Они меняют написание предыдущего символа! И имеют свой codepoint!
  26. Два написания одного символа (1/3) • Не все ёжики одинаковы!

    (спасибо Григорию Петрову за наводку)
  27. Два написания одного символа (3/3) • Не все ёжики одинаковы!

    • Как сравнивать «ёжиков»? • В стандарте Unicode есть нормализация! • Какой из ёжиков нормальный?
  28. Сделаем ёжика снова нормальным! (1/3) Стандартный модуль unicodedata, функция .normalize()

    • В стандарте – аж 4 алгоритма нормализации, и Python реализует их все: • NFD, NFC, NFKD, NFKC (указывать явно)
  29. Сделаем ёжика снова нормальным! (2/3) Стандартный модуль unicodedata, функция .normalize()

    • В стандарте – аж 4 алгоритма нормализации, и Python реализует их все: • NFD, NFC, NFKD, NFKC (указывать явно)
  30. Сделаем ёжика снова нормальным! (3/3) Стандартный модуль unicodedata, функция .normalize()

    • В стандарте – аж 4 алгоритма нормализации, и Python реализует их все: • NFD, NFC, NFKD, NFKC (указывать явно)
  31. Interned strings (1) Их три вида: immortal static, immortal и

    mortal • Смотри ./InternalDocs/string_interning.md • (1) Immortal static interned strings – статически аллоцированы (на стеке): • все ключевые слова, пустая строка, • все 1-символьные строки из “latin-1” (U+00 – U+FF) • имена для builtins переменных, функций и классов (например, исключений), … • Часть в ./Include/internal/pycore_unicodeobject_generated.h • В ./Objects/unicodeobject.c хранятся в словаре, но он их не чистит
  32. Interned strings (2) • Immortal (то есть без GC) –

    динамически аллоцированы (malloc), free в конце • Могут быть shared между sub-interpreters (в т.ч. унаследованы от main interp.) • Sub-interpreters стали публичными в 3.14 • https://habr.com/ru/articles/928054/ «PEP-734: Субинтерпретаторы в Python 3.14» (Космотекст?)
  33. Interned strings (3) • Mortal (с GC) – тоже динамически

    аллоцированы, и могут быть освобождены • например, константы в байт-коде • некоторые встроенные атрибуты у модулей и т.п. • Вызовы: _PyUnicode_InternMortal( и PyUnicode_InternInPlace( • Если хотите строку «интернировать» через C API: • not interned --> mortal --> immortal • Переход в mortal требует взять GIL, затем отпустить (на начало мая 2025)
  34. Парсер из .py файла в AST (1/2) • Юзает Python

    runtime (libpython.so), и больше всего – строки! • Удобно иметь libast.so и libpython.so отдельно (модульность!)
  35. Парсер из .py файла в AST (2/2) • Юзает Python

    runtime (libpython.so), и больше всего – строки! • Удобно иметь libast.so и libpython.so отдельно (модульность!)
  36. Идея уровня PEP: вынос libast (1/4) • Вынос методов str

    в чистую C библиотеку функций (без PyObject*) • Частично есть: ./Objects/stringlib/ (и нужны не все методы: 1-3) • find.h, join.h, … • asciilib.h, ucs1lib.h, ucs2lib.h, ucs4lib.h • Самое сложное: кодеки для # encoding: <кодировка>
  37. Идея уровня PEP: вынос libast (2/4) • Вынос методов str

    в чистую C библиотеку функций (без PyObject*) • Частично есть: ./Objects/stringlib/ (и нужны не все методы: 1-3) • find.h, join.h, … • asciilib.h, ucs1lib.h, ucs2lib.h, ucs4lib.h • В ./Objects/stringlib/codecs.h – только UTF-8, 16, 32 кодеки на C • Built-in кодеки – прямо в unicodeobject.c (~ 4,5 тыс. строк: ~3829-8385) • Обвязка для встроенных кодеков: ./Modules/_codecsmodule.c • Обвязка на C для кодеков азиатских (CJK) языков: ./Modules/cjkcodecs/ • Реестр кодеков: ./Python/codecs.c (можно регать свой на чистом Python)
  38. Идея уровня PEP: вынос libast (3/4) • Есть не-текстовые кодировки

    (base64 и т.п.) – в AST парсер их нельзя • Большинство кодировок – в пакете на чистом Python: ./Lib/encodings/ • всего: около 120 кодировок (почти все – текстовые) • 65 из них – авто-генерированные через ./Tools/unicode/gencodec.py • 2-4 коммита в год • последний раз новый кодек добавлялся в 2016 • => сконвертировать скриптом в Cишный код – технически несложно
  39. Идея уровня PEP: вынос libast (4/4) • Ещё одна сложность:

    найти реальные применения custom кодеков • Есть скрипты от Victor Stinner для поиска по пакетам на PyPI • https://hugovk.dev/blog/2022/how-to-search-5000-python-projects/ • Спасибо Михаилу Ефимову за ссылку выше • Tsche/magic_codec (35 звёзд) • pydong.org/posts/PythonsPreprocessor/ • Описать идею сначала на Discuss форуме или issue в python/cpython • Допустимо ли ограничить AST parser только Сишными кодировками? • Если нет, возможно ли иметь C-only реализацию парсера + его расширение с загрузкой CPython рантайма и добавлением Python-only кодеков? • … • При одобрении детально оформить PEP в виде PR в python/peps
  40. Где ещё в рантайме строки? (1/2) • AST Parser в

    памяти хранит строки в UTF-8 , исходные файлы чаще в UTF-8 • Компилятор сохраняет байткод в .pyc файл строго в UTF-8 • Байткод в памяти (PyCodeObject*) хранит str в адаптивном представлении • А может str внутри переделать на UTF-8?
  41. Где ещё в рантайме строки? (2/2) • AST Parser в

    памяти хранит строки в UTF-8 , исходные файлы чаще в UTF-8 • Компилятор сохраняет байткод в .pyc файл строго в UTF-8 • Байткод в памяти (PyCodeObject*) хранит str в адаптивном представлении • А может str внутри переделать на UTF-8? • faster-cpython/ideas/issues/684 Use UTF-8 internally for strings. (Mark Shannon) • Есть даже прототип Cишной структуры • Но активности пока мало • Может зацепить регулярные выражения (re и подмодули _sre на чистом C)
  42. Нюансы UTF-8 За UTF-8: • Не нужен encode/decode для 98+%

    веб-сайтов, AST и байткода • Компактность для смешанных строк (latin-1 + не latin-1) Против UTF-8: • Индексация – O(N) для UTF-8 вместо O(1) для адаптивного представления • нужна примерно в половине методов • Строки на CJK-языках в 1.5 раза больше, чем в UCS-2 Нейтрально: • O(N) при создании останется: вместо maxchar -> взять длину в codepoint’ах
  43. Что с производительностью? • Реализация строк в Python – строго

    последовательная • SIMD – Single Instruction Multiple Data (векторизация? её нет!) • AVX-2, AVX-512 в x86 • SSE3, SSE4 в ARM • Гибкая векторизация в RISC-V
  44. Сторонние библиотеки • StringZilla: ashvardanian/StringZilla – 2.6k звёзд (неясно, UTF-8

    ли внутри) • [+/-] векторизация (CPU SIMD extensions): x86 (есть AVX-512), ARM • [+] есть Python bindings (Rust, Swift и т.д.), половина методов + доп. методы • simdutf: simdutf/simdutf – 1.4k звёзд • [+] векторизация (CPU SIMD extensions): x86, ARM, RISC-V • [-] нет Python bindings вообще • [+/-] UTF-8 и transcoding в/из UTF-16 и UTF-32, отдельно “base64” (не для строк) • [+] есть модульность (generic / vectorized реализации отделены) • В «проде» у многих JS-related проектов: Chromium, Webkit, Bun (JS JIT), … • [-] обе внутри на C++ • Лицензии: Apache 2.0 / BSD 3-clause и Apache 2.0 / MIT
  45. Варианты интеграции в Python • Внешняя зависимость в ./externals/ (уже

    9 зависимостей есть) • не вариант скорее всего • Переписать на чистом C • больше работы, зато развивает))
  46. Реализация UTF-8 в Mojo • Влита в PR: modular/modular/pull/3401 •

    Написана на чистом Mojo (самая читабельная) • Есть векторизация (встроена в язык) • По неидентифицированным бенчмаркам быстрее fastvalidate-utf8 (13.16 GB/s) • Можно сравнить с другими только на Linux (где Mojo доступен)
  47. Мало бенчмарков и сравнений • StringZilla vs simdutf vs Mojo

    (нет сравнений) • по функциональности (StringZilla должна победить, в 4.0 будет быстрый hashing!) • performance на тех же бенчмарках (сделать общие хотя бы микро-бенчмарки) • по совместимости с набором платформ (simdutf должен победить) • В наборе pyperformance не видно фокуса на строках • Нужны real world бенчмарки: • Есть HuggingFace tokenizers (для ML / DS очень хайповая вещь) • Микробенчмарки: • Датасет lemire/unicode_lipsum • Датасет wikipedia_mars (есть в simdutf)
  48. А ещё нужно готовить железо • Для максимально стабильных результатов

    performance тестов нужно: • Выровнять частоту ядер (выкл. турбо буст и т.п.) • Выделить пару ядер под систему (на Linux), остальные – под user процессы • У NVIDIA есть хорошие гайды по настройке CPU и I/O: • Rivermax Windows Performance Tuning Guide • Rivermax Linux Performance Tuning Guide
  49. Поиск подстроки (find, rfind) • Алгоритм Кнута-Морриса-Пратта (KMP) устарел •

    В Python – комбинация Boyer-Moore (BM) и Boyer-Moore-Horspool (BMH) • Разбор алгоритмов с кодом • Либо вызов из стандартной библиотеки C memmem • Сравнение реализаций memmem (by Simone Faro, март 2025): • https://rurban.github.io/smart/results/best20/englishTexts.html • Мерили через rurban/smart (Reini Urban, автор safeclib) • Самый быстрый алгоритм на большинстве размеров: • EPSM - SSE4 Exact Packed String Matching
  50. Идеи для улучшения • Переход строк на UTF-8 (PEP), векторизация

    строк (PEP) • Перенос кодировок в чистый C (PEP) • Отделение libast.so от libpython.so (PEP, т.к. затронет C API) • Объединение символа с графемой по смыслу (?) • Функция string.iscapwords() • Make .format() thread safe w/GIL! • Адаптировать DTOA из Swift или реализовать алгоритм Ryu • Векторизация CSV, см. C# пакет Sep • “base64” codec из simdutf