Dentro de la máquina virtual Python. Parte 1



Hola a todos. Finalmente decidí averiguar cómo funciona el intérprete de Python. Para hacer esto, comenzó a estudiar un libro de artículos y concibió al mismo tiempo para traducirlo al ruso. El hecho es que las traducciones no le permiten perder una oración incomprensible y la calidad de la asimilación del material aumenta).Pido disculpas de antemano por posibles inexactitudes. Siempre trato de traducir lo más correctamente posible, pero uno de los principales problemas: simplemente no se mencionan algunos términos en el equivalente ruso.

Nota de traducción
Python , «code object», ( ) . , .

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

Introducción


El lenguaje de programación Python ha existido por bastante tiempo. El desarrollo de la primera versión fue iniciado por Guido Van Rossum en 1989, y desde entonces el idioma ha crecido y se ha convertido en uno de los más populares. Python se usa en varias aplicaciones: desde interfaces gráficas hasta aplicaciones de análisis de datos.

El propósito de este artículo es ir detrás de escena del intérprete y proporcionar una visión general conceptual de cómo se ejecuta un programa escrito en Python. CPython se considerará en el artículo, porque al momento de escribir, es la implementación Python más popular y básica.

Python y CPython se usan como sinónimos en este texto, pero cualquier mención de Python significa CPython (la versión de Python implementada en C). Otras implementaciones incluyen PyPy (python implementado en un subconjunto limitado de Python), Jython (implementación en la máquina virtual Java), etc.

Me gusta dividir la ejecución de un programa Python en dos o tres pasos principales (enumerados a continuación), dependiendo de cómo se llame al intérprete. Estos pasos se cubrirán en diversos grados en este artículo:

  1. Inicialización : este paso implica la configuración de las diversas estructuras de datos requeridas por el proceso de Python. Lo más probable es que esto suceda cuando el programa se ejecute en modo no interactivo a través del intérprete de comandos.
  2. — , : , , .
  3. — .

El mecanismo para generar árboles de "análisis", así como árboles de sintaxis abstracta (ASD), es independiente del lenguaje. Por lo tanto, no cubriremos mucho este tema, porque los métodos utilizados en Python son similares a los métodos de otros lenguajes de programación. Por otro lado, el proceso de construcción de tablas de símbolos y objetos de código a partir de ADS en Python es más específico, por lo tanto, merece especial atención. También analiza la interpretación de los objetos de código compilados y todas las demás estructuras de datos. Los temas cubiertos por nosotros incluirán, entre otros: el proceso de construir tablas de símbolos y crear objetos de código, objetos de Python, objetos de marco, objetos de código, objetos funcionales, códigos de operación, bucles de intérprete, generadores y clases de usuario.

Este material está destinado a cualquier persona interesada en aprender cómo funciona la máquina virtual CPython. Se supone que el usuario ya está familiarizado con Python y comprende los conceptos básicos del lenguaje. Al estudiar la estructura de una máquina virtual, encontraremos una cantidad significativa de código C, por lo que un usuario que tenga una comprensión básica del lenguaje C le resultará más fácil entender el material. Entonces, básicamente, lo que necesita para familiarizarse con este material: el deseo de aprender más sobre la máquina virtual CPython.

Este artículo es una versión extendida de notas personales realizadas en el estudio del trabajo interno del intérprete. Hay muchas cosas de calidad en los videos de PyCon , las conferencias escolares y este blog.. Mi trabajo no se habría completado sin estas fantásticas fuentes de conocimiento.

Al final de este libro, el lector podrá comprender las complejidades de cómo el intérprete de Python ejecuta su programa. Esto incluye las diversas etapas de ejecución del programa y las estructuras de datos que son críticas en el programa. Para comenzar, tomaremos una vista panorámica de lo que sucede cuando se ejecuta un programa trivial, cuando el nombre del módulo se pasa al intérprete en la línea de comando. El código ejecutable de CPython se puede instalar desde la fuente, siguiendo la Guía del desarrollador de Python .

Este libro usa la versión Python 3.

Vista de 30,000 pies


Este capítulo habla sobre cómo el intérprete ejecuta un programa Python. En los siguientes capítulos, examinaremos las diversas partes de este "rompecabezas" y proporcionaremos una descripción más detallada de cada parte. Independientemente de la complejidad de un programa escrito en Python, este proceso es siempre el mismo. La excelente explicación dada por Yaniv Aknin en su serie de artículos sobre Python Internal establece el tema para nuestra discusión.

