A história da digitação no exemplo de um grande projeto

Olá pessoal! Hoje vou contar a história do desenvolvimento da digitação no exemplo de um dos projetos no Ostrovok.ru .



Essa história começou muito antes do hype de digitação no python3.5 , além disso, começou dentro de um projeto escrito em python2.7 .

2013 : recentemente, houve o lançamento do python3.3 , não havia sentido em migrar para a nova versão, uma vez que não incluía nenhum recurso específico, e haveria muita dor e sofrimento durante a transição.

Eu participei do projeto Partners no Ostrovok.ru - este serviço foi responsável por tudo relacionado a integrações de parceiros, reservas, estatísticas e uma conta pessoal. Usamos APIs internas para outros microsserviços da empresa e uma API externa para nossos parceiros.

Em algum momento, a equipe formou a seguinte abordagem para escrever manipuladores HTTP ou algum tipo de lógica de negócios:

1) os dados de entrada e saída devem ser descritos por uma estrutura (classe),
2) o conteúdo das instâncias de estruturas deve ser validado de acordo com a descrição,
3) uma função que pega uma estrutura na entrada e fornece a estrutura na saída deve verificar os tipos de dados na entrada e na saída, respectivamente.

Não vou me debruçar sobre cada ponto em detalhes; o exemplo abaixo deve ser suficiente para entender o que está em jogo.

Exemplo
.
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()) 


O exemplo usa bibliotecas: esquemas e pycontracts .

* esquemas - uma maneira de descrever e validar dados.
pycontracts - uma maneira de verificar a entrada / saída de uma função em tempo de execução.

Essa abordagem permite:

  • é mais fácil escrever testes - problemas com a validação não surgem e apenas a lógica comercial é coberta.
  • para garantir o formato e a qualidade da resposta na API - uma estrutura rígida aparece para o que estamos prontos para aceitar e o que podemos oferecer.
  • é mais fácil entender / refatorar o formato de resposta se for uma estrutura complexa com diferentes níveis de aninhamento.

É importante entender que a verificação de tipo (não a validação) funciona apenas em tempo de execução , e isso é conveniente para o desenvolvimento local, executando testes no IC e verificando a liberação do candidato em um ambiente de preparação . Em um ambiente de produção, isso deve ser desativado, caso contrário, o servidor diminuirá a velocidade.

Anos se passaram, nosso projeto cresceu, mais lógica de negócios nova e complexa apareceu, o número de identificadores de API pelo menos não diminuiu.

Em algum momento, comecei a perceber que o lançamento do projeto já levava alguns segundos perceptíveis - isso era irritante, porque toda vez que eu editava o código e fazia os testes, eu precisava sentar e esperar por um longo tempo. Quando essa espera começou a levar de 8 a 10 segundos, finalmente decidimos descobrir o que estava acontecendo sob o capô.

De fato, tudo acabou sendo bastante simples. Ao iniciar um projeto, a biblioteca pycontracts analisa todas as doutrinas cobertas pelo @contract para registrar todas as estruturas na memória e depois verificá-las corretamente. Quando o número de estruturas em um projeto chega a milhares, tudo começa a diminuir.

O que fazer sobre isso? A resposta correta é procurar outras soluções, felizmente no quintal já é 2018 ( python3.5 - python3.6 ) e já migramos nosso projeto para python3.6 .

Comecei a estudar soluções alternativas e pensar em como migrar um projeto de " pycontracts + descrição do tipo na documentação " para "algo + descrição do tipo na anotação de digitação ". Descobriu-se que, se você atualizar pycontracts para a versão mais recente, poderá descrever os tipos no estilo de anotação de digitação , por exemplo, pode ser assim:

 @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, ) ) 

Os problemas começam se você precisar usar estruturas de digitação , por exemplo, Opcional ou União , pois os pycontracts NÃO sabem como trabalhar com eles:

 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, ) ) 

Comecei a procurar bibliotecas alternativas para verificação de tipo em tempo de execução :

* impor
* Typeguard
* pytypes

A aplicação naquele momento não suportava python3.7 , mas já atualizamos, os pytypes não gostaram da sintaxe, como resultado, a escolha recaiu sobre o 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, ) ) 

Aqui estão exemplos de um projeto real:

 @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: ... 

Como resultado, após um longo processo de refatoração, conseguimos transferir completamente o projeto para anotações de digitação com digitação +.

Que resultados alcançamos:

  • O projeto começa em 2-3 segundos, o que não é pelo menos irritante.
  • a legibilidade do código melhorou.
  • o projeto ficou menor tanto no número de linhas quanto nos arquivos, pois não há mais registros de estrutura via @new_contract .
  • os IDEs inteligentes do PyCharm tornaram-se melhores na indexação de um projeto e em sugestões diferentes, porque agora não são comentários, mas importações honestas.
  • Você pode usar analisadores estáticos como mypy e pyre-check , pois eles suportam o trabalho com anotações de digitação .
  • A comunidade python como um todo está se movendo para digitar de uma forma ou de outra, ou seja, as ações atuais são investimentos no futuro do projeto.
  • Às vezes, há problemas com importações cíclicas, mas existem poucas e podem ser negligenciadas.

Espero que este artigo seja útil para você!

Referências:
* impor
* Typeguard
* pytypes
* pycontracts
* esquemas

Source: https://habr.com/ru/post/pt443470/


All Articles