14.03.2019       Выпуск 273 (11.03.2019 - 17.03.2019)       Статьи

История типизации на примере одного большого проекта

Эта история началась задолго до хайпа о typing в python3.5, более того, она началась внутри проекта, написанного еще на python2.7.

Я занимался проектом Partners в Ostrovok.ru – этот сервис отвечал за все, что связано с партнерскими интеграциями, бронированиями, статистикой, личным кабинетом. У нас использовались как внутренние API для других микросервисов компании, так и внешнее API для наших партнеров.

В какой-то момент в команде сформировался следующий подход к написанию обработчиков HTTP ручек или какой-либо бизнес логики

Читать>>




Экспериментальная функция:

Ниже вы видите текст статьи по ссылке. По нему можно быстро понять ссылка достойна прочтения или нет

Просим обратить внимание, что текст по ссылке и здесь может не совпадать.

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

Ostrovok.ru

.

Эта история началась задолго до хайпа о 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





Разместим вашу рекламу

Пиши: mail@pythondigest.ru

Нашли опечатку?

Выделите фрагмент и отправьте нажатием Ctrl+Enter.

Система Orphus