Python sous le capot — Chapitre 3 : Le code source CPython, pas à pas

Avant-propos (du traducteur)

Ceci est la traduction du troisième article de Victor Skvortsov du 11 octobre 2020 : Python behind the scenes #3: stepping through the CPython source code.

Avant-propos

Dans la première et la seconde partie de cette série, nous avons vu les concepts sur lesquels s’appuient l’exécution et la compilation d’un programme Python. Nous continuerons à aborder des concepts dans les prochaines parties, mais nous allons faire une exception pour cette fois nous concentrer sur le code qui donne vie à ces concepts.

Objectif du jour

Le code source de CPython comprend environ 350 000 lignes de code C (à l’exception des fichiers d’en-tête) et près de 600 000 lignes de code Python. Aborder tout cela d’une traite serait sans aucun doute une tâche ardue. Nous nous limiterons donc aujourd’hui à la partie qui s’exécute à chaque fois qu’on lance python. Nous allons commencer par la fonction main() de l’exécutable python et parcourir le code source jusqu’à atteindre la boucle d’évaluation, l’endroit où le bytecode Python est exécuté.

Notre objectif n’est pas de comprendre chacun des morceaux de code que nous rencontrerons, mais de mettre en évidence les parties les plus intéressantes, de les étudier pour, au final, avoir une idée approximative de ce qui se passe au tout début de l’exécution d’un programme Python.

Il y a deux remarques que je dois faire. Premièrement, nous n’entrerons pas dans chacune des fonctions. Nous survolerons certaines parties en en approfondissant d’autres. Néanmoins, je présenterais les fonctions dans l’ordre d’exécution. Deuxièmement, je laisserai le code tel quel, à l’exception de quelques définitions de structure. La seule chose que je me permettrais sera d’ajouter quelques commentaires et de reformuler ceux existants. Tout au long de cet article, tous les commentaires multi-lignes /**/ sont originaux et tous les commentaires d’une seule ligne // sont les miens1. Ceci étant dit, commençons notre voyage dans le code source de CPython.

Récupérer CPython

Avant de pouvoir explorer le code source, nous devons l’obtenir. Clonons le dépôt de CPython :

$ git clone https://github.com/python/cpython/ && cd cpython

La branche actuelle master est le futur CPython 3.10. Ce qui nous intéresse, c’est la dernière version stable, CPython 3.9, alors passons sur la branche 3.9 :

$ git checkout 3.9

Dans le répertoire racine, nous avons le contenu suivant :

$ ls -p
CODE_OF_CONDUCT.md      Objects/                config.sub
Doc/                    PC/                     configure
Grammar/                PCbuild/                configure.ac
Include/                Parser/                 install-sh
LICENSE                 Programs/               m4/
Lib/                    Python/                 netlify.toml
Mac/                    README.rst              pyconfig.h.in
Makefile.pre.in         Tools/                  setup.py
Misc/                   aclocal.m4
Modules/                config.guess

Certains des sous-répertoires listés revêtent une importance particulière pour nous, au cours de cette série :

Si vous ne voyez pas de répertoire pour les tests et que votre rythme cardiaque s’accélère, détendez-vous. C’est dans Lib/test/. Les tests peuvent être utiles non seulement pour le développement, mais également pour comprendre le fonctionnement de CPython. Par exemple, pour comprendre quels types d’optimisation le peephole optimizer est censé faire, vous pouvez aller regarder dans Lib/test/test_peepholer.py. Et pour comprendre ce que font certains morceaux de code du peephole optimizer, vous pouvez supprimer ces morceaux de code, recompiler CPython, exécuter

$ ./python.exe -m test test_peepholer

et voir quels tests échouent.

Dans un monde idéal, tout ce que nous avons à faire pour compiler CPython est d’exécuter ./configure et make :

$ ./configure
$ make -j -s

make générera un exécutable nommé python, mais ne soyez pas surpris de voir python.exe sur Mac OS. L’extension .exe est utilisée pour distinguer l’exécutable du répertoire Python/ sur les systèmes de fichiers insensible à la casse. Consultez le Python Developer’s Guide pour plus d’informations sur la compilation.

À ce stade, nous pouvons affirmer fièrement que nous avons généré notre propre copie de CPython :

