Utilisation de modules stricts dans des projets Python à grande échelle: expérience Instagram. 2e partie

Nous présentons à votre attention la deuxième partie de la traduction du matériel consacré aux fonctionnalités de travail avec les modules dans les projets Instagram Python. La première partie de la traduction donne un aperçu de la situation et montre deux problèmes. L'un d'eux concerne le démarrage lent du serveur, le second - les effets secondaires des commandes d'importation non sécurisées. Aujourd'hui, cette conversation va se poursuivre. Nous examinerons un autre problème et parlerons d'approches pour résoudre tous les problèmes soulevés.



Problème 3: statut mondial mutable


Jetez un œil à une autre catégorie d'erreurs courantes.

def myview(request):     SomeClass.id = request.GET.get("id") 

Nous voici dans la fonction de présentation et attachons l'attribut à une certaine classe en fonction des données reçues de la demande. Vous avez probablement déjà compris l'essence du problème. Le fait est que les classes sont des singletones globaux. Et ici, nous mettons l'état, selon la demande, dans un objet à longue durée de vie. Dans un processus de serveur Web qui prend beaucoup de temps, cela peut entraîner une pollution de chaque demande future faite dans le cadre de ce processus.

La même chose peut facilement se produire lors des tests. En particulier, dans les cas où les programmeurs essaient d'utiliser des correctifs de singe et n'utilisent pas de gestionnaire de contexte - comme mock.patch . Cela peut conduire non pas à une pollution des demandes, mais à une pollution de tous les tests qui seront effectués dans le même processus. C'est une raison sérieuse du comportement peu fiable de notre système de test. Il s'agit d'un problème important et il est très difficile de l'éviter. En conséquence, nous avons abandonné le système de test unifié et sommes passés à un schéma d'isolement de test, qui peut être décrit comme «un test par processus».

En fait, c'est notre troisième problème. Un état global mutable est un phénomène qui n'est pas propre à Python. Vous pouvez le trouver n'importe où. Nous parlons de classes, de modules, de listes ou de dictionnaires attachés aux modules ou aux classes, d'objets singleton créés au niveau du module. Travailler dans un tel environnement requiert de la discipline. Afin d'éviter la pollution de l'état global pendant l'exécution du programme, vous avez besoin d'une très bonne connaissance de Python.

Présentation de modules stricts


L'une des causes profondes de nos problèmes peut être que nous utilisons Python pour résoudre des problèmes pour lesquels ce langage n'est pas conçu. Dans les petites équipes et les petits projets, si vous suivez les règles lors de l'utilisation de Python, ce langage fonctionne très bien. Et nous devrions passer à un langage plus rigoureux.

Mais notre base de code a déjà dépassé la taille qui nous permet au moins de parler de la façon de le réécrire dans une autre langue. Et, plus important encore, malgré tous les problèmes auxquels nous sommes confrontés, Python y est pour beaucoup. Il nous donne plus de bien que de mal. Nos développeurs aiment vraiment ce langage. En conséquence, cela ne dépend que de nous comment faire fonctionner Python à notre échelle et comment nous assurer que nous pouvons continuer à travailler sur le projet pendant son développement.

Trouver des solutions à nos problèmes nous a conduit à une seule idée. Elle consiste à utiliser des modules stricts.

Les modules stricts sont des modules Python d'un nouveau type, au début desquels il y a une construction __strict__ = True . Ils sont implémentés à l'aide de nombreux mécanismes d'extensibilité de bas niveau que Python possède déjà. Un chargeur de module spécial analyse le code à l'aide du module ast , effectue une interprétation abstraite du code chargé pour l'analyser, applique diverses transformations à l'AST, puis recompile l'AST en bytecode Python à l'aide de la fonction de compile intégrée.

Pas d'effets secondaires à l'importation


Les modules stricts imposent certaines restrictions sur ce qui peut se produire au niveau du module. Ainsi, tout le code au niveau du module (y compris les décorateurs et les fonctions / initialiseurs appelés au niveau du module) doit être propre, c'est-à-dire un code exempt d'effets secondaires et n'utilisant pas de mécanismes d'E / S. Ces conditions sont vérifiées par l'interpréteur abstrait à l'aide des moyens d'analyse de code statique au moment de la compilation.

Cela signifie que l'utilisation de modules stricts ne provoque pas d'effets secondaires lors de leur importation. Le code exécuté lors de l'importation du module ne peut plus provoquer de problèmes inattendus. Étant donné que nous testons cela au niveau de l'interprétation abstraite, en utilisant des outils qui comprennent un grand sous-ensemble de Python, nous éliminons le besoin de limiter excessivement l'expressivité de Python. De nombreux types de code dynamique, dépourvus d'effets secondaires, peuvent être utilisés en toute sécurité au niveau du module. Cela inclut divers décorateurs et la définition de constantes de niveau module à l'aide de listes ou de générateurs de dictionnaire.

