Всем привет! Сегодня я расскажу вам историю развития типизации на примере одного из проектов в 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
NerVik
вот этот момент очень интересует, в случае появления циклического импорта вы просто забиваете болт на типы, или как-то решаете проблему. Если второе, то расскажите как?
arxell Автор
пишем докстринг вида
typeguard в таком случае типы не проверяет
В нашем проекте все проблемы циклических импортов возникают, когда одна django модель импортирует другую.
mrShadow
Позвольте уточнить. А предусмотренный модулем typing forward reference в виде
в вашем случае не работает?arxell Автор
Это будет работать если внутри файла есть
но т.к. честный импорт сделать нельзя и сделать как вы выше описали, то будет
По крайне мере у меня так, я не смог эту проблему решить ((