programa lenguaje interpretado ejecución compilar compilado compiler-construction interpreter python

compiler-construction - ejecución - lenguaje interpretado



Proceso de compilación/interpretación de Python (2)

Estoy tratando de entender el proceso de compilador / intérprete de Python más claramente. Desafortunadamente, no he tomado una clase de intérpretes ni he leído mucho sobre ellos.

Básicamente, lo que entiendo ahora es que el código de Python de los archivos .py se compila primero en el bytecode de python (que supongo que son los archivos .pyc que veo ocasionalmente). A continuación, el bytecode se compila en código máquina, un lenguaje que el procesador realmente entiende. Más o menos, he leído este hilo ¿Por qué python compila la fuente en bytecode antes de interpretar?

¿Podría alguien darme una buena explicación de todo el proceso teniendo en cuenta que mi conocimiento de los compiladores / intérpretes es casi inexistente? O, si eso no es posible, ¿tal vez me proporcione algunos recursos que brinden descripciones rápidas de compiladores / intérpretes?

Gracias


El bytecode en realidad no se interpreta como código máquina, a menos que esté utilizando alguna implementación exótica como pypy.

Aparte de eso, tienes la descripción correcta. El bytecode se carga en el tiempo de ejecución de Python y se interpreta mediante una máquina virtual, que es una pieza de código que lee cada instrucción en el bytecode y ejecuta cualquier operación que se indique. Puede ver este bytecode con el módulo dis , de la siguiente manera:

>>> def fib(n): return n if n < 2 else fib(n - 2) + fib(n - 1) ... >>> fib(10) 55 >>> import dis >>> dis.dis(fib) 1 0 LOAD_FAST 0 (n) 3 LOAD_CONST 1 (2) 6 COMPARE_OP 0 (<) 9 JUMP_IF_FALSE 5 (to 17) 12 POP_TOP 13 LOAD_FAST 0 (n) 16 RETURN_VALUE >> 17 POP_TOP 18 LOAD_GLOBAL 0 (fib) 21 LOAD_FAST 0 (n) 24 LOAD_CONST 1 (2) 27 BINARY_SUBTRACT 28 CALL_FUNCTION 1 31 LOAD_GLOBAL 0 (fib) 34 LOAD_FAST 0 (n) 37 LOAD_CONST 2 (1) 40 BINARY_SUBTRACT 41 CALL_FUNCTION 1 44 BINARY_ADD 45 RETURN_VALUE >>>

Explicación detallada

Es muy importante entender que el código anterior nunca es ejecutado por su CPU; ni se convierte alguna vez en algo que sea (al menos, no en la implementación oficial C de Python). La CPU ejecuta el código de máquina virtual, que realiza el trabajo indicado por las instrucciones de bytecode. Cuando el intérprete desea ejecutar la función fib , lee las instrucciones una por una y hace lo que le dicen que haga. Mira la primera instrucción, LOAD_FAST 0 , y así toma el parámetro 0 (el n pasado a fib ) desde donde se mantienen los parámetros y lo empuja a la pila del intérprete (el intérprete de Python es una máquina de pila). Al leer la siguiente instrucción, LOAD_CONST 1 , toma el número constante 1 de una colección de constantes propiedad de la función, que resulta ser el número 2 en este caso, y lo envía a la pila. En realidad puedes ver estas constantes:

>>> fib.func_code.co_consts (None, 2, 1)

La siguiente instrucción, COMPARE_OP 0 , le dice al intérprete que COMPARE_OP 0 los dos elementos de la pila más alta y realice una comparación de desigualdad entre ellos, empujando el resultado booleano nuevamente a la pila. La cuarta instrucción determina, basándose en el valor booleano, si avanzar cinco instrucciones o continuar con la siguiente instrucción. Toda esa verborrea explica if n < 2 parte de la expresión condicional en fib . Será un ejercicio muy instructivo para que descubra el significado y el comportamiento del resto del bytecode de fib . El único de el que no estoy seguro es POP_TOP ; Supongo que JUMP_IF_FALSE se define para dejar su argumento booleano en la pila en lugar de mostrarlo, por lo que debe aparecer explícitamente.

Aún más instructivo es inspeccionar el bytecode sin procesar para fib por lo tanto:

>>> code = fib.func_code.co_code >>> code ''|/x00/x00d/x01/x00j/x00/x00o/x05/x00/x01|/x00/x00S/x01t/x00/x00|/x00/x00d/x01/x00/x18/x83/x01/x00t/x00/x00|/x00/x00d/x02/x00/x18/x83/x01/x00/x17S'' >>> import opcode >>> op = code[0] >>> op ''|'' >>> op = ord(op) >>> op 124 >>> opcode.opname[op] ''LOAD_FAST'' >>>

Por lo tanto, puede ver que el primer byte del bytecode es la instrucción LOAD_FAST . El siguiente par de bytes, ''/x00/x00'' (el número 0 en 16 bits) es el argumento para LOAD_FAST , y le dice al intérprete de bytecode que cargue el parámetro 0 en la pila.


Para completar la gran respuesta de Marcelo Cantos , aquí hay un pequeño resumen de columna por columna para explicar la salida del bytecode desensamblado.

Por ejemplo, dada esta función:

def f(num): if num == 42: return True return False

Esto puede ser desmontado en (Python 3.6):

(1)|(2)|(3)|(4)| (5) |(6)| (7) ---|---|---|---|----------------------|---|------- 2| | | 0|LOAD_FAST | 0|(num) |-->| | 2|LOAD_CONST | 1|(42) | | | 4|COMPARE_OP | 2|(==) | | | 6|POP_JUMP_IF_FALSE | 12| | | | | | | 3| | | 8|LOAD_CONST | 2|(True) | | | 10|RETURN_VALUE | | | | | | | | 4| |>> | 12|LOAD_CONST | 3|(False) | | | 14|RETURN_VALUE | |

Cada columna tiene un propósito específico:

  1. El número de línea correspondiente en el código fuente
  2. Opcionalmente indica la instrucción actual ejecutada (cuando el bytecode proviene de un objeto de marco, por ejemplo)
  3. Una etiqueta que denota un posible JUMP de una instrucción anterior a esta
  4. La dirección en el bytecode que corresponde al índice de bytes (esos son múltiplos de 2 porque Python 3.6 usa 2 bytes para cada instrucción, mientras que podría variar en versiones anteriores)
  5. El nombre de la instrucción (también llamado opname ), cada uno se explica brevemente en el módulo dis y su implementación se puede encontrar en ceval.c (el ciclo central de CPython)
  6. El argumento (si existe) de la instrucción que Python usa internamente para obtener algunas constantes o variables, administrar la pila, saltar a una instrucción específica, etc.
  7. La interpretación humana del argumento de la instrucción