Всем привет! Сегодня я расскажу вам историю развития типизации на примере одного из проектов в Ostrovok.ru.

Эта история началась задолго до хайпа о typing в python3.5, более того, она началась внутри проекта, написанного еще на python2.7.
2013 год: совсем недавно был релиз python3.3, мигрировать на новую версию смысла не было, так как каких-то конкретных фичей она не добавляла, а боли и страдания при переходе принесла бы очень много.
Я занимался проектом Partners в Ostrovok.ru – этот сервис отвечал за все, что связано с партнерскими интеграциями, бронированиями, статистикой, личным кабинетом. У нас использовались как внутренние API для других микросервисов компании, так и внешнее API для наших партнеров.
В какой-то момент в команде сформировался следующий подход к написанию обработчиков HTTP ручек или какой-либо бизнес логики:
1) данные на входе и на выходе должны быть описаны структурой (классом),
2) содержимое экземпляров структур должно быть провалидировано в соответствии с описанием,
3) функция, которая принимает структуру на входе и отдает структуру на выходе, должна проверять типы данных на входе и на выходе соответственно.
Не буду подробно останавливаться на каждом пункте, примера ниже должно хватить, чтобы понять, о чем идет речь.
В примере используются библиотеки: schematics и pycontracts.
* schematics — способ описывать и валидировать данные.
* pycontracts — способ проверять данные на входе/выходе функции в runtime.
Такой подход позволяет:
Важно понимать, что проверка типов (не валидация) работает только в runtime, и это удобно при локальной разработке, запуске тестов в CI и проверке работоспособности релиз кандидата в staging среде. В продакшн среде это необходимо отключать, иначе будет тормозить сервер.
Шли годы, наш проект рос, появлялось больше новой и сложной бизнес-логики, количество API ручек как минимум не уменьшалось.
В какой-то момент я стал замечать, что запуск проекта занимает уже заметные несколько секунд – это раздражало, поскольку каждый раз при редактировании кода и запуске тестов приходилось долгое время сидеть и ждать. Когда это ожидание стало занимать 8-10 секунд, мы решили наконец разобраться, что там творится под капотом.
На деле все оказалось довольно просто. Библиотека pycontracts при запуске проекта парсит все docstring, которые покрыты @contract, чтобы зарегистрировать в памяти все структуры и потом правильно их проверять. Когда количество структур в проекте исчисляется тысячами, вся эта штука начинает тормозить.
Что с этим делать? Правильный ответ – искать другие решения, к счастью на дворе уже 2018 год (python3.5-python3.6), да и свой проект мы уже мигрировали на python3.6.
Я стал изучать альтернативные решения и думать, как можно мигрировать проект с “pycontracts + описание типов в docstring” на “что-то + описание типов в typing annotation”. Оказалось, если обновить pycontracts до свежей версии, то можно описывать типы в typing annotation стиле, например, это может выглядеть так:
Проблемы начинаются в том случае, если нужно использовать структуры из typing, например Optional или Union, так как pycontracts НЕ умеет с ними работать:
Я начал искать альтернативные библиотеки для проверки типов в runtime:
* enforce
* typeguard
* pytypes
Enforce на тот момент не поддерживал python3.7, а мы уже обновились, pytypes не понравился синтаксисом, в итоге выбор пал на typeguard.
Вот примеры из реального проекта:
В итоге после долгого процесса рефакторинга нам удалось полностью перевести проект на typeguard + typing annotations.
Каких результатов мы достигли:
Надеюсь, эта cтатья будет вам полезна!
Ссылки:
* enforce
* typeguard
* pytypes
* pycontracts
* schematics

