2025, Oct 15 13:48
int() и Unicode-цифры в Python: как строка ᪐᭒ становится 2
Разбираем, почему int() в Python принимает Unicode-цифры: как ᪐᭒ превращается в 2, что делает CPython при нормализации, и как проверить это через unicodedata.
При разборе строк в целые числа в Python большинство из нас исходит из того, что там только ASCII‑цифры. Но это допущение мгновенно рушится, как только встречаются цифры Unicode из других письменностей. Небольшой и неожиданный пример: преобразование строки ᪐᭒ с помощью int() возвращает 2. Визуально кажется, что в вводе нет цифр, однако Python без проблем выдаёт корректное число.
Как воспроизвести эффект
Последовательность ниже показывает поведение целиком: сырые байты, кодовые точки и результат int().
sample = '᪐᭒'
raw_bytes = bytes(sample, 'utf-8')
hex_points = [f'U+{ord(ch):04X}' for ch in sample]
value = int(sample)
print(raw_bytes)
print(hex_points)
print(value)
Этот код выведет байты UTF‑8, кодовые точки U+1A90 и U+1B52, а затем число 2.
Что происходит
Строка — не случайный набор символов. Оба знака являются десятичными цифрами Unicode. Первая, ᪐ (U+1A90), — Tai Tham Tham Digit Zero. Вторая, ᭒ (U+1B52), — Balinese Digit Two. Согласно спецификации Python для int(): «Значения 0–9 могут быть представлены любой десятичной цифрой Unicode». Иными словами, Python распознаёт цифры из класса Unicode Decimal Number, а не только ASCII 0–9.
Именно поэтому int('᪐᭒') фактически равносильно int('02') и даёт 2.
Как это устроено в CPython
Внутри CPython пропускает преобразование строки в число через процедуру, которая сперва нормализует ввод. На этом шаге любая кодовая точка с признаком десятичной цифры преобразуется в соответствующую ASCII‑цифру, пробелы приводятся к ASCII, а не‑ASCII символы, которые не являются ни цифрами, ни пробелами (начиная с первого такого символа), заменяются на ?. Лишь после такой нормализации выполняется собственно разбор в выбранной системе счисления.
Шаг сопоставления цифр можно приблизительно воспроизвести с помощью базы данных Unicode в Python:
import unicodedata as ud
example = '᪐᭒'
print([ud.digit(ch, "?") for ch in example])
Результат — 0 и 2, что показывает, как эти символы трактуются как десятичные цифры.
Сканирует ли int() все языки?
Нет, он не перебирает каждую систему счисления. Вместо этого каждый символ проверяется по его свойствам Unicode. Если символ классифицирован как десятичная цифра, его числовое значение известно и корректно сопоставляется. Если нет — он не считается десятичной цифрой. Это означает, что int() работает с десятичными цифрами в разных письменностях, но не справится с недесятичными системами чисел в Unicode.
Решение: скорректировать ожидания, а не парсер
Если вы видите, что int('᪐᭒') возвращает 2, а ожидали ошибку, стоит привести ожидания в соответствие с документированным поведением Python. Язык принимает любые десятичные цифры Unicode для значений 0–9. По сути, Python сначала переводит такие цифры в их ASCII‑аналоги, а затем разбирает результат. Если хотите увидеть такое сопоставление явно, быстрый просмотр через unicodedata, показанный выше, наглядно демонстрирует преобразование.
Для наглядности — короткая демонстрация пути от ввода к значению:
text = '᪐᭒'
print([f'U+{ord(c):04X}' for c in text])  # ['U+1A90', 'U+1B52']
print(int(text))                           # 2
Почему это важно для инженеров
Во-первых, корректность: код, который предполагает только ASCII‑цифры, поведёт себя иначе при Unicode‑вводе. Поведение Python намеренное и опирается на стандарты, что особенно важно при обработке международного текста.
Во-вторых, есть нюансы производительности. Проверка «цифровости» по всему диапазону Unicode опирается на метаданные Unicode и естественно медленнее, чем простой ASCII‑путь вроде 48 <= ord(c) < 58. Это одна из причин, по которой полноформатные Unicode‑строки в Python 3 могут быть медленнее логики, заточенной под ASCII. Вы получаете корректность для глобальной обработки текста — по цене, которая обычно оптимизируется внутри, но не сопоставима с чистым ASCII‑быстрым путём.
Выводы
int() в Python принимает любые десятичные цифры Unicode, а не только ASCII. Такие символы, как ᪐ и ᭒, — полноценные цифры в своих письменностях и при разборе отображаются в 0 и 2, так что int('᪐᭒') ведёт себя как int('02'). Интерпретатор не «ищет» все языки; он полагается на свойства Unicode, чтобы определить значения цифр. Для парсинга, который должен быть независим от письменности и корректен на международных данных, это плюс. Если же код обязан отвергать всё, кроме ASCII‑цифр, явно задавайте ограничения на ввод и выполняйте валидацию. Понимание этого поведения помогает избежать сюрпризов и в функциональности, и в производительности.
Статья основана на вопросе на StackOverflow от Wör Du Schnaffzig и ответе от jonrsharpe.