Источник: Nuances of Programming
Почему мы используем проверку типов?
Помощь типов внесла существенные изменения в систему нашей разработки платформы Tiqets. Среди очевидных преимуществ:
- Понижение когнитивной нагрузки при работе с кодом. Типы параметров и возвращаемых значений ясно указываются и проверяются. Никакого угадывания и сюрпризов.
- Раннее обнаружение ошибок. Если мы возвращаем неверный тип, забываем, что переменная может быть None или случайно объявляем какую-либо переменную повторно, модуль проверки типов сообщает об этом.
- Отсутствие тривиальных модульных тестов. Проверка типов позволяет избежать написания и поддержки банальных модульных тестов.
Ну и в целом можно отметить общее повышение качества разработки. Как и масштабирование, это является ключевым фактором, определяющим высокий темп роста технической команды Tiqet.
Как это выглядит?
Какие типы можно использовать?
Помимо простых типов (int, str, и т.д.) вы можете использовать следующие, которые определены в модуле typing (добавленном в Python 3.5):
- Коллекции: Tuple, Dict, MutableMapping, List, NamedTuple, etc.
- Компоновщики: Union, Optional
- Callable
- Обобщённые: TypeVar, Generic.
Как начать использовать типизацию в Python?
Начинать можно поэтапно: если функция не типизирована, она не будет подвергнута проверке типов.
Вот пошаговое руководство, которому мы следовали в Tiqets:
- Внесите минимум изменений, необходимых для выполнения mypy без ошибок.
- Добавьте шаг mypy в сборку CI и хуки pre-commit. С этого момента появления новых проблем с типами уже можно не бояться. Теперь всё будет только улучшаться.
- Добавляйте типизацию в весь новый код. Обучите вашу команду работе с типами и mypy.
- Постепенно добавляйте типы ко всему остальному коду.
Подводные камни типизации
Несмотря на все свои преимущества, проверка типов в Python далека от совершенства. Вот ряд учтённых нами опасностей, на которые стоит обращать внимание:
Ложное чувство безопасности. Модули проверки типов не будут перехватывать абсолютно все ошибки. Кроме того, типы не будут проверятся при выполнении, пока вы не начнёте использовать библиотеку вроде attrs или Pydantic. Рассматривайте проверку типов как дополнительный шаг безопасности, а не как её полное замещение.
Отсутствие оптимизации. Python не будет использовать знание типов для оптимизации вашего кода.
Нетипизированные библиотеки приведут к появлению ошибок типов. Если метод возвращает значение нетипизированной функции, вы должны вручную проверить, чтобы вызываемая функция возвращала именно заданный вами тип.
Определения типов могут становиться пугающими. Например, Dict[str, Union[str, Union[int, bool, Venue]]]. В данном случае мы советуем следующее: если у вас сложный тип, то, возможно, вы в нём ошиблись и нужно внести корректировки.
За гранью основ
Определяйте типы
Вы можете определить тип, используя TypeVar. В качестве примера, демонстрирующего полезность этого, можно привести функцию, возвращающую элемент из последовательности.
Использование типов в конструкторах classmethod
TypeVar также полезна при объявлении типов конструктора classmethod.
В примере ниже BaseModel.from_dict возвращает BaseModel, а в подклассе Product(BaseModel), Product.from_dict возвращает Product. Тип T должен быть либо BaseModel, либо его подклассом.
Наследование здесь выглядит так:
object ⤑ BaseModel ⤑ Product
Параметр bound='BaseModel' устанавливает BaseModel как верхнюю границу типа: он может быть BaseModel или его подклассом, но не может быть меньше, чем BaseModel (т.е. object).
Почему bound='BaseModel' вместо bound=BaseModel? Потому что, когда мы создавали TypeVar, BaseModel ещё не был определён. Не любите так делать? Мы тоже — поэтому можно рассмотреть включение отложенного вычисления аннотаций (см. PEP 563).
Пример ниже пройдёт проверку типов, но провалится при выполнении, т.к. при вызове TypeVar BaseModel должен быть уже определён. Это пример случая, в котором проверка типов не уловит проблему.
Обнаружение небезопасного ввода
Если вы получаете от пользователей небезопасные строки, то можете захотеть определить для них новый тип. Модуль проверки типов проверит, чтобы вы не отправили небезопасные строки тем функциям, которые принимают только безопасные.
Не просто типы: литералы
Типизация в Python относится не только к типам. Возьмём, к примеру open:
- В режиме "r" он будет считывать текст.
- В режиме "rb" он будет считывать байты.
Вы можете создать такую зависимость между значением параметра и типом, используя Literal.
Типизированные словари
Когда вам нужен типизированный словарь, подумайте, может лучше будет использовать класс данных. И всё же, начиная с версии Python 3.8, вы можете использовать типизированные словари.
Финальные классы, методы, атрибуты и переменные
В Python 3.8 вы можете определять классы, методы и переменные как финальные.
- У финального класса не может быть подкласса.
- Финальный метод не может быть перегружен.
- Финальная переменная не может быть переназначена.
Sphynx
Используйте sphynx-autodoc-typehints, чтобы позволить Sphynx применять определённые вами типы при генерации им документации.
Более ухищрённые определения типов
Как насчёт утиной типизации?
Утиную типизацию также можно подвергнуть проверке типов. Вы можете явно определить, как параметры вашей функции должны крякать, и модуль проверки типов убедится в том, что они делают это правильно.
Представьте функцию, закрывающую такие элементы, как соединения или файлы. Эта функция предполагает, что объект, переданный в качестве параметра, будет иметь метод close, не получающий никаких параметров и ничего не возвращающий. Вы можете сделать это предположение явным, определив Protocol.
Ниже мы создаём протокол Closeable: любой объект, который имеет метод close, не получающий параметров и ничего не возвращающий, является закрываемым. Такие объекты не знают о протоколе.
Обобщённые типы
Контейнерные классы также можно проверить на типы. Чтобы это сделать, мы должны определить новый тип, который будет представлять тип, содержащийся в классе. Ниже мы определяем для нашего контейнера тип T. Когда он будет содержать число, например Container(123), T будет int; когда же в нём будет строка, T будет str.
Разные возвращаемые типы
А что, если функция возвращает разные типы в зависимости от типа вводного параметра? В данном случае простой, но ошибочный подход выглядел бы так:
Несмотря на верность того, что double возвращает int или str, это не является исчерпывающей правдой: эта функция возвращает int, когда её вводный параметр int, и str, когда параметр str.
Правильный же способ определения типа double будет несколько многословен.
Решение сложностей при поиске типов
Модуль проверки типов будет искать тип в ближайшем пространстве имён. В последующем примере mypy подумает, что метод возвращает значения типа A.float, в то время, как на самом деле подразумевается возвращение им встроенного float.
Вам потребуется явно указать, что вы имеете в виду builtins.float.
Приведение типов
Если, несмотря на все ваши усилия, вы не можете добиться от модуля проверки верного вывода типа, можете утвердить его, используя cast.
Используйте cast с осторожностью: очевидно, что так можно с лёгкостью внести сложноуловимые баги.
Последний выход: игнорирование ошибок
Если больше ничто не работает, можете добавить к строке комментарий # type: ignore, который отключит для неё проверку типов.
Перспективы типизации в Python
Проверка типов в Python продолжает совершенствоваться. Вот некоторые из возможностей, ожидаемых в ближайшем будущем:
Унифицированные коды ошибок во всех модулях проверки типов. Это позволит инструментам, вроде редакторов с лёгкостью интерпретировать ошибки типизации, независимо от используемого модуля проверки.
Меньше верблюжьего регистра и импортов: использование list[] вместо typing.List[int].
Более сжатый синтаксис:
- int | float вместо Union[int, float]
- ?str = "" вместо Optional[str]
Какой модуль проверки мы используем в Tiqets?
Мы пользуемся mypy, который является эталонной реализацией и был лучшим инструментом на момент, когда мы начали использовать типизацию (тогда наша база кода имела ещё версию Python 2.7).
Существует также несколько интересных альтернативных реализаций:
- pyright (Microsoft) быстрее, но выполняется в node.js, а не в Python.
- pytype (Google) может выводить типы из неаннотированного кода. Если вам интересны подробности, ознакомьтесь с этим сравнением между mypy и pytype.
- pyre (Facebook) может выполнять инкрементную проверку.
Где следует добавлять типы?
Мы добавляем типы во всём коде.
Вы можете рассматривать их как определённый вид модульного тестирования. Они позволяют автоматически тестировать вводы и выводы кода. А так как они короче и находятся прямо в уточняемом ими коде, то и поддерживать их легче, чем модульные тесты.
Рассмотрите добавление типов везде, где бы вы добавляли модульные тесты.
Об авторе
Оскар Вилаплана является инженером ПО в Tiqets. Он ведёт технический блог этой компании и пишет в свободное время фантастику.
Читайте также:
Читайте нас в телеграмме и vk
Перевод статьи Òscar Vilaplana: Type Checking in Python.