À l'intérieur de la machine virtuelle Python. Partie 1



Bonjour à tous. J'ai finalement décidé de comprendre comment fonctionne l'interpréteur Python. Pour ce faire, il a commencé à étudier un livre d'articles et a conçu en même temps le traduire en russe. Le fait est que les traductions ne vous permettent pas de manquer une phrase incompréhensible et la qualité d'assimilation du matériel augmente).Je m'excuse à l'avance pour d'éventuelles inexactitudes. J'essaie toujours de traduire le plus correctement possible, mais l'un des principaux problèmes: il n'y a tout simplement aucune mention de certains termes dans l'équivalent russe.

Note de traduction
Python , «code object», ( ) . , .

— Python, - , : , , ( ! ) , , - ( ) .. — () - str (, Python3, bytes).

introduction


Le langage de programmation Python existe depuis un certain temps. Le développement de la première version a été lancé par Guido Van Rossum en 1989, et depuis lors, la langue s'est développée et est devenue l'une des plus populaires. Python est utilisé dans diverses applications: des interfaces graphiques aux applications d'analyse de données.

Le but de cet article est d'aller dans les coulisses de l'interpréteur et de fournir un aperçu conceptuel de la façon dont un programme écrit en Python est exécuté. CPython sera considéré dans l'article, car au moment de la rédaction, c'est l'implémentation Python la plus populaire et la plus basique.

Python et CPython sont utilisés comme synonymes dans ce texte, mais toute mention de Python signifie CPython (la version python implémentée en C). D'autres implémentations incluent PyPy (python implémenté dans un sous-ensemble limité de Python), Jython (implémentation sur la machine virtuelle Java), etc.

J'aime diviser l'exécution d'un programme Python en deux ou trois étapes principales (listées ci-dessous), selon la façon dont l'interpréteur est appelé. Ces étapes seront traitées à des degrés divers dans cet article:

  1. Initialisation - cette étape implique la mise en place des différentes structures de données requises par le processus python. Cela se produira très probablement lorsque le programme sera exécuté en mode non interactif via le shell de l'interpréteur.
  2. — , : , , .
  3. — .

Le mécanisme de génération d'arbres "d'analyse", ainsi que d'arbres à syntaxe abstraite (ASD), est indépendant du langage. Par conséquent, nous ne couvrirons pas beaucoup ce sujet, car les méthodes utilisées en Python sont similaires aux méthodes d'autres langages de programmation. D'un autre côté, le processus de construction de tables de symboles et d'objets de code à partir d'ADS en Python est plus spécifique, il mérite donc une attention particulière. Il traite également de l'interprétation des objets de code compilés et de toutes les autres structures de données. Les sujets traités par nous comprendront, sans s'y limiter: le processus de construction de tables de symboles et de création d'objets de code, objets Python, objets de cadre, objets de code, objets fonctionnels, opcodes, boucles d'interpréteur, générateurs et classes d'utilisateurs.

Ce matériel est destiné à toute personne intéressée à apprendre comment fonctionne la machine virtuelle CPython. Il est supposé que l'utilisateur est déjà familier avec python et comprend les bases du langage. Lors de l'étude de la structure d'une machine virtuelle, nous rencontrerons une quantité importante de code C, il sera donc plus facile pour un utilisateur ayant une compréhension élémentaire du langage C de comprendre le matériau. Et donc, fondamentalement, ce dont vous avez besoin pour vous familiariser avec ce matériel: le désir d'en savoir plus sur la machine virtuelle CPython.

Cet article est une version étendue des notes personnelles faites dans l'étude du travail interne de l'interprète. Il y a beaucoup de choses de qualité dans les vidéos PyCon , les conférences scolaires et ce blog.. Mon travail n'aurait pas été achevé sans ces fantastiques sources de connaissances.

