Saltar al contenido

Muchos programadores de Python no pueden resolver este rompecabezas

septiembre 24, 2021
1eg9c9zJzNJY xpanjVmbOg

PROGRAMACIÓN

Muchos programadores de Python no pueden resolver este rompecabezas

Una breve introducción a «Python bajo el capó» para principiantes

Naser Tamimi

23 de diciembre de 2020·5 min de lectura

1*eg9c9zJzNJY

Foto de Priscilla Du Preez en Unsplash

Déjame ir directo al rompecabezas. ¡¡¿Estás listo?!! Bien, eche un vistazo a estos dos códigos súper simples.

Código 1) El código simple.
Código 2) Definió una función main () para ejecutar el mismo código simple.

Ambos códigos hacen una tarea ficticia. Toman números entre 0 y 10 millones (a través de un bucle for) y calculan su mod (resto) de 5. Hasta ahora, muy fácil. Ahora, ¿está interesado en medir su tiempo de ejecución?

Código 1) Se agregó un temporizador simple al Código 1.
Código 2) Se agregó un temporizador simple al Código 2.

Solo agregamos un temporizador simple a ambos códigos para medir sus tiempos de ejecución. Dado que ambos códigos realizan la misma tarea simple, esperamos ver los mismos tiempos de ejecución.

Por supuesto yoF los tiempos de ejecución fueron los mismos, no escribí este artículo. En realidad, los tiempos de ejecución son 739 ms y 434 ms para el Código 1 y el Código 2, respectivamente. ¡¡¡¡SORPRESA!!!!

Foto de Rhys Kentish en Unsplash

Este rompecabezas es conocido por algunos programadores de Python. Sin embargo, la mayoría de los programadores de Python no conocen este acertijo porque necesita una comprensión más profunda de cómo funciona Python. Este artículo le ofrece una introducción a «¿Qué sucede cuando ejecuta un código Python? «

NOTA: Este artículo está escrito en un lenguaje simple para principiantes de Python. Si es un profesional y necesita algunos recursos, vaya a la sección «recursos adicionales» al final de este artículo.

¿Qué sucede cuando ejecuta un código fuente de Python?

Para que sea más fácil, permítanme centrarme en la implementación de Python más popular llamada CPython. Si no sabe qué implementación de Python está utilizando, existe un 90% de posibilidades de que esté utilizando CPython.

Esto es lo que le sucede a su código fuente cuando lo ejecuta.

1*XPT73Xf B0iMBh6hZQVC0Q

¿Qué sucede cuando ejecuta un código fuente de Python?

Primero, su código fuente se divide en tokens a través de un proceso llamado «Lexing». Por ejemplo, x=1 se dividiría en fichas como x, =, y1. Luego, los tokens se organizan en un árbol de sintaxis abstracta (AST) a través de un proceso llamado «Análisis». Después de eso, un «compilador» convierte todo en un código abstracto llamado «Bytecode».

Es importante comprender que en Python, a diferencia de otros lenguajes (por ejemplo, C, C ++, Java, etc.), el compilador no toma el «código fuente» y lo convierte en el «código de máquina». En cambio, el compilador toma el «código fuente» y lo convierte en un «Bytecode». Es tarea del intérprete tomar el código de bytes y ejecutarlo de la manera que la máquina lo entienda.

Para aquellos de ustedes que tienen curiosidad por ver Bytecodes, a veces se almacenan (en caché) como archivos .pyc en su disco. Créame, no puede entenderlos. ¡¡¡Solo para satisfacer tu curiosidad !!!

Entre los cuatro pasos para ejecutar un código en Python, el intérprete tiene el trabajo más pesado que hacer y los otros tres pasos no manejan demasiada carga. Por lo tanto, cada vez que desee investigar el rendimiento de un programa Python, debe mirar el paso de interpretación y buscar algunas pistas.

El intérprete lee el código de bytes y ejecuta sus instrucciones. Si el código de bytes es como una receta, las instrucciones son como diferentes pasos en esa receta.

Si pudiéramos leer el código de bytes, podríamos tener algunas pistas sobre nuestro rompecabezas. Para echar un vistazo a las instrucciones del código de bytes, podemos usar el dis paquete. dis es un paquete de Python para desensamblar y decodificar códigos de bytes y mostrarlos de una manera que los humanos puedan entender. La salida de ladis.dis() tiene una estructura como esta:

Anatomía de una instrucción en Python mostrada con dis Biblioteca.

No repaso los detalles del dis paquete y céntrese en la única columna de Nombre de la operación. El nombre de la operación indica al intérprete de Python qué se debe hacer. Si tiene demasiada curiosidad, de hecho, un archivo llamado ceval.c tiene todas las instrucciones (ver aquí).

Yo corrí dis.dis() para ambos códigos, y para que te sea más sencillo, destaco la parte importante, que es la parte del bucle. La siguiente figura muestra los códigos de bytes para ambos códigos.

1*kFNXJUrSQ9S UwbZQJ LDA

Salidas de dis.dis () para Code 1 y Code 2

Como puede ver, ambos códigos son muy similares en términos de instrucciones dadas. Pero, si observa más de cerca, encontrará algunas diferencias sutiles (pero importantes) en los códigos de bytes. En el Código 1, vemos STORE_NAME y LOAD_NAME, pero en el Código 2, vemos STORE_FAST y LOAD_FAST. Parece que la diferencia de tiempo de ejecución se debe a diferencias en esos dos tipos de instrucciones. Puedes echar un vistazo a ceval.c archivo para ver las diferencias.

Permíteme que te lo haga simple y claro. El intérprete procesa variables i y x diferente en el Código 1 en comparación con el Código 2 (nota para _NAME y _FAST sufijos). Ambos i y x son variables globales en el Código 1, y CPython almacena esas variables en una estructura de datos de diccionario, lo que hace que el proceso de carga sea más lento en comparación con las variables locales, que se almacenan en una matriz de tamaño fijo. Recuperar una variable de una matriz de tamaño fijo es mucho más rápido en comparación con un diccionario. ¿Por qué Python hace eso? Simplemente porque en el código principal, no sabemos cuántas variables aparecerán, pero el número de variables está fijo en una función.

Si esta es la razón, hagamos una prueba. Juguemos con el intérprete y definamos ambos x y i variables como variables globales en el Código 2 (el código rápido) y mida el tiempo de ejecución nuevamente. Aquí está el código 2 después de cambiar.

Código 3) Lo mismo que el Código 2. Acabo de definir las variables i y x para ver si las variables globales son responsables del rendimiento lento de nuestro código de rompecabezas.

Ejecuté el Código 3 y me tomó 805 ms (en comparación con el Código 2, que tomó 434 ms). El número está muy cerca del Código 1 (es decir, 739 ms). Muestra que, como esperábamos, el procesamiento de variables globales lleva más tiempo en comparación con las variables locales (matriz de tamaño fijo frente a diccionario).

Resumen

Como puede ver, podríamos resolver el acertijo con un poco de conocimiento sobre cómo funciona el intérprete de Python y obteniendo ayuda de dis Biblioteca. Algunos otros factores, por ejemplo, «predicción de código de operación » también contribuyen a la diferencia de tiempo de ejecución, pero en aras de la simplicidad, lo ignoramos en este artículo.

Si está interesado en saber más sobre la base de Python. Recomiendo encarecidamente esta serie de blogs: https://tech.blog.aknin.name/category/my-projects/pythons-innards/

Sígueme en Twitter para conocer las últimas historias: https://twitter.com/TamimiNas

close