El módulo fuente test.py se puede ejecutar desde la línea de comandos (cuando se pasa como argumento al programa de intérprete de Python en forma de $ python test.py). Esta es solo una forma de invocar el ejecutable de Python. También podemos lanzar un intérprete interactivo, ejecutar líneas de un archivo como código, etc. Pero este y otros métodos no nos interesan. Es la transferencia del módulo como argumento (dentro de la línea de comando) al archivo ejecutable (Figura 2.1) que mejor refleja el flujo de varias acciones que están involucradas en la ejecución real del código.


Figura 2.1: Transmisión en tiempo de ejecución.

El ejecutable de Python es un programa normal de C, por lo que cuando se lo llama, se producen procesos similares a los que existen, por ejemplo, en el kernel de Linux o en el programa simple hello world. Tómese un minuto de su tiempo para comprender: el ejecutable de Python es solo otro programa que lanza el suyo. Tales "relaciones" existen entre el lenguaje C y el ensamblador (o llvm). El proceso de inicialización estándar (que depende de la plataforma donde se lleva a cabo la ejecución) comienza cuando se llama al ejecutable de Python con el nombre del módulo como argumento.

Este artículo asume que está utilizando un sistema operativo basado en Unix, por lo que algunas características pueden variar en Windows.

El lenguaje C en el inicio ejecuta toda su "magia" de inicialización: carga bibliotecas, verifica / establece variables de entorno, y después de eso, el método principal del ejecutable de Python se inicia como cualquier otro programa en C. El archivo ejecutable principal de Python se encuentra en ./Programs/python.c y realiza una inicialización (como hacer copias de los argumentos de la línea de comando del programa que se pasaron al módulo). La función principal llama a la función Py_Main ubicada en ./Modules/main.c . Procesa el proceso de inicialización del intérprete: analiza los argumentos de la línea de comandos, establece indicadores, lee variables de entorno, ejecuta enlaces, aleatoriza funciones hash, etc. También llamadoPy_Initialize de pylifecycle.c , que maneja la inicialización de las estructuras de datos del estado del intérprete y el flujo, son dos estructuras de datos muy importantes.

Al examinar las declaraciones de las estructuras de datos del intérprete y los estados de flujo, queda claro por qué son necesarias. El estado del intérprete y la secuencia son solo estructuras con punteros a campos que contienen la información necesaria para ejecutar el programa. Los datos de estado del intérprete se crean a través de typedef (solo piense en esta palabra clave en C como una definición de tipo, aunque esto no es del todo cierto). El código para esta estructura se muestra en el Listado 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;

Listado de Código 2.1: Estructura de datos del estado del intérprete

Cualquiera que haya usado el lenguaje de programación Python durante mucho tiempo puede reconocer varios campos mencionados en esta estructura (sysdict, builtins, codec).

  1. El * campo siguiente es una referencia a otra instancia del intérprete, ya que pueden existir varios intérpretes de Python dentro del mismo proceso.
  2. El campo * tstate_head indica el hilo principal de ejecución (si el programa es multiproceso, entonces el intérprete es común para todos los hilos creados por el programa). Discutiremos esto con más detalle en breve.
  3. modules, modules_by_index, sysdict, builtins e importlib hablan por sí mismos. Todos ellos se definen como instancias de PyObject , que es el tipo raíz para todos los objetos en la máquina virtual Python. Los objetos de Python se discutirán con más detalle en los siguientes capítulos.
  4. Los campos relacionados con el códec * contienen información que ayuda a descargar codificaciones. Esto es muy importante para decodificar bytes.

La ejecución del programa debe ocurrir en un hilo. La estructura de estado de la secuencia contiene toda la información que la secuencia necesita para ejecutar algún objeto de código. Parte de la estructura de datos de flujo se muestra en el Listado 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;

Listado 2.2: Parte de la

estructura de datos del estado del flujo Las estructuras de datos del intérprete y los estados del flujo se discuten con más detalle en los siguientes capítulos. El proceso de inicialización también establece mecanismos de importación, así como un nivel básico.

Después de completar toda la inicialización, Py_Main llama a la función run_file (también ubicada en el módulo main.c). La siguiente es una serie de llamadas a funciones: PyRun_AnyFileExFlags -> PyRun_SimpleFileExFlags -> PyRun_FileExFlags -> PyParser_ASTFromFileObject. PyRun_SimpleFileExFlagscrea el espacio de nombres __principal__ en el que se ejecutará el contenido del archivo. También comprueba si existe la versión pyc del archivo (el archivo pyc es un archivo simple que contiene una versión ya compilada del código fuente). Si existe una versión de pyc, se intentará leerlo como un archivo binario y luego ejecutarlo. Si falta el archivo pyc, se llama a PyRun_FileExFlags, etc. La función PyParser_ASTFromFileObject llama a PyParser_ParseFileObject , que lee el contenido del módulo y crea árboles de análisis a partir de él. Luego, el árbol creado se pasa a PyParser_ASTFromNodeObject , que crea un árbol de sintaxis abstracta a partir de él.

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