À la fin de ce livre, le lecteur pourra comprendre les subtilités de la façon dont l'interpréteur Python exécute votre programme. Cela comprend les différentes étapes de l'exécution du programme et les structures de données qui sont critiques dans le programme. Pour commencer, nous prendrons une vue d'ensemble de ce qui se passe lorsqu'un programme trivial est exécuté, lorsque le nom du module est transmis à l'interpréteur sur la ligne de commande. Le code exécutable CPython peut être installé à partir des sources, en suivant le Guide du développeur Python .

Ce livre utilise la version Python 3.

Vue de 30 000 pieds


Ce chapitre explique comment l'interpréteur exécute un programme Python. Dans les chapitres suivants, nous examinerons les différentes parties de ce «puzzle» et fournirons une description plus détaillée de chaque partie. Quelle que soit la complexité d'un programme écrit en Python, ce processus est toujours le même. L'excellente explication donnée par Yaniv Aknin dans sa série d'articles sur Python Internal définit le sujet de notre discussion.

Le module source test.py peut être exécuté à partir de la ligne de commande (en le passant comme argument au programme interpréteur Python sous la forme de $ python test.py). Ce n'est qu'une façon d'invoquer l'exécutable Python. Nous pouvons également lancer un interpréteur interactif, exécuter des lignes d'un fichier sous forme de code, etc. Mais cela et d'autres méthodes ne nous intéressent pas. C'est le transfert du module en tant qu'argument (à l'intérieur de la ligne de commande) vers le fichier exécutable (figure 2.1) qui reflète le mieux le flux de diverses actions impliquées dans l'exécution réelle du code.


Figure 2.1: Stream à l'exécution.

L'exécutable python est un programme C ordinaire, donc lorsqu'il est appelé, des processus similaires à ceux qui existent, par exemple, dans le noyau Linux ou le simple programme hello world, se produisent. Prenez une minute de votre temps pour comprendre: l'exécutable python n'est qu'un autre programme qui lance le vôtre. De telles "relations" existent entre le langage C et l'assembleur (ou llvm). Le processus d'initialisation standard (qui dépend de la plate-forme où l'exécution a lieu) démarre lorsque l'exécutable python est appelé avec le nom du module comme argument.

Cet article suppose que vous utilisez un système d'exploitation basé sur Unix, donc certaines fonctionnalités peuvent varier sous Windows.

Le langage C au démarrage exécute toute sa «magie» d'initialisation - charge les bibliothèques, vérifie / définit les variables d'environnement, et après cela, la méthode principale de l'exécutable python est lancée comme tout autre programme C. Le fichier exécutable principal de Python se trouve dans ./Programs/python.c et effectue une initialisation (telle que la copie des arguments de ligne de commande de programme qui ont été passés au module). La fonction principale appelle ensuite la fonction Py_Main située dans ./Modules/main.c . Il traite le processus d'initialisation de l'interpréteur: analyse les arguments de la ligne de commande, définit les drapeaux, lit les variables d'environnement, exécute les hooks, randomise les fonctions de hachage, etc. Aussi appeléPy_Initialize de pylifecycle.c , qui gère l'initialisation de l'interpréteur et des structures de données d'état de flux, sont deux structures de données très importantes.

L'examen des déclarations des structures de données de l'interpréteur et des états des flux montre clairement pourquoi elles sont nécessaires. L'état de l'interpréteur et du flux ne sont que des structures avec des pointeurs vers des champs qui contiennent les informations nécessaires à l'exécution du programme. Les données d'état de l'interpréteur sont créées via typedef (pensez simplement à ce mot-clé en C comme définition de type, bien que ce ne soit pas entièrement vrai). Le code de cette structure est indiqué dans l'extrait 2.1.

 1     typedef struct _is {
 2 
 3         struct _is *next;
 4         struct _ts *tstate_head;
 5 
 6         PyObject *modules;
 7         PyObject *modules_by_index;
 8         PyObject *sysdict;
 9         PyObject *builtins;
10         PyObject *importlib;
11 
12         PyObject *codec_search_path;
13         PyObject *codec_search_cache;
14         PyObject *codec_error_registry;
15         int codecs_initialized;
16         int fscodec_initialized;
17 
18         PyObject *builtins_copy;
19     } PyInterpreterState;

