Deconstrucción del intérprete: comprensión detrás del código de bytes de Python

Cuando el intérprete de CPython ejecuta su programa, primero se traduce en una secuencia de instrucciones de código de bytes. Bytecode es un lenguaje intermedio para la máquina virtual de Python que se utiliza como optimización del rendimiento.

En lugar de ejecutar directamente el código fuente legible por humanos, se utilizan códigos numéricos compactos, constantes y referencias que representan el resultado del análisis sintáctico y semántico del compilador. Esto ahorra tiempo y memoria para ejecuciones repetidas de programas o parte de programas. Por ejemplo, el código de bytes resultante de este paso de compilación se almacena en caché en el disco en archivos .pyc y .pyo para que la ejecución del mismo archivo de Python sea más rápida la segunda vez. Todo esto es completamente transparente para el programador. No es necesario que tenga en cuenta que se produce este paso intermedio de traducción, o cómo la máquina virtual de Python trata con el código de bytes. De hecho, el formato del código de bytes se considera un detalle de implementación y no se garantiza que permanezca estable o compatible entre las versiones de Python. Y todavía, uno puede encontrar muy esclarecedor ver cómo se hace la salchicha y echar un vistazo detrás de las abstracciones proporcionadas por el intérprete CPython. Comprender al menos parte del funcionamiento interno puede ayudarlo a escribir un código de mayor rendimiento.

Ejemplo: tomemos esta función showMeByte() simple como una muestra de laboratorio y comprenda el código de bytes de Python:

def showMeByte(name):
    return "hello "+name+" !!!"
  
  
print(showMeByte("amit kumra"))

Producción:

hello amit kumra !!!

CPython primero traduce nuestro código fuente a un lenguaje intermedio antes de ejecutarlo. Podemos ver los resultados de este paso de compilación. Cada función tiene un __code__atributo (en Python 3) que podemos usar para obtener las instrucciones, constantes y variables de la máquina virtual que usa nuestra función showMeByte:

Ejemplo:

def showMeByte(name):
    return "hello "+name+" !!!"
  
  
print(showMeByte.__code__.co_code)
  
print(showMeByte.__code__.co_consts)
  
print(showMeByte.__code__.co_stacksize)
  
print(showMeByte.__code__.co_varnames)
  
print(showMeByte.__code__.co_flags)
  
print(showMeByte.__code__.co_name)
  
print(showMeByte.__code__.co_names)

Producción:

b'd\x01|\x00\x17\x00d\x02\x17\x00S\x00'
(None, 'hello ', ' !!!')
2
('name',)
67
showMeByte
()

Puede ver que co_consts contiene partes de la string de saludo que ensambla nuestra función. Las constantes y el código se mantienen separados para ahorrar espacio en la memoria. Entonces, en lugar de repetir los valores constantes reales en el flujo de instrucciones co_code, Python almacena las constantes por separado en una tabla de búsqueda. El flujo de instrucciones puede hacer referencia a una constante con un índice en la tabla de búsqueda. Lo mismo ocurre con las variables almacenadas en el campo co_varnames. Los desarrolladores de CPython nos dieron otra herramienta llamada desensamblador para facilitar la inspección del código de bytes. El desensamblador de código de bytes de Python vive en el módulo dis que es parte de la biblioteca estándar. Así que podemos importarlo y llamar dis.dis()a nuestra función de saludo para obtener una representación de su código de bytes un poco más fácil de leer:

Ejemplo:

import dis
  
  
def showMeByte(name):
    return "hello "+name+" !!!"
  
dis.dis(showMeByte)
bytecode = dis.code_info(showMeByte)
print(bytecode)
  
bytecode = dis.Bytecode(showMeByte)
print(bytecode)
  
for i in bytecode:
    print(i)

Producción:

python-bytecode

Las instrucciones ejecutables o instrucciones simples le dicen al procesador qué hacer. Cada instrucción consta de un código de operación (opcode). Cada instrucción ejecutable genera una instrucción en lenguaje máquina. Lo principal que hizo el desmontaje fue dividir el flujo de instrucciones y darle a cada código de operación un nombre legible por humanos como LOAD_CONST. También puede ver cómo las referencias constantes y variables ahora se intercalan con el código de bytes y se imprimen en su totalidad para ahorrarnos la gimnasia mental de una búsqueda en la tabla co_const y co_varnames.

Primero recupera la constante en el índice 1(‘Hola’) y la coloca en la pila. Luego carga el contenido de la variable de nombre y también los coloca en la pila. La pila es la estructura de datos utilizada como almacenamiento de trabajo interno para la máquina virtual. Hay diferentes clases de máquinas virtuales y una de ellas se llama máquina de pila. La máquina virtual de CPython es una implementación de dicha máquina de pila. La máquina virtual de CPython es una implementación de dicha máquina de pila. Supongamos que la pila comienza vacía. Después de ejecutar los dos primeros códigos de operación, así es como se ve el contenido de la VM (0 es el elemento superior):

0: ’amit kumra’(contents of “name”)
1: ‘hello ‘

La instrucción BINARY_ADD extrae los dos valores de string de la pila, los concatena y luego vuelve a colocar el resultado en la pila:

0: ‘hello amit kumra’

Luego hay otro LOAD_CONST para obtener la string de signos de exclamación en la pila:

0 : ‘ !!!’
1:’Hello amit kumra’

El siguiente código de operación BINARY_ADD nuevamente combina los dos para generar la string de saludo final:

0: ‘hello amit kumra !!!’

La última instrucción de código de bytes es RETURN_VALUE, que le dice a la máquina virtual que lo que está actualmente en la parte superior de la pila es el valor de retorno de esta función para que pueda pasarse a la persona que llama. Entonces, finalmente, rastreamos cómo nuestra showMeCode()función es ejecutada internamente por la máquina virtual CPython.

Publicación traducida automáticamente

Artículo escrito por amitkkumra y traducido por Barcelona Geeks. The original can be accessed here. Licence: CCBY-SA

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *