L'histoire de la frappe sur l'exemple d'un grand projet

Bonjour à tous! Aujourd'hui, je vais vous raconter l'histoire du développement de la dactylographie sur l'exemple d'un des projets de Ostrovok.ru .



Cette histoire a commencé bien avant le battage médiatique sur python3.5 , d'ailleurs, elle a commencé dans un projet écrit en python2.7 .

2013 : tout récemment, il y avait une version de python3.3 , il n'y avait aucun intérêt à migrer vers la nouvelle version, car elle n'ajoutait aucune fonctionnalité spécifique, et il y aurait beaucoup de douleur et de souffrance pendant la transition.

J'ai été impliqué dans le projet Partners chez Ostrovok.ru - ce service était responsable de tout ce qui concernait les intégrations de partenaires, les réservations, les statistiques et mon compte personnel. Nous avons utilisé à la fois des API internes pour d'autres microservices de l'entreprise et une API externe pour nos partenaires.

À un moment donné, l'équipe a formé l'approche suivante pour écrire des gestionnaires HTTP ou une sorte de logique métier:

1) les données d'entrée et de sortie doivent être décrites par une structure (classe),
2) le contenu des instances de structures doit être validé conformément à la description,
3) une fonction qui prend une structure à l'entrée et donne la structure à la sortie devrait vérifier les types de données à l'entrée et à la sortie, respectivement.

Je ne m'attarderai pas sur chaque point en détail, l'exemple ci-dessous devrait suffire à comprendre ce qui est en jeu.

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


L'exemple utilise des bibliothèques: schémas et pycontracts .

* schémas - une façon de décrire et de valider les données.
* pycontracts - un moyen de vérifier l'entrée / sortie d'une fonction lors de l'exécution.

Cette approche vous permet de:

  • il est plus facile d'écrire des tests - les problèmes de validation ne se posent pas et seule la logique métier est couverte.
  • pour garantir le format et la qualité de la réponse dans l'API - un cadre rigide apparaît pour ce que nous sommes prêts à accepter et ce que nous pouvons donner.
  • il est plus facile de comprendre / refactoriser le format de réponse s'il s'agit d'une structure complexe avec différents niveaux d'imbrication.

Il est important de comprendre que la vérification de type (non-validation) ne fonctionne que lors de l' exécution , ce qui est pratique pour le développement local, l'exécution de tests dans CI et la vérification que la version candidate fonctionne dans un environnement intermédiaire . Dans un environnement de production, cela doit être désactivé, sinon le serveur ralentira.

Des années ont passé, notre projet a grandi, une logique métier plus nouvelle et plus complexe est apparue, le nombre de descripteurs d'API au moins n'a pas diminué.

À un moment donné, j'ai commencé à remarquer que le lancement du projet prenait déjà quelques secondes notables - c'était ennuyeux, car chaque fois que je modifiais le code et exécutais les tests, je devais m'asseoir et attendre longtemps. Lorsque cette attente a commencé à prendre 8 à 10 secondes, nous avons finalement décidé de comprendre ce qui se passait sous le capot.

En fait, tout s'est avéré assez simple. Lors du démarrage d'un projet, la bibliothèque pycontracts analyse toute la docstring couverte par @contract afin d'enregistrer toutes les structures en mémoire puis de les vérifier correctement. Lorsque le nombre de structures dans un projet s'élève à des milliers, tout cela commence à ralentir.

Que faire à ce sujet? La bonne réponse est de chercher d'autres solutions, heureusement dans la cour est déjà 2018 ( python3.5 - python3.6 ), et nous avons déjà migré notre projet vers python3.6 .

J'ai commencé à étudier des solutions alternatives et à réfléchir à la façon de migrer un projet de « pycontracts + description de type dans docstring » vers «quelque chose + description de type dans annotation de frappe ». Il s'est avéré que si vous mettez à niveau pycontracts vers la dernière version, vous pouvez décrire les types dans le style d' annotation de frappe , par exemple, cela pourrait ressembler à ceci:

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

Les problèmes commencent si vous devez utiliser des structures de frappe , par exemple Facultatif ou Union , car pycontracts ne sait PAS comment les utiliser:

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

J'ai commencé à chercher des bibliothèques alternatives pour la vérification de type lors de l' exécution :

* appliquer
* typeguard
* pytypes

Enforce à cette époque ne supportait pas python3.7 , mais nous l'avons déjà mis à jour, les pytypes n'aimaient pas la syntaxe, par conséquent, le choix s'est porté sur 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, ) ) 

Voici des exemples d'un vrai projet:

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

En conséquence, après un long processus de refactoring, nous avons pu transférer complètement le projet vers les annotations de typeguard + typing .

Quels résultats avons-nous obtenus:

  • Le projet démarre en 2-3 secondes, ce qui n'est au moins pas gênant.
  • la lisibilité du code s'est améliorée.
  • le projet est devenu plus petit tant dans le nombre de lignes que dans les fichiers, puisqu'il n'y a plus d'enregistrement de structure via @new_contract .
  • les IDE PyCharm intelligents sont devenus meilleurs pour indexer un projet et faire des conseils différents, car maintenant ce ne sont plus des commentaires, mais des importations honnêtes.
  • Vous pouvez utiliser des analyseurs statiques comme mypy et pyre -check , car ils prennent en charge le travail avec les annotations de frappe .
  • La communauté python dans son ensemble s'oriente vers la saisie sous une forme ou une autre, c'est-à-dire que les actions en cours sont des investissements dans l'avenir du projet.
  • il y a parfois des problèmes avec les importations cycliques, mais il y en a peu et ils peuvent être négligés.

J'espère que cet article vous sera utile!

Références:
* appliquer
* typeguard
* pytypes
* pycontracts
* schémas

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


All Articles