Exemple de code 2.1: Structure des données d'état de l'interpréteur

Quiconque utilise le langage de programmation Python depuis longtemps peut reconnaître plusieurs champs mentionnés dans cette structure (sysdict, builtins, codec).

  1. Le champ * next est une référence à une autre instance de l'interpréteur, car plusieurs interprètes Python peuvent exister dans le même processus.
  2. Le champ * tstate_head indique le thread principal d'exécution (si le programme est multi-thread, alors l'interpréteur est commun à tous les threads créés par le programme). Nous en discuterons plus en détail sous peu.
  3. modules, modules_by_index, sysdict, builtins et importlib parlent d'eux-mêmes. Tous sont définis comme des instances de PyObject , qui est le type racine de tous les objets de la machine virtuelle Python. Les objets Python seront discutés plus en détail dans les chapitres suivants.
  4. Les champs liés au codec * contiennent des informations qui aident au téléchargement des encodages. Ceci est très important pour le décodage d'octets.

L'exécution du programme doit avoir lieu dans un thread. La structure d'état du flux contient toutes les informations dont le flux a besoin pour exécuter un objet de code. Une partie de la structure des données de flux est présentée dans le Listing 2.2.

 1     typedef struct _ts {
 2         struct _ts *prev;
 3         struct _ts *next;
 4         PyInterpreterState *interp;
 5 
 6         struct _frame *frame;
 7         int recursion_depth;
 8         char overflowed; 
 9                         
10         char recursion_critical; 
11         int tracing;
12         int use_tracing;
13 
14         Py_tracefunc c_profilefunc;
15         Py_tracefunc c_tracefunc;
16         PyObject *c_profileobj;
17         PyObject *c_traceobj;
18 
19         PyObject *curexc_type;
20         PyObject *curexc_value;
21         PyObject *curexc_traceback;
22 
23         PyObject *exc_type;
24         PyObject *exc_value;
25         PyObject *exc_traceback;
26 
27         PyObject *dict;  /* Stores per-thread state */
28         int gilstate_counter;
29 
30         ... 
31     } PyThreadState;

Listing 2.2: Partie de la

structure des données d'état du flux Les structures de données de l' interpréteur et les états du flux sont abordés plus en détail dans les chapitres suivants. Le processus d'initialisation met également en place des mécanismes d'importation ainsi que des stdio élémentaires.

Après avoir terminé toute l'initialisation, Py_Main appelle la fonction run_file (également située dans le module main.c). Voici une série d'appels de fonction: PyRun_AnyFileExFlags -> PyRun_SimpleFileExFlags -> PyRun_FileExFlags -> PyParser_ASTFromFileObject. PyRun_SimpleFileExFlagscrée l'espace de noms __main__ dans lequel le contenu du fichier sera exécuté. Il vérifie également si la version pyc du fichier existe (le fichier pyc est un simple fichier contenant une version déjà compilée du code source). S'il existe une version pyc, une tentative sera faite de la lire en tant que fichier binaire, puis de l'exécuter. Si le fichier pyc est manquant, alors PyRun_FileExFlags est appelé, etc. La fonction PyParser_ASTFromFileObject appelle PyParser_ParseFileObject , qui lit le contenu du module et en construit des arbres d'analyse. Ensuite, l'arborescence créée est transmise à PyParser_ASTFromNodeObject , qui en crée une arborescence de syntaxe abstraite.

, Py_INCREF Py_DECREF. , . CPython : , , Py_INCREF. , , Py_DECREF.