AST se genera cuando se llama run_mod . Esta función llama a PyAST_CompileObject , que crea objetos de código a partir de AST. Tenga en cuenta que el código de bytes generado durante la llamada PyAST_CompileObject se pasa a través del optimizador de mirilla simple , que realiza una optimización baja del código de bytes generado antes de crear objetos de código. La función run_mod continuación, se aplica la PyEval_EvalCode función de la ceval.c archivo al objeto código. Esto lleva a otra serie de llamadas a funciones: PyEval_EvalCode -> PyEval_EvalCode -> _PyEval_EvalCodeWithName -> _PyEval_EvalFrameEx. El objeto de código se pasa como argumento a la mayoría de estas funciones de una forma u otra. _PyEval_EvalFrameEx- Este es un bucle de intérprete normal que maneja la ejecución de objetos de código. Sin embargo, se llama no solo con el objeto de código como argumento, sino con el objeto de marco, que tiene como atributo un campo que se refiere al objeto de código. Este marco proporciona el contexto para la ejecución del objeto de código. En palabras simples: el bucle de intérprete lee continuamente la siguiente instrucción indicada por el contador de instrucciones del conjunto de instrucciones. Luego ejecuta esta instrucción: agrega o elimina objetos de la pila de valores en el proceso hasta que se vacía en la matriz de instrucciones que se ejecutarán (bueno, o sucede algo excepcional que interrumpe el ciclo).

Python proporciona un conjunto de funciones que puede usar para examinar objetos de código real. Por ejemplo, un programa simple puede compilarse en un objeto de código y desmontarse para obtener códigos de operación que son ejecutados por la máquina virtual de Python. Esto se muestra en el Listado 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        

Listado de Código 2.3: Desmontaje de una función en Python El

archivo de encabezado ./Include/opcodes.h contiene una lista completa de todas las instrucciones / códigos de operación para la máquina virtual Python. Los códigos de operación son bastante simples. Tome nuestro ejemplo en el Listado 2.3, que tiene un conjunto de cuatro instrucciones. LOAD_FAST carga el valor de su argumento (en este caso x) en la pila de valores. La máquina virtual de Python se basa en la pila, por lo que los valores para las operaciones de código de operación se "extraen" de la pila, y los resultados del cálculo se vuelven a colocar en la pila para que otros códigos de operación los utilicen. Luego, BINARY_MULTIPLY extrae dos elementos de la pila, realiza la multiplicación binaria de ambos valores y empuja el resultado nuevamente a la pila. Instrucción de valor de retornorecupera un valor de la pila, establece el valor de retorno para el objeto en este valor y sale del bucle de intérprete. Si observa el Listado 2.3, está claro que esta es una simplificación bastante sólida.

La explicación actual del bucle de intérprete no tiene en cuenta una serie de detalles, que se discutirán en capítulos posteriores. Por ejemplo, estas son las preguntas a las que no recibimos respuesta:

  • ¿De dónde provienen los valores cargados por la declaración LOAD_FAST?
  • ¿De dónde vienen los argumentos, que se usan como parte de las instrucciones?
  • ¿Cómo se manejan las llamadas a funciones y métodos anidados?
  • ¿Cómo maneja el bucle de intérprete las excepciones?

Después de completar todas las instrucciones, la función Py_Main continúa la ejecución, pero esta vez comienza el proceso de limpieza. Si se llama a Py_Initialize para realizar la inicialización durante el inicio del intérprete, se llama a Py_FinalizeEx para realizar la limpieza. Este proceso incluye esperar la salida de los subprocesos, llamar a los controladores de salida, así como liberar la memoria aún utilizada asignada por el intérprete.

Y así, miramos la descripción de "alto nivel" de los procesos que ocurren en el ejecutable de Python cuando se ejecuta un script. Como se señaló anteriormente, quedan muchas preguntas por responder. En el futuro, profundizaremos en el estudio del intérprete y consideraremos en detalle cada una de las etapas. Y comenzaremos describiendo el proceso de compilación en el próximo capítulo.

All Articles