Есть одна ловушка читаемости кода, которой легко избежать, если вы о ней знаете; тем не менее она встречается постоянно: это отсутствующие единицы измерения. Рассмотрим три фрагмента кода на Python, Java и Haskell:
time.sleep(300)
Thread.sleep(300)
threadDelay 300
Сколько «спят» эти программы? Программа на Python выполняет задержку на пять минут, программа на Java — на 0,3 секунды, а программа на Haskell — на 0,3 миллисекунды.
Как это можно понять из кода? А никак. Вам просто нужно знать, что аргументом
time.sleep
являются секунды, а threadDelay
— микросекунды. Если вы часто ищете эту информацию, то рано или поздно её запомните, но как сохранить читаемость кода для людей, никогда не встречавшихся с time.sleep
?Вариант 1: вставить единицу измерения в имя
Вместо этого:
def frobnicate(timeout: int) -> None:
...
frobnicate(300)
сделаем вот так:
def frobnicate(*, timeout_seconds: int) -> None:
# The * forces the caller to use named arguments
# for all arguments after the *.
...
frobnicate(timeout_seconds=300)
В первом случае мы даже не можем сказать в месте вызова, что 300 — это таймаут, но даже если бы мы это знали, то 300 чего? Миллисекунд? Секунд? Марсианских дней? И напротив, второй пример совершенно не требует объяснений.
Использование именованных аргументов — удобная возможность для языков, которые её поддерживают, но это не всегда возможно. Даже в Python, где
time.sleep
определяется с одним аргументом по имени secs
, мы не можем вызвать sleep(secs=300)
из-за особенностей реализации. В таком случае можно присвоить имя значению.Вместо этого:
time.sleep(300)
сделаем так:
sleep_seconds = 300
time.sleep(sleep_seconds)
Теперь в коде нет неоднозначностей, и он читаем даже без обращения к документации.
Вариант 2: использовать строгие типы
Вместо вставки единиц измерения в имя можно использовать более строгие типы, чем integer или float. Например, мы можем использовать тип duration.
Вместо этого:
def frobnicate(timeout: int) -> None:
...
frobnicate(300)
Сделаем вот так:
def frobnicate(timeout: timedelta) -> None:
...
timeout = timedelta(seconds=300)
frobnicate(timeout)
Чтобы иметь возможность интерпретировать единицу измерения заданного числа с плавающей запятой, необходимо как-то сообщить о ней. Если вам повезёт, то эта информация будет находиться в имени переменной или аргумента, но если не повезёт, то она будет указана лишь в документации, или не указана вовсе. Однако для значения
timedelta
нет неоднозначности интерпретаций, это часть типа. Кроме того, это устраняет неоднозначность из кода.Область применимости
Совет использовать строгие типы или вставлять единицы измерения в имена можно применять не только для переменных и аргументов функций, но и для API, имён метрик, форматов сериализации, файлов конфигураций, флагов командной строки и т. п. И хотя чаще всего единицы требуются для значений длительности, этот совет применим и к денежным величинам, длинам, размерам данных и т. п.
Например, возвращайте не такое:
{
"error_code": "E429",
"error_message": "Rate limit exceeded",
"retry_after": 100,
}
а такое:
{
"error_code": "E429",
"error_message": "Rate limit exceeded",
"retry_after_seconds": 100,
}
Не создавайте таких файлов конфигураций:
request_timeout = 10
лучше выберите один из этих вариантов:
request_timeout = 10s
request_timeout_seconds = 10
И не проектируйте бухгалтерское CLI-приложение таким образом:
show-transactions --minimum-amount 32
выберите один из этих вариантов:
show-transactions --minimum-amount-eur 32
show-transactions --minimum-amount "32 EUR"