AST est généré lorsque run_mod est appelé . Cette fonction appelle PyAST_CompileObject , qui crée des objets de code à partir d'AST. Notez que le bytecode généré pendant l'appel PyAST_CompileObject est transmis via l'optimiseur de judas simple , qui effectue une faible optimisation du bytecode généré avant de créer des objets de code. La fonction de run_mod applique alors la PyEval_EvalCode fonction du ceval.c fichier à l'objet de code. Cela conduit à une autre série d'appels de fonction: PyEval_EvalCode -> PyEval_EvalCode -> _PyEval_EvalCodeWithName -> _PyEval_EvalFrameEx. L'objet code est passé en argument à la plupart de ces fonctions sous une forme ou une autre. _PyEval_EvalFrameEx- Il s'agit d'une boucle d'interpréteur normale qui gère l'exécution des objets de code. Cependant, il est appelé non seulement avec l'objet code comme argument, mais avec l'objet frame, qui a pour attribut un champ qui fait référence à l'objet code. Ce cadre fournit le contexte pour l'exécution de l'objet de code. En termes simples: la boucle d'interprétation lit en continu l'instruction suivante indiquée par le compteur d'instructions dans le tableau d'instructions. Ensuite, il exécute cette instruction: il ajoute ou supprime des objets de la pile de valeurs dans le processus jusqu'à ce qu'il se vide dans le tableau d'instructions à exécuter (enfin, ou quelque chose d'exceptionnel se produit qui perturbe la boucle).

Python fournit un ensemble de fonctions que vous pouvez utiliser pour examiner des objets de code réels. Par exemple, un programme simple peut être compilé en un objet code et désassemblé pour obtenir des opcodes exécutés par la machine virtuelle python. Ceci est illustré dans le Listing 2.3.

1         >>> def square(x):
2         ...     return x*x
3         ... 
4 
5         >>> dis(square)
6         2           0 LOAD_FAST                0 (x)
7                     2 LOAD_FAST                0 (x)
8                     4 BINARY_MULTIPLY     
9                     6 RETURN_VALUE        

Exemple de code 2.3: DĂ©sassembler une fonction en Python Le

fichier d'en-tête ./Include/opcodes.h contient une liste complète de toutes les instructions / opcodes pour la machine virtuelle Python. Les opcode sont assez simples. Prenons notre exemple dans le Listing 2.3, qui contient un ensemble de quatre instructions. LOAD_FAST charge la valeur de son argument (dans ce cas x) sur la pile de valeurs. La machine virtuelle python est basée sur la pile, de sorte que les valeurs des opérations d'opcode sont «extraites» de la pile et les résultats du calcul sont repoussés sur la pile pour une utilisation ultérieure par d'autres opcodes. Ensuite, BINARY_MULTIPLY extrait deux éléments de la pile, effectue une multiplication binaire des deux valeurs et repousse le résultat sur la pile. Instruction VALEUR RENVOYÉErécupère une valeur de la pile, définit la valeur de retour de l'objet sur cette valeur et quitte la boucle d'interpréteur. Si vous regardez le Listing 2.3, il est clair que c'est une simplification assez forte.

L'explication actuelle de la boucle d'interprétation ne prend pas en compte un certain nombre de détails, qui seront discutés dans les chapitres suivants. Par exemple, voici les questions auxquelles nous n'avons pas reçu de réponse:

  • D'oĂą proviennent les valeurs chargĂ©es par l'instruction LOAD_FAST?
  • D'oĂą viennent les arguments, qui sont utilisĂ©s dans le cadre des instructions?
  • Comment les appels de fonction et de mĂ©thode imbriquĂ©s sont-ils traitĂ©s?
  • Comment la boucle interprète gère-t-elle les exceptions?

Après avoir terminé toutes les instructions, la fonction Py_Main continue son exécution, mais cette fois démarre le processus de nettoyage. Si Py_Initialize est appelé pour effectuer l'initialisation lors du démarrage de l'interpréteur, Py_FinalizeEx est appelé pour effectuer le nettoyage. Ce processus comprend l'attente de la sortie des threads, l'appel à tous les gestionnaires de sortie, ainsi que la libération de la mémoire encore utilisée allouée par l'interpréteur.

Et donc, nous avons examiné la description "de haut niveau" des processus qui se produisent dans l'exécutable Python lorsqu'un script est exécuté. Comme indiqué précédemment, de nombreuses questions restent sans réponse. À l'avenir, nous approfondirons l'étude de l'interprète et examinerons en détail chacune des étapes. Et nous commencerons par décrire le processus de compilation dans le chapitre suivant.

All Articles