$ ./python.exe
Python 3.9.0+ (heads/3.9-dirty:20bdeedfb4, Oct 10 2020, 16:55:24)
[Clang 10.0.0 (clang-1000.10.44.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> 2 ** 16
65536

Le code source

Comme tout autre programme C, l’exécution de CPython commence par la fonction main(), dans Python/python.c :

/* Minimal main program -- everything is loaded from the library */

#include "Python.h"

#ifdef MS_WINDOWS
int
wmain(int argc, wchar_t **argv)
{
    return Py_Main(argc, argv);
}
#else
int
main(int argc, char **argv)
{
    return Py_BytesMain(argc, argv);
}
#endif

Il ne s’y passe pas grand-chose. On peut toutefois mentionner que sous Windows, CPython utilise wmain() au lieu de main() afin de recevoir argv sous forme de strings encodées en UTF-16. Pour les autres plateformes, CPython doit effectuer une conversion de string supplémentaire de char vers wchar_t. L’encodage d’une string char dépend des paramètres régionaux et l’encodage d’une string wchar_t dépend de la taille de wchar_t. Par exemple, si sizeof(wchar_t) == 4, l’encodage UCS-4 est utilisé. La PEP 383 en dit plus à ce sujet.

Py_Main() et Py_BytesMain() se situent dans le Modules/main.c. Leur tâche consiste essentiellement à appeler pymain_main() avec des arguments légèrement différents :

int
Py_Main(int argc, wchar_t **argv)
{
    _PyArgv args = {
        .argc = argc,
        .use_bytes_argv = 0,
        .bytes_argv = NULL,
        .wchar_argv = argv};
    return pymain_main(&args);
}


int
Py_BytesMain(int argc, char **argv)
{
    _PyArgv args = {
        .argc = argc,
        .use_bytes_argv = 1,
        .bytes_argv = argv,
        .wchar_argv = NULL};
    return pymain_main(&args);
}

Attardons-nous sur pymain_main(), bien qu’il ne semble pas faire pas grand-chose non plus :

static int
pymain_main(_PyArgv *args)
{
    PyStatus status = pymain_init(args);
    if (_PyStatus_IS_EXIT(status)) {
        pymain_free();
        return status.exitcode;
    }
    if (_PyStatus_EXCEPTION(status)) {
        pymain_exit_error(status);
    }

    return Py_RunMain();
}

La dernière fois, nous avons vu qu’avant d’exécuter un programme, CPython fait beaucoup de choses pour le compiler. Il s’avère que CPython fait beaucoup de choses avant même de compiler un programme. Ces étapes constituent l’initialisation de CPython. Comme nous l’avons déjà vu dans la première partie, CPython fonctionne en trois étapes :

  1. L’initialisation.
  2. La compilation.
  3. L’interprétation.

Donc, ce que fait pymain_main() c’est appeler pymain_init() pour effectuer l’initialisation, puis Py_RunMain() pour passer aux étapes suivantes. Une question demeure : Que fait CPython lors de l’initialisation ? Réfléchissons-y quelques instants. Au minimum, CPython doit :

Avant de sauter dans pymain_init() pour voir comment tout cela est fait, discutons plus en détail du processus d’initialisation.

L’initialisation

Depuis CPython 3.8, l’initialisation se fait en trois phases distinctes :

Ces phases introduisent de nouvelles possibilités de façon progressive. La phase de pré-initialisation initialise le runtime state, configure l’allocateur de mémoire par défaut et effectue une configuration très basique. Il n’y a encore aucun signe de Python. La phase d’initialisation du noyau initialise l’interpréter state principal et le thread state principal, les types et exceptions standards, le module builtins, le module sys et le système d’importation. À ce stade, vous pouvez utiliser le « noyau » de Python. Cependant, certaines choses ne sont pas encore disponibles. Par exemple, le module sys n’est que partiellement initialisé et seule l’importation des modules standards et figés est prise en charge. Après la phase d’initialisation principale, CPython est entièrement initialisé et prêt à compiler et exécuter un programme Python.

Quels bénéfices peut-on tirer d’une initialisation en plusieurs phases ? Grossièrement, cela permet de régler CPython plus facilement. Par exemple, on peut définir un allocateur de mémoire personnalisé dans l’état preinitialized ou remplacer la configuration du chemin dans l’état core_initialized. Bien sûr, CPython n’a pas besoin de régler quoi que ce soit lui-même. Ces fonctionnalités sont importantes pour les utilisateurs de l’API Python/C qui étendent et embarque Python. Les PEP 432 et PEP 587 expliquent plus en détail pourquoi l’initialisation en plusieurs phases est une bonne idée.

s’occupe principalement de la pré-initialisation et appelle Py_InitializeFromConfig() à la fin pour exécuter la phase d’initialisation du noyau et la phase d’initialisation principale :

static PyStatus
pymain_init(const _PyArgv *args)
{
    PyStatus status;

    // Initialise le runtime state.
    status = _PyRuntime_Initialize();
    if (_PyStatus_EXCEPTION(status)) {
        return status;
    }

    // Initialise le `preconfig` par défaut.
    PyPreConfig preconfig;
    PyPreConfig_InitPythonConfig(&preconfig);

    // Effectue la pré-initialisation
    status = _Py_PreInitializeFromPyArgv(&preconfig, args);
    if (_PyStatus_EXCEPTION(status)) {
        return status;
    }
    // Pré-initialisé. Prépare `config` aux prochaines phases d’initialisation.

    // Initialise le `config` par défaut.
    PyConfig config;
    PyConfig_InitPythonConfig(&config);

    // Stocke les arguments de la ligne de commande dans `config->argv`.
    if (args->use_bytes_argv) {
        status = PyConfig_SetBytesArgv(&config, args->argc, args->bytes_argv);
    }
    else {
        status = PyConfig_SetArgv(&config, args->argc, args->wchar_argv);
    }
    if (_PyStatus_EXCEPTION(status)) {
        goto done;
    }

    // Effectue l’initialisation du noyau, puis principale.
    status = Py_InitializeFromConfig(&config);
        if (_PyStatus_EXCEPTION(status)) {
        goto done;
    }
    status = _PyStatus_OK();

done:
    PyConfig_Clear(&config);
    return status;
}

_PyRuntime_Initialize() initialise le runtime state. Le runtime state est stocké dans la variable globale nommée _PyRuntime de type _PyRuntimeState, défini tel que ci-dessous :

/* Full Python runtime state */

typedef struct pyruntimestate {
    /* Is running Py_PreInitialize()? */
    int preinitializing;

    /* Is Python preinitialized? Set to 1 by Py_PreInitialize() */
    int preinitialized;

    /* Is Python core initialized? Set to 1 by _Py_InitializeCore() */
    int core_initialized;

    /* Is Python fully initialized? Set to 1 by Py_Initialize() */
    int initialized;

    /* Set by Py_FinalizeEx(). Only reset to NULL if Py_Initialize() is called again. */
    _Py_atomic_address _finalizing;

    struct pyinterpreters {
        PyThread_type_lock mutex;
        PyInterpreterState *head;
        PyInterpreterState *main;
        int64_t next_id;
    } interpreters;

    unsigned long main_thread;

    struct _ceval_runtime_state ceval;
    struct _gilstate_runtime_state gilstate;

    PyPreConfig preconfig;

    // ... moins intéressant pour le moment.
} _PyRuntimeState;

Le champ preconfig à la fin de _PyRuntimeState contient la configuration utilisée pour pré-initialiser CPython. Elle est également utilisée lors de la phase suivante pour terminer la configuration. Voici la définition de PyPreConfig, largement commenté :

typedef struct {
    int _config_init;     /* _PyConfigInitEnum value */

    /* Parse Py_PreInitializeFromBytesArgs() arguments?
       See PyConfig.parse_argv */
    int parse_argv;

    /* If greater than 0, enable isolated mode: sys.path contains
       neither the script's directory nor the user's site-packages directory.

       Set to 1 by the -I command line option. If set to -1 (default), inherit
       Py_IsolatedFlag value. */
    int isolated;

    /* If greater than 0: use environment variables.
       Set to 0 by -E command line option. If set to -1 (default), it is
       set to !Py_IgnoreEnvironmentFlag. */
    int use_environment;

    /* Set the LC_CTYPE locale to the user preferred locale? If equals to 0,
       set coerce_c_locale and coerce_c_locale_warn to 0. */
    int configure_locale;

    /* Coerce the LC_CTYPE locale if it's equal to "C"? (PEP 538)

       Set to 0 by PYTHONCOERCECLOCALE=0. Set to 1 by PYTHONCOERCECLOCALE=1.
       Set to 2 if the user preferred LC_CTYPE locale is "C".

       If it is equal to 1, LC_CTYPE locale is read to decide if it should be
       coerced or not (ex: PYTHONCOERCECLOCALE=1). Internally, it is set to 2
       if the LC_CTYPE locale must be coerced.

       Disable by default (set to 0). Set it to -1 to let Python decide if it
       should be enabled or not. */
    int coerce_c_locale;

    /* Emit a warning if the LC_CTYPE locale is coerced?

       Set to 1 by PYTHONCOERCECLOCALE=warn.

       Disable by default (set to 0). Set it to -1 to let Python decide if it
       should be enabled or not. */
    int coerce_c_locale_warn;

#ifdef MS_WINDOWS
    /* If greater than 1, use the "mbcs" encoding instead of the UTF-8
       encoding for the filesystem encoding.

       Set to 1 if the PYTHONLEGACYWINDOWSFSENCODING environment variable is
       set to a non-empty string. If set to -1 (default), inherit
       Py_LegacyWindowsFSEncodingFlag value.

       See PEP 529 for more details. */
    int legacy_windows_fs_encoding;
#endif

    /* Enable UTF-8 mode? (PEP 540)

       Disabled by default (equals to 0).

       Set to 1 by "-X utf8" and "-X utf8=1" command line options.
       Set to 1 by PYTHONUTF8=1 environment variable.

       Set to 0 by "-X utf8=0" and PYTHONUTF8=0.

       If equals to -1, it is set to 1 if the LC_CTYPE locale is "C" or
       "POSIX", otherwise it is set to 0. Inherit Py_UTF8Mode value value. */
    int utf8_mode;

    /* If non-zero, enable the Python Development Mode.

       Set to 1 by the -X dev command line option. Set by the PYTHONDEVMODE
       environment variable. */
    int dev_mode;

    /* Memory allocator: PYTHONMALLOC env var.
       See PyMemAllocatorName for valid values. */
    int allocator;
} PyPreConfig;

Après l’appel à _PyRuntime_Initialize(), la variable globale _PyRuntime est initialisée avec des valeurs par défaut. Ensuite, PyPreConfig_InitPythonConfig() initialise un nouveau preconfig par défaut, puis _Py_PreInitializeFromPyArgv() effectue la vraie pré-initialisation. Pourquoi initialiser une seconde preconfig s’il y en a déjà une dans _PyRuntime ? Gardez à l’esprit que de nombreuses fonctions appelées par CPython sont également exposées par l’API Python/C. Ainsi, CPython utilise cette API tel qu’elle est pensée. Une autre conséquence de ceci est que, lorsque vous parcourez le code source de CPython, comme nous le faisons aujourd’hui, vous rencontrez souvent des fonctions qui semblent faire plus que ce que vous attendez d’elles. Par exemple, _PyRuntime_Initialize() est appelée plusieurs fois pendant le processus d’initialisation. Bien sûr, il ne fait rien sur les appels suivants.

_Py_PreInitializeFromPyArgv() lit les arguments de la ligne de commande, les variables d’environnement et les variables de configuration globales à définir dans _PyRuntime.preconfig, les paramètres régionaux courants et l’allocateur de mémoire. Il ne lit pas toute la configuration mais uniquement les paramètres pertinents pour la phase de pré-initialisation. Par exemple, il analyse uniquement les arguments -E, -I et -X.

À ce stade, le runtime est pré-initialisé. Le reste de pymain_init() prépare config pour la prochaine phase d’initialisation. Vous ne devez pas confondre config et preconfig. Le premier est une structure contenant une grande partie de la configuration Python. Il est très utilisé durant la phase d’initialisation et lors de l’exécution d’un programme Python. Je vous invite à regarder la longue définition de config pour vous faire une idée de comment il est utilisé :

/* --- PyConfig ---------------------------------------------- */

typedef struct {
    int _config_init;     /* _PyConfigInitEnum value */

    int isolated;         /* Isolated mode? see PyPreConfig.isolated */
    int use_environment;  /* Use environment variables? see PyPreConfig.use_environment */
    int dev_mode;         /* Python Development Mode? See PyPreConfig.dev_mode */

    /* Install signal handlers? Yes by default. */
    int install_signal_handlers;

    int use_hash_seed;      /* PYTHONHASHSEED=x */
    unsigned long hash_seed;

    /* Enable faulthandler?
       Set to 1 by -X faulthandler and PYTHONFAULTHANDLER. -1 means unset. */
    int faulthandler;

    /* Enable PEG parser?
       1 by default, set to 0 by -X oldparser and PYTHONOLDPARSER */
    int _use_peg_parser;

    /* Enable tracemalloc?
       Set by -X tracemalloc=N and PYTHONTRACEMALLOC. -1 means unset */
    int tracemalloc;

    int import_time;        /* PYTHONPROFILEIMPORTTIME, -X importtime */
    int show_ref_count;     /* -X showrefcount */
    int dump_refs;          /* PYTHONDUMPREFS */
    int malloc_stats;       /* PYTHONMALLOCSTATS */

    /* Python filesystem encoding and error handler:
       sys.getfilesystemencoding() and sys.getfilesystemencodeerrors().

       Default encoding and error handler:

       * if Py_SetStandardStreamEncoding() has been called: they have the
         highest priority;
       * PYTHONIOENCODING environment variable;
       * The UTF-8 Mode uses UTF-8/surrogateescape;
       * If Python forces the usage of the ASCII encoding (ex: C locale
         or POSIX locale on FreeBSD or HP-UX), use ASCII/surrogateescape;
       * locale encoding: ANSI code page on Windows, UTF-8 on Android and
         VxWorks, LC_CTYPE locale encoding on other platforms;
       * On Windows, "surrogateescape" error handler;
       * "surrogateescape" error handler if the LC_CTYPE locale is "C" or "POSIX";
       * "surrogateescape" error handler if the LC_CTYPE locale has been coerced
         (PEP 538);
       * "strict" error handler.

       Supported error handlers: "strict", "surrogateescape" and
       "surrogatepass". The surrogatepass error handler is only supported
       if Py_DecodeLocale() and Py_EncodeLocale() use directly the UTF-8 codec;
       it's only used on Windows.

       initfsencoding() updates the encoding to the Python codec name.
       For example, "ANSI_X3.4-1968" is replaced with "ascii".

       On Windows, sys._enablelegacywindowsfsencoding() sets the
       encoding/errors to mbcs/replace at runtime.


       See Py_FileSystemDefaultEncoding and Py_FileSystemDefaultEncodeErrors.
       */
    wchar_t *filesystem_encoding;
    wchar_t *filesystem_errors;

    wchar_t *pycache_prefix;  /* PYTHONPYCACHEPREFIX, -X pycache_prefix=PATH */
    int parse_argv;           /* Parse argv command line arguments? */

    /* Command line arguments (sys.argv).

       Set parse_argv to 1 to parse argv as Python command line arguments
       and then strip Python arguments from argv.

       If argv is empty, an empty string is added to ensure that sys.argv
       always exists and is never empty. */
    PyWideStringList argv;

    /* Program name:

       - If Py_SetProgramName() was called, use its value.
       - On macOS, use PYTHONEXECUTABLE environment variable if set.
       - If WITH_NEXT_FRAMEWORK macro is defined, use __PYVENV_LAUNCHER__
         environment variable is set.
       - Use argv[0] if available and non-empty.
       - Use "python" on Windows, or "python3 on other platforms. */
    wchar_t *program_name;

    PyWideStringList xoptions;     /* Command line -X options */

    /* Warnings options: lowest to highest priority. warnings.filters
       is built in the reverse order (highest to lowest priority). */
    PyWideStringList warnoptions;

    /* If equal to zero, disable the import of the module site and the
       site-dependent manipulations of sys.path that it entails. Also disable
       these manipulations if site is explicitly imported later (call
       site.main() if you want them to be triggered).

       Set to 0 by the -S command line option. If set to -1 (default), it is
       set to !Py_NoSiteFlag. */
    int site_import;

    /* Bytes warnings:

       * If equal to 1, issue a warning when comparing bytes or bytearray with
         str or bytes with int.
       * If equal or greater to 2, issue an error.

       Incremented by the -b command line option. If set to -1 (default), inherit
       Py_BytesWarningFlag value. */
    int bytes_warning;

    /* If greater than 0, enable inspect: when a script is passed as first
       argument or the -c option is used, enter interactive mode after
       executing the script or the command, even when sys.stdin does not appear
       to be a terminal.

       Incremented by the -i command line option. Set to 1 if the PYTHONINSPECT
       environment variable is non-empty. If set to -1 (default), inherit
       Py_InspectFlag value. */
    int inspect;

    /* If greater than 0: enable the interactive mode (REPL).

       Incremented by the -i command line option. If set to -1 (default),
       inherit Py_InteractiveFlag value. */
    int interactive;

    /* Optimization level.

       Incremented by the -O command line option. Set by the PYTHONOPTIMIZE
       environment variable. If set to -1 (default), inherit Py_OptimizeFlag
       value. */
    int optimization_level;

    /* If greater than 0, enable the debug mode: turn on parser debugging
       output (for expert only, depending on compilation options).

       Incremented by the -d command line option. Set by the PYTHONDEBUG
       environment variable. If set to -1 (default), inherit Py_DebugFlag
       value. */
    int parser_debug;

    /* If equal to 0, Python won't try to write ``.pyc`` files on the
       import of source modules.

       Set to 0 by the -B command line option and the PYTHONDONTWRITEBYTECODE
       environment variable. If set to -1 (default), it is set to
       !Py_DontWriteBytecodeFlag. */
    int write_bytecode;

    /* If greater than 0, enable the verbose mode: print a message each time a
       module is initialized, showing the place (filename or built-in module)
       from which it is loaded.

       If greater or equal to 2, print a message for each file that is checked
       for when searching for a module. Also provides information on module
       cleanup at exit.

       Incremented by the -v option. Set by the PYTHONVERBOSE environment
       variable. If set to -1 (default), inherit Py_VerboseFlag value. */
    int verbose;

    /* If greater than 0, enable the quiet mode: Don't display the copyright
       and version messages even in interactive mode.

       Incremented by the -q option. If set to -1 (default), inherit
       Py_QuietFlag value. */
    int quiet;

   /* If greater than 0, don't add the user site-packages directory to
      sys.path.

      Set to 0 by the -s and -I command line options , and the PYTHONNOUSERSITE
      environment variable. If set to -1 (default), it is set to
      !Py_NoUserSiteDirectory. */
    int user_site_directory;

    /* If non-zero, configure C standard steams (stdio, stdout,
       stderr):

       - Set O_BINARY mode on Windows.
       - If buffered_stdio is equal to zero, make streams unbuffered.
         Otherwise, enable streams buffering if interactive is non-zero. */
    int configure_c_stdio;

    /* If equal to 0, enable unbuffered mode: force the stdout and stderr
       streams to be unbuffered.

       Set to 0 by the -u option. Set by the PYTHONUNBUFFERED environment
       variable.
       If set to -1 (default), it is set to !Py_UnbufferedStdioFlag. */
    int buffered_stdio;

    /* Encoding of sys.stdin, sys.stdout and sys.stderr.
       Value set from PYTHONIOENCODING environment variable and
       Py_SetStandardStreamEncoding() function.
       See also 'stdio_errors' attribute. */
    wchar_t *stdio_encoding;

    /* Error handler of sys.stdin and sys.stdout.
       Value set from PYTHONIOENCODING environment variable and
       Py_SetStandardStreamEncoding() function.
       See also 'stdio_encoding' attribute. */
    wchar_t *stdio_errors;

#ifdef MS_WINDOWS
    /* If greater than zero, use io.FileIO instead of WindowsConsoleIO for sys
       standard streams.

       Set to 1 if the PYTHONLEGACYWINDOWSSTDIO environment variable is set to
       a non-empty string. If set to -1 (default), inherit
       Py_LegacyWindowsStdioFlag value.

       See PEP 528 for more details. */
    int legacy_windows_stdio;
#endif

    /* Value of the --check-hash-based-pycs command line option:

       - "default" means the 'check_source' flag in hash-based pycs
         determines invalidation
       - "always" causes the interpreter to hash the source file for
         invalidation regardless of value of 'check_source' bit
       - "never" causes the interpreter to always assume hash-based pycs are
         valid

       The default value is "default".

       See PEP 552 "Deterministic pycs" for more details. */
    wchar_t *check_hash_pycs_mode;

    /* --- Path configuration inputs ------------ */

    /* If greater than 0, suppress _PyPathConfig_Calculate() warnings on Unix.
       The parameter has no effect on Windows.

       If set to -1 (default), inherit !Py_FrozenFlag value. */
    int pathconfig_warnings;

    wchar_t *pythonpath_env; /* PYTHONPATH environment variable */
    wchar_t *home;          /* PYTHONHOME environment variable,
                               see also Py_SetPythonHome(). */

    /* --- Path configuration outputs ----------- */

    int module_search_paths_set;  /* If non-zero, use module_search_paths */
    PyWideStringList module_search_paths;  /* sys.path paths. Computed if
                                       module_search_paths_set is equal
                                       to zero. */

    wchar_t *executable;        /* sys.executable */
    wchar_t *base_executable;   /* sys._base_executable */
    wchar_t *prefix;            /* sys.prefix */
    wchar_t *base_prefix;       /* sys.base_prefix */
    wchar_t *exec_prefix;       /* sys.exec_prefix */
    wchar_t *base_exec_prefix;  /* sys.base_exec_prefix */
    wchar_t *platlibdir;        /* sys.platlibdir */

    /* --- Parameter only used by Py_Main() ---------- */

    /* Skip the first line of the source ('run_filename' parameter), allowing use of non-Unix forms of
       "#!cmd".  This is intended for a DOS specific hack only.

       Set by the -x command line option. */
    int skip_source_first_line;

    wchar_t *run_command;   /* -c command line argument */
    wchar_t *run_module;    /* -m command line argument */
    wchar_t *run_filename;  /* Trailing command line argument without -c or -m */

    /* --- Private fields ---------------------------- */

    /* Install importlib? If set to 0, importlib is not initialized at all.
       Needed by freeze_importlib. */
    int _install_importlib;

    /* If equal to 0, stop Python initialization before the "main" phase */
    int _init_main;

    /* If non-zero, disallow threads, subprocesses, and fork.
       Default: 0. */
    int _isolated_interpreter;

    /* Original command line arguments. If _orig_argv is empty and _argv is
       not equal to [''], PyConfig_Read() copies the configuration 'argv' list
       into '_orig_argv' list before modifying 'argv' list (if parse_argv
       is non-zero).

       _PyConfig_Write() initializes Py_GetArgcArgv() to this list. */
    PyWideStringList _orig_argv;
} PyConfig;

De la même façon que PyPreConfig_InitPythonConfig() est appelé par pymain_init() pour créer le preconfig par défaut, pymain_init() appelle maintenant PyConfig_InitPythonConfig() pour créer le config par défaut. Il appelle ensuite PyConfig_SetBytesArgv() pour stocker les arguments de la ligne de commande dans config.argv et Py_InitializeFromConfig() pour lancer le noyau ainsi que les principales phases d’initialisation. Passons de pymain_init() à Py_InitializeFromConfig() :

PyStatus
Py_InitializeFromConfig(const PyConfig *config)
{
    if (config == NULL) {
        return _PyStatus_ERR("initialization config is NULL");
    }

    PyStatus status;

    // Oui, on l’appelle une seconde fois.
    status = _PyRuntime_Initialize();
    if (_PyStatus_EXCEPTION(status)) {
        return status;
    }
    _PyRuntimeState *runtime = &_PyRuntime;

    PyThreadState *tstate = NULL;
    // La phase d’initialisation du noyau.
    status = pyinit_core(runtime, config, &tstate);
    if (_PyStatus_EXCEPTION(status)) {
        return status;
    }
    config = _PyInterpreterState_GetConfig(tstate->interp);

    if (config->_init_main) {
        // La phase d’initialisation principale.
        status = pyinit_main(tstate);
        if (_PyStatus_EXCEPTION(status)) {
            return status;
        }
    }

    return _PyStatus_OK();
}

On voit clairement la séparation entre les phases d’initialisation. La phase du noyau est effectuée par pyinit_core(), et la phase principale est effectuée par pyinit_main(). La fonction pyinit_core() initialise le « noyau » de Python. Plus précisément :

  1. Il prépare la configuration : Analyse les arguments de la ligne de commande, lit les variables d’environnement, calcule la configuration du chemin, choisit les encodages des flux standards et du système de fichiers et écrit tout cela à l’endroit approprié dans config.
  2. Il applique la configuration : Configure les flux standards, génère la clé de hachage secrète, crée l’interpreter state principal et le thread state principal, initialise et prends le GIL, active le GC, initialise les types standards et les exceptions, initialise les modules sys et builtins et configure le système d’importation des modules standards et figés.

Au cours de la première étape, CPython calcule config.module_search_paths, qui sera ultérieurement copié dans sys.path. Cette étape n’est pas très intéressante, alors concentrons-nous sur pyinit_config() que pyinit_core() appelle pour effectuer la deuxième étape :

static PyStatus
pyinit_config(_PyRuntimeState *runtime,
              PyThreadState **tstate_p,
              const PyConfig *config)
{
    // Défini les variables globales `Py_*` depuis `config`.
    // Initialise les flux standards C (`stdin`, `stdout`, `stderr`).
    // Défini la clé secrète de hashage.
    PyStatus status = pycore_init_runtime(runtime, config);
    if (_PyStatus_EXCEPTION(status)) {
        return status;
    }

    PyThreadState *tstate;
    // Créé l’*interpreter state* principal et le *thread state* principal.
    // Prends le GIL.
    status = pycore_create_interpreter(runtime, config, &tstate);
    if (_PyStatus_EXCEPTION(status)) {
        return status;
    }
    *tstate_p = tstate;

    // Initialise les types, exceptions, `sys`, `builtins`, `importlib`, etc.
    status = pycore_interp_init(tstate);
    if (_PyStatus_EXCEPTION(status)) {
        return status;
    }

    /* C’est seulement une fois ici que le noyau du runtime est initialisé. */
    runtime->core_initialized = 1;
    return _PyStatus_OK();
}

Premièrement, pycore_init_runtime() copie certains des champs de config dans les variables de configuration globales correspondantes. Ces variables étaient utilisées pour configurer CPython avant l’introduction de PyConfig et continuent de faire partie de l’API Python/C.

Ensuite, pycore_init_runtime() définit les modes de mise en mémoire tampon pour les pointeurs de fichiers stdio, stdout et stderr. Sur les systèmes de type Unix, cela se fait en appelant la fonction de bibliothèque setvbuf().

Enfin, pycore_init_runtime() génère la clé de hachage secrète stockée dans la variable globale _Py_HashSecret. Cette clé est prise avec l’entrée par la fonction de hachage SipHash24, que CPython utilise pour calculer les hachages. La clé est générée aléatoirement à chaque démarrage de CPython. Le but de la randomisation est de protéger une application Python contre les attaques DoS par collision de hachage. Python et de nombreux autres langages, notamment PHP, Ruby, JavaScript et C#, étaient autrefois vulnérables à de telles attaques. Un attaquant pouvait envoyer un ensemble de chaînes avec le même hachage à une application et augmenter considérablement le temps processeur nécessaire pour placer ces chaînes dans le dictionnaire, car elles se trouvent toutes dans le même compartiment. La solution est de fournir une fonction de hachage avec la clé générée aléatoirement inconnue de l’attaquant. Python permet également de générer une clé de façon déterministe en définissant la variable d’environnement PYTHONHASHSEED à une valeur fixe. Pour en savoir plus sur ce type d’attaque, consultez cette présentation. Pour en savoir plus sur l’algorithme de hachage de CPython, consultez PEP 456.

Dans la première partie, nous avons vu que CPython utilise un thread state pour stocker des données spécifiques au thread, telles que la pile d’appels et l’exception state, et un interpreter state pour stocker des données spécifiques à l’interpréteur, telles que des modules chargés et des paramètres d’importation. La fonction pycore_create_interpreter() crée l’interpreter state et le thread state du thread principal de l’OS. Nous n’avons pas encore vu à quoi ressemblent ces structures, voici donc la définition de la structure interpreter state :

// Le typedef de PyInterpreterState est dans Include/pystate.h.
struct _is {

    // `_PyRuntime.interpreters.head` stocke l’interpréteur le plus récemment créé.
    // `next` permet d’accéder à tous les interpréteurs.
    struct _is *next;
    // `tstate_head` pointe vers le thread state le plus récemment créé.
    // Les thread states d’un même interpréteur sont liés entre eux.
    struct _ts *tstate_head;

    /* Référence à la variable globale `_PyRuntime`. Ce champ existe
       pour ne pas avoir à passer le runtime en plus de `tstate` à une fonction.
       On récupère le runtime depuis `tstate`: `tstate->interp->runtime`. */
    struct pyruntimestate *runtime;

    int64_t id;
    // Sert à compter les références de l’interpréteur.
    int64_t id_refcount;
    int requires_idref;
    PyThread_type_lock id_mutex;

    int finalizing;

    struct _ceval_state ceval;
    struct _gc_runtime_state gc;

    PyObject *modules;  // `sys.modules` pointe dessus.
    PyObject *modules_by_index;
    PyObject *sysdict;  // `sys.__dict__` pointe dessus.
    PyObject *builtins; // `builtins.__dict__` pointe dessus.
    PyObject *importlib;

    // Une liste de fonction de recherche de codecs (encodeurs/décodeurs).
    PyObject *codec_search_path;
    PyObject *codec_search_cache;
    PyObject *codec_error_registry;
    int codecs_initialized;

    struct _Py_unicode_state unicode;

    PyConfig config;

    PyObject *dict;  /* Stores per-interpreter state */

    PyObject *builtins_copy;
    PyObject *import_func;
    /* Initialisé dans `PyEval_EvalFrameDefault()`. */
    _PyFrameEvalFunction eval_frame;

    // Voir le module `atexit` pour plus d’informations.
    void (*pyexitfunc)(PyObject *);
    PyObject *pyexitmodule;

    uint64_t tstate_next_unique_id;

    // Voir le module `warnings` pour plus d’informations.
    struct _warnings_runtime_state warnings;

    // Une liste de audit hooks, voir `sys.addaudithook` pour plus d’informations.
    PyObject *audit_hooks;

#if _PY_NSMALLNEGINTS + _PY_NSMALLPOSINTS > 0
    // Les petits entiers sont pré-alloués dans ce tableau pour pouvoir être partagé.
    // L’intervalle par défaut est [-5, 256].
    PyLongObject* small_ints[_PY_NSMALLNEGINTS + _PY_NSMALLPOSINTS];
#endif

  // ... moins intéressant pour le moment.
};

La chose importante à noter ici est que config appartient à l’interpreter state. La configuration lue précédemment est stockée dans le champ config de l’interpreter state nouvellement créé. La structure de thread state est définie comme suit :

// Le typedef de PyThreadState est dans Include/pystate.h.
struct _ts {

    // Une liste doublement-liée est utilisée pour accéder aux thread states appartenant à l’interpréteur.
    struct _ts *prev;
    struct _ts *next;
    PyInterpreterState *interp;

    // Référence au frame object courant (il peut être NULL).
    // La pile d’appel accessible via `frame->f_back`.
    PyFrameObject *frame;

    // ... vérifie si le niveau de recursion est trop profond.

    // ... traçage/profilage.

    /* L’exception actuellement levée. */
    PyObject *curexc_type;
    PyObject *curexc_value;
    PyObject *curexc_traceback;

    /* L’exception actuellement gérée, si aucune coroutines/generateurs n’est
     * présent. Le dernier élément de la pile référencé est toujours `exc_info`.
     */
    _PyErr_StackItem exc_state;

    /* Pointeur vers le haut de la pile des exceptions actuellement gérées. */
    _PyErr_StackItem *exc_info;

    PyObject *dict;  /* Stock les states par thread. */

    int gilstate_counter;

    PyObject *async_exc; /* Exception asynchrone à lever. */
    unsigned long thread_id; /* Id du thread où ce `tstate` a été créé. */

    /* Id unique du thread state. */
    uint64_t id;

    // ... moins intéressant pour le moment.
};

Après avoir créé le thread state du thread principal de l’OS, pycore_create_interpreter() initialise le GIL, qui empêche plusieurs threads de travailler avec des objets Python en même temps. Si vous créez un nouveau thread via le module threading, il exécute la cible donnée dans la boucle d’évaluation. Dans ce cas, les threads attendent le GIL et le prennent au début de chaque itération de la boucle d’évaluation. Un thread peut accéder à son threading state, ce dernier étant passé comme argument à la fonction d’évaluation. Cependant, si vous écrivez une extension C et appelez l’API Python/C pour prendre le GIL, CPython doit non seulement prendre le GIL, mais également associer le thread courant au thread state correspondant. Ceci est fait en stockant un thread state au moment de sa création dans le stockage spécifique au thread (voir la fonction de la bibliothèque standard pthread_setspecific() pour les systèmes Unix). C’est le mécanisme qui permet à n’importe quel thread d’accéder à son thread state. Le GIL mérite un billet séparé. Il en va de même pour le système d’objets Python et le mécanisme d’importation, que nous mentionnerons également brièvement dans cet article.

Après la création du premier interpreter state et du premier thread state, pyinit_config() appelle pycore_interp_init() pour terminer la phase d’initialisation du noyau. Le code de pycore_interp_init() est explicite :

static PyStatus
pycore_interp_init(PyThreadState *tstate)
{
    PyStatus status;
    PyObject *sysmod = NULL;

    status = pycore_init_types(tstate);
    if (_PyStatus_EXCEPTION(status)) {
        goto done;
    }

    status = _PySys_Create(tstate, &sysmod);
    if (_PyStatus_EXCEPTION(status)) {
        goto done;
    }

    status = pycore_init_builtins(tstate);
    if (_PyStatus_EXCEPTION(status)) {
        goto done;
    }

    status = pycore_init_import_warnings(tstate, sysmod);

done:
    // Py_XDECREF() diminue le compteur de référence d’un objet.
    // À 0, l’objet est désalloué.
    Py_XDECREF(sysmod);
    return status;
}

La fonction pycore_init_types() initialise les types standards. Mais qu’entend-on par là ? Et que sont vraiment ces types ? Comme vous le savez probablement, tout ce avec quoi vous travaillez en Python est un objet. Les nombres, les strings, les listes, les fonctions, les modules, les frame objects, les classes personnalisées et les types standards sont tous des objets Python. Un objet Python est une instance de la structure PyObject ou une instance de toute autre structure C dont le premier champ est de type PyObject. La structure PyObject a deux champs. Le premier champ, de type Py_ssize_t, stocke le nombre de références. Le deuxième champ, de type PyTypeObject, pointe vers le type Python de l’objet. Voici la définition de PyObject :

typedef struct _object {
    _PyObject_HEAD_EXTRA // pour le débogage uniquement
    Py_ssize_t ob_refcnt;
    PyTypeObject *ob_type;
} PyObject;

Et voici l’exemple d’un objet Python plus familier, le float :

typedef struct {
    PyObject_HEAD // macro that expands to PyObject `ob_base`;
    double ob_fval;
} PyFloatObject

Le standard C stipule qu’un pointeur vers n’importe quelle structure peut être converti en pointeur vers son premier membre et vice versa. Sachant que tout objet Python a un PyObject comme premier membre, CPython peut traiter n’importe quel objet Python comme un PyObject, ce qu’il fait partout. Vous pouvez le considérer comme une manière C de faire des sous-classes. L’avantage d’une telle astuce est qu’elle permet de réaliser le polymorphisme. Par exemple, il permet d’écrire une fonction qui peut prendre n’importe quel objet Python comme argument en prenant un PyObject.

La raison pour laquelle CPython peut faire des choses avec un PyObject est que le comportement d’un objet Python est déterminé par son type, et un PyObject a toujours un type. Un type « sait » comment créer les objets de ce type, comment calculer leurs hachages, comment les additionner, comment les appeler, comment accéder à leurs attributs, comment les désallouer et bien plus encore. Les types sont également des objets Python représentés par la structure PyTypeObject. Tous les types ont le même type, à savoir PyType_Type. Et le type de PyType_Type pointe vers PyType_Type lui-même. Si cette explication semble compliquée, cet exemple devrait vous permettre d’y voir plus clair :

$ ./python.exe -q
>>> type([])
<class 'list'>
>>> type(type([]))
<class 'type'>
>>> type(type(type([])))
<class 'type'>

Les champs de PyTypeObject sont très bien documentés dans le manuel de référence de l’API Python/C. Je ne laisse ici que la définition de la structure sous-jacente à PyTypeObject pour vous donner une idée de la quantité d’informations qu’un type Python stocke :

// PyTypeObject est un typedef de la structure _typeobject
struct _typeobject {
    PyObject_VAR_HEAD // expands to
                      // PyObject ob_base;
                      // Py_ssize_t ob_size;
    const char *tp_name; /* For printing, in format "<module>.<name>" */
    Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */

    /* Methods to implement standard operations */

    destructor tp_dealloc;
    Py_ssize_t tp_vectorcall_offset;
    getattrfunc tp_getattr;
    setattrfunc tp_setattr;
    PyAsyncMethods *tp_as_async; /* formerly known as tp_compare (Python 2)
                                    or tp_reserved (Python 3) */
    reprfunc tp_repr;

    /* Method suites for standard classes */
    PyNumberMethods *tp_as_number;
    PySequenceMethods *tp_as_sequence;
    PyMappingMethods *tp_as_mapping;

    /* More standard operations (here for binary compatibility) */

    hashfunc tp_hash;
    ternaryfunc tp_call;
    reprfunc tp_str;
    getattrofunc tp_getattro;
    setattrofunc tp_setattro;

    /* Functions to access object as input/output buffer */
    PyBufferProcs *tp_as_buffer;

    /* Flags to define presence of optional/expanded features */
    unsigned long tp_flags;

    const char *tp_doc; /* Documentation string */

    /* Assigned meaning in release 2.0 */
    /* call function for all accessible objects */
    traverseproc tp_traverse;

    /* delete references to contained objects */
    inquiry tp_clear;

    /* Assigned meaning in release 2.1 */
    /* rich comparisons */
    richcmpfunc tp_richcompare;

    /* weak reference enabler */
    Py_ssize_t tp_weaklistoffset;

    /* Iterators */
    getiterfunc tp_iter;
    iternextfunc tp_iternext;

    /* Attribute descriptor and subclassing stuff */
    struct PyMethodDef *tp_methods;
    struct PyMemberDef *tp_members;
    struct PyGetSetDef *tp_getset;
    struct _typeobject *tp_base;
    PyObject *tp_dict;
    descrgetfunc tp_descr_get;
    descrsetfunc tp_descr_set;
    Py_ssize_t tp_dictoffset;
    initproc tp_init;
    allocfunc tp_alloc;
    newfunc tp_new;
    freefunc tp_free; /* Low-level free-memory routine */
    inquiry tp_is_gc; /* For PyObject_IS_GC */
    PyObject *tp_bases;
    PyObject *tp_mro; /* method resolution order */
    PyObject *tp_cache;
    PyObject *tp_subclasses;
    PyObject *tp_weaklist;
    destructor tp_del;

    /* Type attribute cache version tag. Added in version 2.6 */
    unsigned int tp_version_tag;

    destructor tp_finalize;
    vectorcallfunc tp_vectorcall;
};

Les types standards, tels que int et list, sont implémentés en définissant statiquement des instances de PyTypeObject, comme ici pour la list :

PyTypeObject PyList_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "list",
    sizeof(PyListObject),
    0,
    (destructor)list_dealloc,                   /* tp_dealloc */
    0,                                          /* tp_vectorcall_offset */
    0,                                          /* tp_getattr */
    0,                                          /* tp_setattr */
    0,                                          /* tp_as_async */
    (reprfunc)list_repr,                        /* tp_repr */
    0,                                          /* tp_as_number */
    &list_as_sequence,                          /* tp_as_sequence */
    &list_as_mapping,                           /* tp_as_mapping */
    PyObject_HashNotImplemented,                /* tp_hash */
    0,                                          /* tp_call */
    0,                                          /* tp_str */
    PyObject_GenericGetAttr,                    /* tp_getattro */
    0,                                          /* tp_setattro */
    0,                                          /* tp_as_buffer */
    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC |
        Py_TPFLAGS_BASETYPE | Py_TPFLAGS_LIST_SUBCLASS, /* tp_flags */
    list___init____doc__,                       /* tp_doc */
    (traverseproc)list_traverse,                /* tp_traverse */
    (inquiry)_list_clear,                       /* tp_clear */
    list_richcompare,                           /* tp_richcompare */
    0,                                          /* tp_weaklistoffset */
    list_iter,                                  /* tp_iter */
    0,                                          /* tp_iternext */
    list_methods,                               /* tp_methods */
    0,                                          /* tp_members */
    0,                                          /* tp_getset */
    0,                                          /* tp_base */
    0,                                          /* tp_dict */
    0,                                          /* tp_descr_get */
    0,                                          /* tp_descr_set */
    0,                                          /* tp_dictoffset */
    (initproc)list___init__,                    /* tp_init */
    PyType_GenericAlloc,                        /* tp_alloc */
    PyType_GenericNew,                          /* tp_new */
    PyObject_GC_Del,                            /* tp_free */
    .tp_vectorcall = list_vectorcall,
};

CPython doit également initialiser les types standards (l’origine de notre discussion). Chaque type nécessite une initialisation pour, par exemple, ajouter des méthodes spéciales, comme __call__ et __eq__, au dictionnaire du type et les faire pointer vers les fonctions tp_ * correspondantes. Cette initialisation commune se fait en appelant PyType_Ready() pour chaque type :

PyStatus
_PyTypes_Init(void)
{
    // Les noms des méthodes spéciales "__hash__", "__call_", etc. sont définis par cet appel.
    PyStatus status = _PyTypes_InitSlotDefs();
    if (_PyStatus_EXCEPTION(status)) {
        return status;
    }

#define INIT_TYPE(TYPE, NAME) \
    do { \
        if (PyType_Ready(TYPE) < 0) { \
            return _PyStatus_ERR("Can't initialize " NAME " type"); \
        } \
    } while (0)

    INIT_TYPE(&PyBaseObject_Type, "object");
    INIT_TYPE(&PyType_Type, "type");
    INIT_TYPE(&_PyWeakref_RefType, "weakref");
    INIT_TYPE(&_PyWeakref_CallableProxyType, "callable weakref proxy");
    INIT_TYPE(&_PyWeakref_ProxyType, "weakref proxy");
    INIT_TYPE(&PyLong_Type, "int");
    INIT_TYPE(&PyBool_Type, "bool");
    INIT_TYPE(&PyByteArray_Type, "bytearray");
    INIT_TYPE(&PyBytes_Type, "str");
    INIT_TYPE(&PyList_Type, "list");
    INIT_TYPE(&_PyNone_Type, "None");
    INIT_TYPE(&_PyNotImplemented_Type, "NotImplemented");
    INIT_TYPE(&PyTraceBack_Type, "traceback");
    INIT_TYPE(&PySuper_Type, "super");
    INIT_TYPE(&PyRange_Type, "range");
    INIT_TYPE(&PyDict_Type, "dict");
    INIT_TYPE(&PyDictKeys_Type, "dict keys");
    // ... un peu plus de 50 types.
    return _PyStatus_OK();

#undef INIT_TYPE
}

Certains types standards nécessitent une initialisation additionnelle, spécifique au type. Par exemple, l’initialisation de int est nécessaire pour pré-allouer des petits entiers dans le tableau interp->small_ints afin qu’ils puissent être réutilisés, et l’initialisation de float est nécessaire pour déterminer comment la machine actuelle représente le nombre à virgule flottante.

Quand les types standards sont initialisés, pycore_interp_init() appelle _PySys_Create() pour créer le module sys. Pourquoi le module sys est-il le premier à être créé ? Il est bien sûr très important, car il contient des éléments tels que les arguments de ligne de commande passés au programme (sys.argv), la liste des chemins de recherche des modules (sys.path), beaucoup de paramètres spécifiques au système et à l’implémentation ; sys.version, sys.implementation, sys.thread_info, etc. Et diverses fonctions permettant d’interagir avec l’interpréteur ; sys.addaudithook(), sys.settrace(), etc. Mais la principale raison pour laquelle on crée le module sys si tôt c’est l’initialisation de sys.modules. Il pointe vers le dictionnaire interp->modules, qui est également créé par _PySys_Create(), et agit comme un cache pour tous les modules importés. C’est le premier endroit pour rechercher un module, et c’est l’endroit où tous les modules chargés sont enregistrés. Le système d’importation repose fortement sur sys.modules.

Après l’appel à _PySys_Create(), le module sys n’est que partiellement initialisé. Les fonctions et la plupart des variables sont disponibles, mais les données spécifiques à l’appel, telles que sys.argv et sys._xoptions, et la configuration liée au chemin, telle que sys.path et sys.exec_prefix, seront définies lors de la phase principale d’initialisation.

Lorsque le module sys est créé, pycore_interp_init() appelle pycore_init_builtins() pour initialiser le module builtins. Les fonctions standards, comme abs(), dir() etprint(), les types standards, comme dict, int et str, les exceptions standards, comme Exception et ValueError, et les constantes standards, comme False, Ellipsis et None, sont tous membres du module builtins. Les fonctions standards font partie de la définition du module, mais d’autres membres doivent être placés explicitement dans le dictionnaire du module. La fonction pycore_init_builtins() fait cela. Plus tard, frame->f_builtins pointera sur ce dictionnaire pour rechercher par noms. C’est la raison pour laquelle nous n’avons pas besoin d’importer builtins explicitement.

La dernière étape de la phase d’initialisation du noyau est effectuée par la fonction pycore_init_import_warnings(). Vous savez probablement que Python dispose d’un mécanisme pour émettre des avertissements, comme ici :

$ ./python.exe -q
>>> import imp
<stdin>:1: DeprecationWarning: the imp module is deprecated in favour of importlib; ...

Les avertissements peuvent être ignorés, transformés en exceptions et affichés de différentes manières. CPython dispose de filtres pour cela. Certains filtres sont activés par défaut et la fonction pycore_init_import_warnings() est celle qui les active. Le plus important, c’est que pycore_init_import_warnings() configure le système d’importation pour les modules standards et figés.

Les modules standards et figés sont deux types spéciaux de modules. Ils ont en commun d’être compilés directement dans l’exécutable python. La différence est que les modules standards sont écrits en C, tandis que les modules figés sont écrits en Python. Comment est-il possible de compiler un module écrit en Python dans l’exécutable ? Ceci est intelligemment fait en intégrant la représentation binaire du code object d’un module dans le code source C. C’est l’utilitaire Freeze qui est utilisé pour générer cette représentation.

_frozen_importlib est un exemple de module figé. C’est le noyau du système d’importation. L’instruction Python import amène à la fonction _frozen_importlib._find_and_load(). Pour prendre en charge l’importation des modules standards et figés, pycore_init_import_warnings() appelle init_importlib(), et la toute première chose que fait init_importlib() est d’importer _frozen_importlib. On pourrait croire que CPython doive paradoxalement importer _frozen_importlib pour importer _frozen_importlib, mais ce n’est pas le cas. Le module _frozen_importlib fait partie de l’API universelle d’import de n’importe quel module. Cependant, Si CPython sait qu’il doit importer un module figé, il peut le faire sans s’appuyer sur _frozen_importlib.

Le module _frozen_importlib dépend de deux autres modules. Il a d’abord besoin du module sys pour accéder à sys.modules. Il a ensuite besoin du module _imp, qui implémente des fonctions d’importation bas niveau, y compris les fonctions de création des modules standards et figés. Le problème est que _frozen_importlib ne peut importer aucun module, car l’instruction import dépend de _frozen_importlib lui-même. La solution est de créer le module _imp depuis init_importlib() et de l’injecter, ainsi que le module sys, dans _frozen_importlib en appelant _frozen_importlib._install(sys, _imp). Cet amorçage du système d’importation met fin à la phase d’initialisation principale.

Nous quittons pyinit_core() et entrons dans pyinit_main(), qui effectue la phase d’initialisation principale. Nous constatons qu’il fait quelques vérifications et appelle init_interp_main() pour faire le travail. Ce travail peut être résumé comme suit :

  1. Récupérer les horloges en temps réel et monotoniques du système, s’assurer que time.time(), time.monotonic() et time.perf_counter() fonctionneront correctement.
  2. Terminez l’initialisation du module sys. Cela inclut la définition des variables de configuration du chemin, telles que sys.path, sys.executable et sys.exec_prefix, et des variables spécifiques à l’appel du programme, telles que sys.argv et sys._xoptions.
  3. Ajouter la prise en charge de l’importation de modules (externes) basés sur les chemins. Cela se fait par l’import d’un autre module figé appelé importlib._bootstrap_external. Il permet l’importation des modules basés sur sys.path. De plus, le module figé zipimport est importé. Il permet l’importation de modules à partir d’archives ZIP, c’est-à-dire que les répertoires répertoriés dans le sys.path peuvent être des archives ZIP.
  4. Normaliser les noms des encodages du système de fichiers et des flux standards. Définir les gestionnaires d’erreurs pour l’encodage et le décodage lors de l’utilisation du système de fichiers.
  5. Installez les gestionnaires de signaux par défaut. Ce sont des gestionnaires qui sont exécutés lorsqu’un processus reçoit un signal comme SIGINT. Les gestionnaires personnalisés peuvent être configurés à l’aide du module signal.
  6. Importer le module io et initialiser sys.stdin, sys.stdout et sys.stderr. Cela se fait essentiellement en appelant io.open() sur les descripteurs de fichiers des flux standards.
  7. Faire pointer builtins.open sur io.OpenWrapper, afin que open() soit disponible en tant que fonction standard.
  8. Créer le module __main__, définir __main __.__ builtins__ sur builtins et __main __.__ loader__ sur _frozen_importlib.BuiltinImporter. À ce stade, le module __main__ ne contient rien d’autre.
  9. Importez les modules warning et site. Le module site ajoute à sys.path les répertoires spécifiques au site. C’est pourquoi sys.path contient normalement le répertoire avec les modules installés, comme /usr/local/lib/python3.9/site-packages/.
  10. Définir interp-> runtime->initialized = 1.

L’initialisation de CPython est terminée. La fonction pymain_init() se termine, et nous entrons dans Py_RunMain() pour voir ce que CPython fait avant d’entrer dans la boucle d’évaluation.

Exécuter un programme Python

La fonction Py_RunMain() semble bien vide :

int
Py_RunMain(void)
{
    int exitcode = 0;

    pymain_run_python(&exitcode);

    if (Py_FinalizeEx() < 0) {
        /* Valeur dont il est peu probable qu’elle puisse être confondue avec un status de sortie valide
           ou un ayant une autre signification particulière. */
        exitcode = 120;
    }

    // Libère la mémoire qui n’est pas libérée par `Py_FinalizeEx()`.
    pymain_free();

    if (_Py_UnhandledKeyboardInterrupt) {
        exitcode = exit_sigint();
    }

    return exitcode;
}

Py_RunMain() appelle d’abords pymain_run_python() pour exécuter Python. Ensuite, il appelle Py_FinalizeEx() pour annuler l’initialisation. La fonction Py_FinalizeEx() libèrent autant de mémoire utilisé par CPython que possible, et le reste est libéré par pymain_free(). Une des raisons pour laquelle on « finalise » CPython est de pouvoir appeler les fonctions de sortie, y compris les fonctions enregistrées avec le module atexit.

Comme vous le savez probablement, il existe plusieurs façons d’exécuter python, à savoir :

Interactivement :

$ ./cpython/python.exe
>>> import sys
>>> sys.path[:1]
['']

Depuis stdin :

$ echo "import sys; print(sys.path[:1])" | ./cpython/python.exe
['']

Sous forme de commande :

$ ./cpython/python.exe -c "import sys; print(sys.path[:1])"
['']

Sous forme de script :

$ ./cpython/python.exe 03/print_path0.py
['/Users/Victor/Projects/tenthousandmeters/python_behind_the_scenes/03']

Sous forme de module :

$ ./cpython/python.exe -m 03.print_path0
['/Users/Victor/Projects/tenthousandmeters/python_behind_the_scenes']

Et, d’une façon moins connue, sous forme de package scripté (ici, print_path0_package est un répertoire avec un __main__.py) :

$ ./cpython/python.exe 03/print_path0_package
['/Users/Victor/Projects/tenthousandmeters/python_behind_the_scenes/03/print_path0_package']

Remarquez que je suis remonté d’un dossier depuis cpython/ pour montrer que les différents modes d’invocation entraînent différentes valeurs de sys.path[0]. La seconde fonction, pymain_run_python(), calcule la valeur de sys.path[0], l’ajoute à sys.path et exécute Python dans le mode approprié suivant config :

static void
pymain_run_python(int *exitcode)
{
    PyInterpreterState *interp = _PyInterpreterState_GET();
    PyConfig *config = (PyConfig*)_PyInterpreterState_GetConfig(interp);

    // Ajoute le chemin de recherche à `sys.path`
    PyObject *main_importer_path = NULL;
    if (config->run_filename != NULL) {
        // Génère le chemin de recherche dans le cas où `filename` est un package
        // (un répertoire ou un fichier ZIP) contenant __main__.py, et le stock dans `main_importer_path`.
        // Ou bien laisse `main_importer_path` inchangé.
        // Gère les autres cas plus loin.
        if (pymain_get_importer(config->run_filename, &main_importer_path,
                                exitcode)) {
            return;
        }
    }

    if (main_importer_path != NULL) {
        if (pymain_sys_path_add_path0(interp, main_importer_path) < 0) {
            goto error;
        }
    }
    else if (!config->isolated) {
        PyObject *path0 = NULL;
        // Génère le chemin de recherche qui sera ajouté à `sys.path`.
        // Si on exécute un script, il s’agit du répertoire d’où le script est chargé.
        // Si on exécute un module (`-m`), il s’agit du répertoire de travail courant.
        // Sinon, c’est une string vide.
        int res = _PyPathConfig_ComputeSysPath0(&config->argv, &path0);
        if (res < 0) {
            goto error;
        }

        if (res > 0) {
            if (pymain_sys_path_add_path0(interp, path0) < 0) {
                Py_DECREF(path0);
                goto error;
            }
            Py_DECREF(path0);
        }
    }

    PyCompilerFlags cf = _PyCompilerFlags_INIT;

    // Affiche la version et la plateforme en mode interactif.
    pymain_header(config);
    // Importe le module `readline` fournissant l’autocomplétion,
    // l’édition de ligne et d’historique en mode interactif.
    pymain_import_readline(config);

    // Exécute Python suivant le mode d’invocation (script, -m, -c, etc.).
    if (config->run_command) {
        *exitcode = pymain_run_command(config->run_command, &cf);
    }
    else if (config->run_module) {
        *exitcode = pymain_run_module(config->run_module, 1);
    }
    else if (main_importer_path != NULL) {
        *exitcode = pymain_run_module(L"__main__", 0);
    }
    else if (config->run_filename != NULL) {
        *exitcode = pymain_run_file(config, &cf);
    }
    else {
        *exitcode = pymain_run_stdin(config, &cf);
    }

    // Entre en mode interactif après avoir exécuté le programme.
    // Activé par `-i` et `PYTHONINSPECT`.
    pymain_repl(config, &cf, exitcode);
    goto done;

error:
    *exitcode = pymain_exit_err_print();

done:
    Py_XDECREF(main_importer_path);
}

Nous ne suivrons pas tous les branchements, mais on partira du principe qu’on exécute un programme Python en tant que script. Cela nous conduit à la fonction pymain_run_file(), qui vérifie si le fichier spécifié peut être ouvert, s’assure qu’il ne s’agit pas d’un répertoire et appelle PyRun_AnyFileExFlags(). La fonction PyRun_AnyFileExFlags() gère un cas particulier, lorsque le fichier est un terminal (isatty(fd) renvoie 1). Si tel est le cas, il entre en mode interactif :

$ ./python.exe /dev/ttys000
>>> 1 + 1
2

Sinon, il appelle PyRun_SimpleFileExFlags(). Vous avez sûrement déjà dû voir des fichiers .pyc apparaissant constamment dans les répertoires __pycache__ à côté de vos modules. Un fichier .pyc contient le code source compilé d’un programme Python, c’est-à-dire l’object code « marshalé »2 d’un module. Grâce aux fichiers .pyc, nous n’avons pas besoin de recompiler les modules à chaque fois que nous les importons. Je suppose que vous êtes déjà au courant, mais sachez qu’il est possible d’exécuter un fichier .pyc directement :

$ ./cpython/python.exe 03/__pycache__/print_path0.cpython-39.pyc
['/Users/Victor/Projects/tenthousandmeters/python_behind_the_scenes/03/__pycache__']

The PyRun_SimpleFileExFlags() function implements this logic. It checks whether the file is a .pyc file, whether it's compiled for the current CPython version and, if yes, calls run_pyc_file(). If the file is not a .pyc file, it calls PyRun_FileExFlags(). Most importantly, though, is that PyRun_SimpleFileExFlags() imports the main module and passes main's dictionary to PyRun_FileExFlags() as the global and the local namespace in which the file will be executed:

La fonction PyRun_SimpleFileExFlags() implémente cette logique. Elle regarde si le fichier est un .pyc, s’il est compilé pour la version actuelle de CPython et si oui, appelle run_pyc_file(). Si le fichier n’est pas un .pyc, elle appelle PyRun_FileExFlags(). Le plus important, cependant, est que PyRun_SimpleFileExFlags() importe le module __main__ et transmet le dictionnaire de __main__ à PyRun_FileExFlags() en tant que namespace global et local dans lequel le fichier sera exécuté :

int
PyRun_SimpleFileExFlags(FILE *fp, const char *filename, int closeit,
                        PyCompilerFlags *flags)
{
    PyObject *m, *d, *v;
    const char *ext;
    int set_file_name = 0, ret = -1;
    size_t len;

    m = PyImport_AddModule("__main__");
    if (m == NULL)
        return -1;
    Py_INCREF(m);
    d = PyModule_GetDict(m);

    if (PyDict_GetItemString(d, "__file__") == NULL) {
        PyObject *f;
        f = PyUnicode_DecodeFSDefault(filename);
        if (f == NULL)
            goto done;
        if (PyDict_SetItemString(d, "__file__", f) < 0) {
            Py_DECREF(f);
            goto done;
        }
        if (PyDict_SetItemString(d, "__cached__", Py_None) < 0) {
            Py_DECREF(f);
            goto done;
        }
        set_file_name = 1;
        Py_DECREF(f);
    }

    // Regarde si un fichier .pyc est passé.
    len = strlen(filename);
    ext = filename + len - (len > 4 ? 4 : 0);
    if (maybe_pyc_file(fp, filename, ext, closeit)) {
        FILE *pyc_fp;
        /* Essai d’exécuter le fichier pyc. Le rouvre en mode binaire. */
        if (closeit)
            fclose(fp);
        if ((pyc_fp = _Py_fopen(filename, "rb")) == NULL) {
            fprintf(stderr, "python: Can't reopen .pyc file\n");
            goto done;
        }

        if (set_main_loader(d, filename, "SourcelessFileLoader") < 0) {
            fprintf(stderr, "python: failed to set __main__.__loader__\n");
            ret = -1;
            fclose(pyc_fp);
            goto done;
        }
        v = run_pyc_file(pyc_fp, filename, d, d, flags);
    } else {
        /* Laissez __main __.__ loader__ seul lors de l’exécution depuis stdin. */
        if (strcmp(filename, "<stdin>") != 0 &&
            set_main_loader(d, filename, "SourceFileLoader") < 0) {
            fprintf(stderr, "python: failed to set __main__.__loader__\n");
            ret = -1;
            goto done;
        }
        v = PyRun_FileExFlags(fp, filename, Py_file_input, d, d,
                              closeit, flags);
    }
    flush_io();
    if (v == NULL) {
        Py_CLEAR(m);
        PyErr_Print();
        goto done;
    }
    Py_DECREF(v);
    ret = 0;
  done:
    if (set_file_name) {
        if (PyDict_DelItemString(d, "__file__")) {
            PyErr_Clear();
        }
        if (PyDict_DelItemString(d, "__cached__")) {
            PyErr_Clear();
        }
    }
    Py_XDECREF(m);
    return ret;
}

La fonction PyRun_FileExFlags() démarre le processus de compilation. Il lance le parser, récupère l’AST du module et appelle run_mod() pour exécuter l’AST. Il crée également un objet PyArena, que CPython utilise pour allouer des petits objets (plus petits ou égaux à 512 octets) :

PyObject *
PyRun_FileExFlags(FILE *fp, const char *filename_str, int start, PyObject *globals,
                  PyObject *locals, int closeit, PyCompilerFlags *flags)
{
    PyObject *ret = NULL;
    mod_ty mod;
    PyArena *arena = NULL;
    PyObject *filename;
    int use_peg = _PyInterpreterState_GET()->config._use_peg_parser;

    filename = PyUnicode_DecodeFSDefault(filename_str);
    if (filename == NULL)
        goto exit;

    arena = PyArena_New();
    if (arena == NULL)
        goto exit;

    // Lance le parser.
    // Par défaut, c’est le nouveau parser, PEG, qui est utilisé.
    // Il faut passer `-X oldparser` pour utiliser l’ancien.
    // `mod` signifie « module ». C’est le nœud à la racine de l’AST.
    if (use_peg) {
        mod = PyPegen_ASTFromFileObject(fp, filename, start, NULL, NULL, NULL,
                                        flags, NULL, arena);
    }
    else {
        mod = PyParser_ASTFromFileObject(fp, filename, NULL, start, 0, 0,
                                         flags, NULL, arena);
    }

    if (closeit)
        fclose(fp);
    if (mod == NULL) {
        goto exit;
    }
    // Compile l’AST et le lance.
    ret = run_mod(mod, filename, globals, locals, flags, arena);

exit:
    Py_XDECREF(filename);
    if (arena != NULL)
        PyArena_Free(arena);
    return ret;
}

run_mod() exécute le compilateur en appelant PyAST_CompileObject(), récupère le code object du module et appelle run_eval_code_obj() pour exécuter le code object. Durant l’interval, il déclenche l’événement exec, qui est la façon pour CPython de notifier les outils d’audit lorsque quelque chose d’important se produit dans le runtime Python. La PEP 578 explique ce mécanisme.

static PyObject *
run_mod(mod_ty mod, PyObject *filename, PyObject *globals, PyObject *locals,
            PyCompilerFlags *flags, PyArena *arena)
{
    PyThreadState *tstate = _PyThreadState_GET();
    PyCodeObject *co = PyAST_CompileObject(mod, filename, flags, -1, arena);
    if (co == NULL)
        return NULL;

    if (_PySys_Audit(tstate, "exec", "O", co) < 0) {
        Py_DECREF(co);
        return NULL;
    }

    PyObject *v = run_eval_code_obj(tstate, co, globals, locals);
    Py_DECREF(co);
    return v;
}

Nous avons pu voir comment le compilateur fonctionne dans la seconde partie.

  1. Création de la table de symboles.
  2. Création du CFG des blocs de base.
  3. Assemblage de CFG en code object.

C’est exactement ce que fait PyAST_CompileObject(), donc nous ne nous concentrerons pas là-dessus pour l’instant.

run_eval_code_obj() commence une chaîne d’appels de quelques fonctions qui nous amène dans _PyEval_EvalCode(). Je colle toutes ces fonctions ici, pour que vous puissiez voir d’où viennent les paramètres de _PyEval_EvalCode() :

static PyObject *
run_eval_code_obj(PyThreadState *tstate, PyCodeObject *co, PyObject *globals, PyObject *locals)
{
    PyObject *v;
    // Cas particulier où CPython est embarqué. Nous pouvons l’ignorer sans conséquences.
    /*
     * We explicitly re-initialize `_Py_UnhandledKeyboardInterrupt` every eval
     * _just in case_ someone is calling into an embedded Python where they
     * don't care about an uncaught KeyboardInterrupt exception (why didn't they
     * leave `config.install_signal_handlers` set to 0?!?) but then later call
     * `Py_Main()` itself (which _checks_ this flag and dies with a signal after
     * its interpreter exits).  We don't want a previous embedded interpreter's
     * uncaught exception to trigger an unexplained signal exit from a future
     * `Py_Main()` based one.
     */
    _Py_UnhandledKeyboardInterrupt = 0;

    /* Défini globals['__builtins__'] s’il n’existe pas encore. */
    // Dans notre cas, il a déjà été défini sur le module `builtins` durant l’initialisation principale.
    if (globals != NULL && PyDict_GetItemString(globals, "__builtins__") == NULL) {
        if (PyDict_SetItemString(globals, "__builtins__",
                                 tstate->interp->builtins) < 0) {
            return NULL;
        }
    }

    v = PyEval_EvalCode((PyObject*)co, globals, locals);
    if (!v && _PyErr_Occurred(tstate) == PyExc_KeyboardInterrupt) {
        _Py_UnhandledKeyboardInterrupt = 1;
    }
    return v;
}
PyObject *
PyEval_EvalCode(PyObject *co, PyObject *globals, PyObject *locals)
{
    return PyEval_EvalCodeEx(co,
                      globals, locals,
                      (PyObject **)NULL, 0,
                      (PyObject **)NULL, 0,
                      (PyObject **)NULL, 0,
                      NULL, NULL);
}
PyObject *
PyEval_EvalCodeEx(PyObject *_co, PyObject *globals, PyObject *locals,
                  PyObject *const *args, int argcount,
                  PyObject *const *kws, int kwcount,
                  PyObject *const *defs, int defcount,
                  PyObject *kwdefs, PyObject *closure)
{
    return _PyEval_EvalCodeWithName(_co, globals, locals,
                                    args, argcount,
                                    kws, kws != NULL ? kws + 1 : NULL,
                                    kwcount, 2,
                                    defs, defcount,
                                    kwdefs, closure,
                                    NULL, NULL);
}
PyObject *
_PyEval_EvalCodeWithName(PyObject *_co, PyObject *globals, PyObject *locals,
           PyObject *const *args, Py_ssize_t argcount,
           PyObject *const *kwnames, PyObject *const *kwargs,
           Py_ssize_t kwcount, int kwstep,
           PyObject *const *defs, Py_ssize_t defcount,
           PyObject *kwdefs, PyObject *closure,
           PyObject *name, PyObject *qualname)
{
    PyThreadState *tstate = _PyThreadState_GET();
    return _PyEval_EvalCode(tstate, _co, globals, locals,
               args, argcount,
               kwnames, kwargs,
               kwcount, kwstep,
               defs, defcount,
               kwdefs, closure,
               name, qualname);
}

Rappelez-vous qu’un code object décrit ce qu’un morceau de code fait, mais pour exécuter un code object, CPython doit lui créer un state, et c’est ce qu’est un frame object. _PyEval_EvalCode() crée un frame object pour un code object donné avec des paramètres spécifiés. Dans notre cas, la plupart des paramètres sont NULL, il n’y a donc pas grand-chose à faire. Beaucoup plus de travail est nécessaire lorsque CPython exécute, par exemple, le code object d’une fonction en passant différents types d’arguments. En conséquence, _PyEval_EvalCode() fait près de 300 lignes. Nous verrons ce qu’elles font dans les prochaines parties. Vous pouvez aller directement à la fin de _PyEval_EvalCode() et constater qu’il appelle _PyEval_EvalFrame() pour évaluer le frame object créé :

PyObject *
_PyEval_EvalCode(PyThreadState *tstate,
           PyObject *_co, PyObject *globals, PyObject *locals,
           PyObject *const *args, Py_ssize_t argcount,
           PyObject *const *kwnames, PyObject *const *kwargs,
           Py_ssize_t kwcount, int kwstep,
           PyObject *const *defs, Py_ssize_t defcount,
           PyObject *kwdefs, PyObject *closure,
           PyObject *name, PyObject *qualname)
{
    assert(is_tstate_valid(tstate));

    PyCodeObject* co = (PyCodeObject*)_co;
    PyFrameObject *f;
    PyObject *retval = NULL;
    PyObject **fastlocals, **freevars;
    PyObject *x, *u;
    const Py_ssize_t total_args = co->co_argcount + co->co_kwonlyargcount;
    Py_ssize_t i, j, n;
    PyObject *kwdict;

    if (globals == NULL) {
        _PyErr_SetString(tstate, PyExc_SystemError,
                         "PyEval_EvalCodeEx: NULL globals");
        return NULL;
    }

    /* Créé la frame. */
    f = _PyFrame_New_NoTrack(tstate, co, globals, locals);
    if (f == NULL) {
        return NULL;
    }
    fastlocals = f->f_localsplus;
    freevars = f->f_localsplus + co->co_nlocals;

    /* Créé un dictionnaire pour les paramètres des mot-clés (**kwags). */
    if (co->co_flags & CO_VARKEYWORDS) {
        kwdict = PyDict_New();
        if (kwdict == NULL)
            goto fail;
        i = total_args;
        if (co->co_flags & CO_VARARGS) {
            i++;
        }
        SETLOCAL(i, kwdict);
    }
    else {
        kwdict = NULL;
    }

    /* Copie tous les arguments positionnels dans les variables locales. */
    if (argcount > co->co_argcount) {
        n = co->co_argcount;
    }
    else {
        n = argcount;
    }
    for (j = 0; j < n; j++) {
        x = args[j];
        Py_INCREF(x);
        SETLOCAL(j, x);
    }

    /* Ajoute les autres arguments positionnels dans l’argument *args. */
    if (co->co_flags & CO_VARARGS) {
        u = _PyTuple_FromArray(args + n, argcount - n);
        if (u == NULL) {
            goto fail;
        }
        SETLOCAL(total_args, u);
    }

    /* Gère les arguments des mots-clés passés sous la forme de deux tableaux stridés. */
    kwcount *= kwstep;
    for (i = 0; i < kwcount; i += kwstep) {
        PyObject **co_varnames;
        PyObject *keyword = kwnames[i];
        PyObject *value = kwargs[i];
        Py_ssize_t j;

        if (keyword == NULL || !PyUnicode_Check(keyword)) {
            _PyErr_Format(tstate, PyExc_TypeError,
                          "%U() keywords must be strings",
                          co->co_name);
            goto fail;
        }

        /* Speed hack: fait une comparaison de pointeurs raws. Dans la mesure où les noms sont
           normalement internés cela doit pratiquement toujours réussir. */
        co_varnames = ((PyTupleObject *)(co->co_varnames))->ob_item;
        for (j = co->co_posonlyargcount; j < total_args; j++) {
            PyObject *name = co_varnames[j];
            if (name == keyword) {
                goto kw_found;
            }
        }

        /* Fallback lent, au cas où... */
        for (j = co->co_posonlyargcount; j < total_args; j++) {
            PyObject *name = co_varnames[j];
            int cmp = PyObject_RichCompareBool( keyword, name, Py_EQ);
            if (cmp > 0) {
                goto kw_found;
            }
            else if (cmp < 0) {
                goto fail;
            }
        }

        assert(j >= total_args);
        if (kwdict == NULL) {

            if (co->co_posonlyargcount
                && positional_only_passed_as_keyword(tstate, co,
                                                     kwcount, kwnames))
            {
                goto fail;
            }

            _PyErr_Format(tstate, PyExc_TypeError,
                          "%U() got an unexpected keyword argument '%S'",
                          co->co_name, keyword);
            goto fail;
        }

        if (PyDict_SetItem(kwdict, keyword, value) == -1) {
            goto fail;
        }
        continue;

      kw_found:
        if (GETLOCAL(j) != NULL) {
            _PyErr_Format(tstate, PyExc_TypeError,
                          "%U() got multiple values for argument '%S'",
                          co->co_name, keyword);
            goto fail;
        }
        Py_INCREF(value);
        SETLOCAL(j, value);
    }

    /* Vérifiez le nombre d’arguments positionnels. */
    if ((argcount > co->co_argcount) && !(co->co_flags & CO_VARARGS)) {
        too_many_positional(tstate, co, argcount, defcount, fastlocals);
        goto fail;
    }

    /* Ajoute les arguments positionnels manquants
     * (copie les valeurs par défaut depuis `defs`). */
    if (argcount < co->co_argcount) {
        Py_ssize_t m = co->co_argcount - defcount;
        Py_ssize_t missing = 0;
        for (i = argcount; i < m; i++) {
            if (GETLOCAL(i) == NULL) {
                missing++;
            }
        }
        if (missing) {
            missing_arguments(tstate, co, missing, defcount, fastlocals);
            goto fail;
        }
        if (n > m)
            i = n - m;
        else
            i = 0;
        for (; i < defcount; i++) {
            if (GETLOCAL(m+i) == NULL) {
                PyObject *def = defs[i];
                Py_INCREF(def);
                SETLOCAL(m+i, def);
            }
        }
    }

    /* Ajoute les arguments en mot-clés manquants
     * (copie les valeurs par défaut depuis `kwdefs`). */
    if (co->co_kwonlyargcount > 0) {
        Py_ssize_t missing = 0;
        for (i = co->co_argcount; i < total_args; i++) {
            PyObject *name;
            if (GETLOCAL(i) != NULL)
                continue;
            name = PyTuple_GET_ITEM(co->co_varnames, i);
            if (kwdefs != NULL) {
                PyObject *def = PyDict_GetItemWithError(kwdefs, name);
                if (def) {
                    Py_INCREF(def);
                    SETLOCAL(i, def);
                    continue;
                }
                else if (_PyErr_Occurred(tstate)) {
                    goto fail;
                }
            }
            missing++;
        }
        if (missing) {
            missing_arguments(tstate, co, missing, -1, fastlocals);
            goto fail;
        }
    }

    /* Alloue et initialise le stockage des cellules, et copie les
     * variables libres dans la frame. */
    for (i = 0; i < PyTuple_GET_SIZE(co->co_cellvars); ++i) {
        PyObject *c;
        Py_ssize_t arg;
        /* Prends en compte la possibilité que la cellule soit un argument. */
        if (co->co_cell2arg != NULL &&
            (arg = co->co_cell2arg[i]) != CO_CELL_NOT_AN_ARG) {
            c = PyCell_New(GETLOCAL(arg));
            /* Nettoie la copie locale. */
            SETLOCAL(arg, NULL);
        }
        else {
            c = PyCell_New(NULL);
        }
        if (c == NULL)
            goto fail;
        SETLOCAL(co->co_nlocals + i, c);
    }

    /* Copie les variables de closure dans les variables libres. */
    for (i = 0; i < PyTuple_GET_SIZE(co->co_freevars); ++i) {
        PyObject *o = PyTuple_GET_ITEM(closure, i);
        Py_INCREF(o);
        freevars[PyTuple_GET_SIZE(co->co_cellvars) + i] = o;
    }

    /* Gère les générateurs/coroutines/générateurs asynchrones. */
    if (co->co_flags & (CO_GENERATOR | CO_COROUTINE | CO_ASYNC_GENERATOR)) {
        PyObject *gen;
        int is_coro = co->co_flags & CO_COROUTINE;

        /* Inutile de garder la référence à f_back, elle sera définie
         * à la reprise du générateur. */
        Py_CLEAR(f->f_back);

        /* Créé un nouveau générateur contenant la frame prêt à fonctionner
         * et la renvoie en tant que valeur. */
        if (is_coro) {
            gen = PyCoro_New(f, name, qualname);
        } else if (co->co_flags & CO_ASYNC_GENERATOR) {
            gen = PyAsyncGen_New(f, name, qualname);
        } else {
            gen = PyGen_NewWithQualName(f, name, qualname);
        }
        if (gen == NULL) {
            return NULL;
        }

        _PyObject_GC_TRACK(f);

        return gen;
    }

    retval = _PyEval_EvalFrame(tstate, f, 0);

fail: /* Saute ici depuis le prelude en cas d’échec. */

    /* déréférencer la frame peut entraîner l’invocation de la méthode __del__,
       qui peut invoquer à son tour un appel Python. Pendant qu’on termine
       la frame Python courante (`f`), la pile C associées est utilisé, donc
       `recursion_depth` doit être augmenté pendant cette période.
    */
    if (Py_REFCNT(f) > 1) {
        Py_DECREF(f);
        _PyObject_GC_TRACK(f);
    }
    else {
        ++tstate->recursion_depth;
        Py_DECREF(f);
        --tstate->recursion_depth;
    }
    return retval;
}

_PyEval_EvalFrame() est un wrapper autour de interp->eval_frame(), la fonction d’évaluation de la frame. Il est possible de faire pointer interp->eval_frame() vers une fonction personnalisée. Pourquoi voudrait-on faire une telle chose ? Par exemple, cela permet d’ajouter un compilateur JIT à CPython en remplaçant la fonction d’évaluation par défaut par celle qui stocke le code machine compilé dans un code object et l’exécute. PEP 523 a introduit cette fonctionnalité dans CPython 3.6.

Par défaut, interp->eval_frame() pointe sur _PyEval_EvalFrameDefault(). Cette fonction, définie dans Python/ceval.c, comporte près de 3000 lignes de code. Aujourd’hui, nous ne nous intéresserons qu’à l’une d’entre elles. La ligne 1336 de Python/ceval.c démarre ce que nous attendions depuis si longtemps : La boucle d’évaluation.

Conclusion

Nous avons examiné beaucoup de choses aujourd’hui. Nous avons commencé par un aperçu du projet CPython, compilé CPython, parcouru son code source, et étudié la phase d’initialisation en cours de route. Globalement, je pense que cela devrait vous donner une bonne compréhension de ce que fait CPython avant de commencer à interpréter le bytecode. Ce qui se passe après est le sujet du prochain billet.

En attendant, je vous recommande vivement de passer un peu de temps à explorer le code source de CPython par vous-même, pour consolider ce que nous avons appris aujourd’hui et pour en apprendre plus. Je parie que la lecture de cet article a suscité beaucoup d’interrogations, vous devriez donc avoir quelque chose à chercher. Amusez-vous bien !

Le quatrième chapitre est disponible.


  1. NdT : Tous les commentaires ne seront pas traduits, notamment les plus évidant. 

  2. NdT : Le « marshalage » est le procédé qui transforme la représentation en mémoire d’un objet (ici, l’object code) en donnés adaptées au stockage ou à la transmission. Plus d’informations sur la page Wikipédia

Dernière mise à jour : jeu. 17 décembre 2020