Soyons plus clairs, considérons un exemple. Voici le module strict correctement écrit:

 """Module docstring.""" __strict__ = True from utils import log_to_network MY_LIST = [1, 2, 3] MY_DICT = {x: x+1 for x in MY_LIST} def log_calls(func):    def _wrapped(*args, **kwargs):        log_to_network(f"{func.__name__} called!")        return func(*args, **kwargs)    return _wrapped @log_calls def hello_world():    log_to_network("Hello World!") 

Dans ce module, nous pouvons utiliser les constructions Python habituelles, y compris le code dynamique, celui qui est utilisé pour créer le dictionnaire et celui qui décrit le décorateur au niveau du module. Dans le même temps, l'accès aux ressources réseau dans les fonctions _wrapped ou hello_world est tout à fait normal. Le fait est qu'ils ne sont pas appelés au niveau du module.

Mais si nous avons déplacé l'appel log_to_network vers la fonction externe log_calls , ou si nous avons essayé d'utiliser un décorateur qui a provoqué des effets secondaires (comme @route de l'exemple précédent), ou si nous avons utilisé l'appel hello_world() au niveau du module, alors il cesserait d'être strict strict -module.

Comment savoir qu'il n'est pas sûr d'appeler log_to_network ou d' route fonctions au niveau du module? Nous partons de l'hypothèse que tout ce qui est importé de modules qui ne sont pas des modules stricts n'est pas sûr, à l'exception de certaines fonctions de la bibliothèque standard qui sont connues pour être sûres. Si le module utils est un module strict, nous pouvons nous fier à l'analyse de notre module pour nous faire savoir si la fonction log_to_network est log_to_network .

En plus d'améliorer la fiabilité du code, les importations exemptes d'effets secondaires éliminent un obstacle sérieux au téléchargement sécurisé de code incrémentiel. Cela ouvre d'autres possibilités pour explorer les moyens d'accélérer les équipes d'importation. Si le code au niveau du module est exempt d'effets secondaires, cela signifie que nous pouvons exécuter en toute sécurité des instructions de module individuelles en mode "paresseux", sur demande, lors de l'accès aux attributs du module. C'est bien mieux que de suivre l'algorithme «gourmand», dans l'application duquel tout le code du module est exécuté à l'avance. Et, compte tenu du fait que la forme de toutes les classes du module strict est complètement connue au moment de la compilation, nous pourrons même essayer à l'avenir d'organiser le stockage permanent des métadonnées du module (classes, fonctions, constantes) générées par l'exécution de code. Cela nous permettra d'organiser l'importation rapide de modules inchangés, ce qui ne nécessite pas l'exécution répétée du bytecode du niveau du module.

Immunité et attribut __slots__


Les modules et classes stricts qui y sont déclarés sont immuables après leur création. Les modules sont rendus immuables à l'aide de la transformation interne du corps du module en une fonction dans laquelle l'accès à toutes les variables globales est organisé par le biais de variables de fermeture. Ces changements ont sérieusement réduit la possibilité d'un changement aléatoire de l'état global, bien que l'état global mutable puisse encore être déterminé s'il est décidé de l'utiliser via des modules de niveau de conteneur mutables.

Les membres des classes déclarées dans des modules stricts doivent également être déclarés dans __init__ . Ils sont automatiquement écrits dans l'attribut __slots__ lors de la transformation AST effectuée par le chargeur de module. Par conséquent, plus tard, vous ne pouvez plus attacher d'attributs supplémentaires à l'instance de classe. Voici une classe similaire:

 class Person:    def __init__(self, name, age):        self.name = name        self.age = age 

Au cours de la transformation AST, qui est effectuée lors du traitement de modules stricts, les opérations d'attribution de valeurs aux attributs de name et d' age effectuées dans __init__ seront détectées et un attribut de la forme __slots__ = ('name', 'age') sera attaché à la classe. Cela empêchera tout autre attribut d'être ajouté à l'instance de classe. (Si des annotations de type sont utilisées, nous prenons en compte les informations sur les types disponibles au niveau de la classe, telles que name: str , et les ajoutons également à la liste des emplacements).

Les limitations décrites rendent non seulement le code plus fiable. Ils aident à accélérer l'exécution du code. La transformation automatique des classes avec l'ajout de l'attribut __slots__ augmente l'efficacité de l'utilisation de la mémoire lorsque vous travaillez avec ces classes. Cela vous permet de vous débarrasser des recherches de dictionnaire lorsque vous travaillez avec des instances individuelles de classes, ce qui accélère l'accès aux attributs. De plus, nous pouvons continuer à optimiser ces modèles pendant l'exécution du code Python, ce qui nous permettra d'améliorer encore notre système.

Résumé


Les modules stricts sont encore une technologie expérimentale. Nous avons un prototype fonctionnel, nous en sommes aux premières étapes du déploiement de ces capacités en production. Nous espérons qu'après avoir acquis suffisamment d'expérience dans l'utilisation de modules stricts, nous pourrons en parler davantage.

Chers lecteurs! Pensez-vous que les fonctionnalités offertes par les modules stricts sont utiles dans votre projet Python?


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


All Articles