Эта история началась задолго до хайпа о typing в python3.5, более того, она началась внутри проекта, написанного еще на python2.7.
2013 год: совсем недавно был релиз python3.3, мигрировать на новую версию смысла не было, так как каких-то конкретных фичей она не добавляла, а боли и страдания при переходе принесла бы очень много.
Я занимался проектом Partners в Ostrovok.ru – этот сервис отвечал за все, что связано с партнерскими интеграциями, бронированиями, статистикой, личным кабинетом. У нас использовались как внутренние API для других микросервисов компании, так и внешнее API для наших партнеров.
В какой-то момент в команде сформировался следующий подход к написанию обработчиков HTTP ручек или какой-либо бизнес логики:
1) данные на входе и на выходе должны быть описаны структурой (классом),
2) содержимое экземпляров структур должно быть провалидировано в соответствии с описанием,
3) функция, которая принимает структуру на входе и отдает структуру на выходе, должна проверять типы данных на входе и на выходе соответственно.
Не буду подробно останавливаться на каждом пункте, примера ниже должно хватить, чтобы понять, о чем идет речь.
Пример
.
import datetime as dt from contracts import new_contract, contract from schematics.models import Model from schematics.types import IntType, DateType # in class OrderInfoData(Model): order_id = IntType(required=True) # out class OrderInfoResult(Model): order_id = IntType(required=True) checkin_at = DateType(required=True) checkout_at = DateType(required=True) cancelled_at = DateType(required=False) @new_contract def pyOrderInfoData(x): return isinstance(x, OrderInfoData) @new_contract def pyOrderInfoResult(x): return isinstance(x, OrderInfoResult) @contract def get_order_info(data_in): """ :type data_in: pyOrderInfoData :rtype: pyOrderInfoResult """ return OrderInfoResult( dict( order_id=data_in.order_id, checkin_at=dt.datetime.today(), checkout_at=dt.datetime.today() + dt.timedelta(days=1), cancelled_at=None, ) ) if __name__ == '__main__': data_in = OrderInfoData(dict(order_id=777)) data_out = get_order_info(data_in) print(data_out.to_native())
В примере используются библиотеки: schematics и pycontracts.
* schematics — способ описывать и валидировать данные.
* pycontracts — способ проверять данные на входе/выходе функции в runtime.
Такой подход позволяет:
- проще писать тесты – проблемы с валидацией не возникают, и покрывается только бизнес-логика.
- гарантировать формат и качество ответа в API – появляются жесткие рамки того, что мы готовы принять и что мы можем отдать.
- проще понимать/рефакторить формат ответа, если это сложная структура с разными уровнями вложенности.
Важно понимать, что проверка типов (не валидация) работает только в runtime, и это удобно при локальной разработке, запуске тестов в CI и проверке работоспособности релиз кандидата в staging среде. В продакшн среде это необходимо отключать, иначе будет тормозить сервер.
Шли годы, наш проект рос, появлялось больше новой и сложной бизнес-логики, количество API ручек как минимум не уменьшалось.
В какой-то момент я стал замечать, что запуск проекта занимает уже заметные несколько секунд – это раздражало, поскольку каждый раз при редактировании кода и запуске тестов приходилось долгое время сидеть и ждать. Когда это ожидание стало занимать 8-10 секунд, мы решили наконец разобраться, что там творится под капотом.
На деле все оказалось довольно просто. Библиотека pycontracts при запуске проекта парсит все docstring, которые покрыты @contract, чтобы зарегистрировать в памяти все структуры и потом правильно их проверять. Когда количество структур в проекте исчисляется тысячами, вся эта штука начинает тормозить.
Что с этим делать? Правильный ответ – искать другие решения, к счастью на дворе уже 2018 год (python3.5-python3.6), да и свой проект мы уже мигрировали на python3.6.
Я стал изучать альтернативные решения и думать, как можно мигрировать проект с “pycontracts + описание типов в docstring” на “что-то + описание типов в typing annotation”. Оказалось, если обновить pycontracts до свежей версии, то можно описывать типы в typing annotation стиле, например, это может выглядеть так:
@contract def get_order_info(data_in: OrderInfoData) -> OrderInfoResult: return OrderInfoResult( dict( order_id=data_in.order_id, checkin_at=dt.datetime.today(), checkout_at=dt.datetime.today() + dt.timedelta(days=1), cancelled_at=None, ) )
Проблемы начинаются в том случае, если нужно использовать структуры из typing, например Optional или Union, так как pycontracts НЕ умеет с ними работать:
from typing import Optional @contract def get_order_info(data_in: OrderInfoData) -> Optional[OrderInfoResult]: return OrderInfoResult( dict( order_id=data_in.order_id, checkin_at=dt.datetime.today(), checkout_at=dt.datetime.today() + dt.timedelta(days=1), cancelled_at=None, ) )
Я начал искать альтернативные библиотеки для проверки типов в runtime:
* enforce
* typeguard
* pytypes
Enforce на тот момент не поддерживал python3.7, а мы уже обновились, pytypes не понравился синтаксисом, в итоге выбор пал на typeguard.
from typeguard import typechecked @typechecked def get_order_info(data_in: OrderInfoData) -> Optional[OrderInfoResult]: return OrderInfoResult( dict( order_id=data_in.order_id, checkin_at=dt.datetime.today(), checkout_at=dt.datetime.today() + dt.timedelta(days=1), cancelled_at=None, ) )
Вот примеры из реального проекта:
@typechecked def view( request: HttpRequest, data_in: AffDeeplinkSerpIn, profile: Profile, contract: Contract, ) -> AffDeeplinkSerpOut: ... @typechecked def create_contract( user: Union[User, AnonymousUser], user_uid: Optional[str], params: RegistrationCreateSchemaIn, account_manager: Manager, support_manager: Manager, sales_manager: Optional[Manager], legal_entity: LegalEntity, partner: Partner, ) -> tuple: ... @typechecked def get_metaorder_ids_from_ordergroup_orders( orders: Tuple[OrderGroupOrdersIn, ...], contract: Contract ) -> list: ...
В итоге после долгого процесса рефакторинга нам удалось полностью перевести проект на typeguard + typing annotations.
Каких результатов мы достигли:
- проект запускается за 2-3 секунды, что как минимум не раздражает.
- повысилась читаемость кода.
- проект стал меньше как в количестве строк, так и в файлах, так как больше нет регистраций структур через @new_contract.
- умные IDE типа PyCharm стали лучше индексировать проект и делать разные подсказки, поскольку теперь это не комментарии, а честные импорты.
- можно использовать статические анализаторы вроде mypy и pyre-check, так как они поддерживают работу с typing annotations.
- python сообщество в целом движется в сторону типизации в том или ином виде, то есть текущие действия – это инвестиции в будущее проекта.
- иногда возникают проблемы с циклическими импортами, но их немного, и ими можно пренебречь.
Надеюсь, эта cтатья будет вам полезна!
Ссылки:
* enforce
* typeguard
* pytypes
* pycontracts
* schematics
