Yaroslav Bibaev

Субъективный блог

25 Oct 2020

Проверяем, что строка состоит из цифр на python

Ни так давно мне потребовалось проверить в одном месте, что строка состоит из цифр, к которым мы все так давно привыкли. На всякий случай напомню, как выглядят для меня всем привычные цифры. Вот так: 0123456789.

В памяти возникли какие-то воспоминания про str.isdigit(). «Ха, плевое дело!» – подумал я. Этим и воспользовался. Написал. Тесты проходят. Кажется, что все работает так, как нужно. Но как оказалось, что не все так просто…

В python, как в высокоуровневом языке, есть аж целых три метода для того, чтобы понять, что строка состоит из цифр: str.isdigit(), str.isnumeric() и str.isdecimal(). Подробнее, как это работает, разберем на примерах:

>>> '1'.isdigit()
True
>>> '𐩁'.isdigit()
True
>>> '٠'.isdigit()
True
>>> '⅕'.isdigit()
False
>>> '1a'.isdigit()
False
>>> '1'.isnumeric()
True
>>> '𐩁'.isnumeric()
True
>>> '٠'.isnumeric()
True
>>> '⅕'.isnumeric()
True
>>> '1a'.isnumeric()
False
>>> '1'.isdecimal()
True
>>> '𐩁'.isdecimal()
False
>>> '٠'.isdecimal()
True
>>> '⅕'.isdecimal()
False
>>> '1a'.isdecimal()
False

Да, казалось бы, что str.isdecimal() – это почти то, что нам нужно, но положительный результат с символом U+2155 – явно не то, что в действительности хотелось бы нам увидеть. Видимо, придется двигаться в пользу небольшого самописного варианта.

Начнем с самого простого, что приходит в голову: пытаемся сразу преобразовать строку к числу, а при невозможности операции предполагаем, что это не число. Вот как это должно примерно выглядеть:

>>> def is_digit(value: str) -> bool:
...     try:
...         int(value)
...     except ValueError:
...         return False
...     return True
...
>>> is_digit('1')
True
>>> is_digit('𐩁')
False
>>> is_digit('٠')
True
>>> is_digit('⅕')
False
>>> is_digit('1a')
False

Получилось то же самое, что и str.isdecimal(), только кода в разы больше. Ну что, теперь настало время регулярных выражений:

>>> import re
>>> def is_digit(value: str) -> bool:
...     return bool(re.match('^[0-9]+$', value))
...
>>> is_digit('1')
True
>>> is_digit('𐩁')
False
>>> is_digit('٠')
False
>>> is_digit('⅕')
False
>>> is_digit('1a')
False

Этот вариант – то, что нам нужно. В принципе, можно на нем остановиться. Единственное, что если вы испытываете к регулярным выражениям не самые теплые чувства, то можно переписать это без их использования:

>>> import string
>>> def is_digit(value: str) -> bool:
...     return bool(value and all(s in string.digits for s in value))
...
>>> is_digit('1')
True
>>> is_digit('𐩁')
False
>>> is_digit('٠')
False
>>> is_digit('⅕')
False
>>> is_digit('1a')
False

Стало ли лучше? Возможно. Конечно, можно заморочиться, сравнить оба метода по скорости и после этого выбрать то, что быстрее. Но это точно не цель этой заметки. Оставим это на откуп пытливым умам и юным оптимизаторам.

От себя скажу лишь то, что по возможности лучше испльзовать встроенные методы или, если они не подходят, то стартаться писать понятный код и уж только потом, когда не хватает скорости, пробовать это переписать на другой язык. На этом перестаю капитанить и до скорых встреч в следующих заметках. 😉