¿Por qué hay tantas pitones? Una comparación de implementación de Python

Publicado: 2022-03-11

Python es increíble.

Sorprendentemente, esa es una declaración bastante ambigua. ¿Qué quiero decir con 'Python'? ¿Me refiero a Python, la interfaz abstracta? ¿Me refiero a CPython, la implementación común de Python (y que no debe confundirse con el nombre similar de Cython)? ¿O me refiero a algo completamente diferente? Tal vez me estoy refiriendo oblicuamente a Jython, IronPython o PyPy. O tal vez me he vuelto loco y estoy hablando de RPython o RubyPython (que son cosas muy, muy diferentes).

Si bien las tecnologías mencionadas anteriormente tienen nombres y referencias comunes, algunas de ellas tienen propósitos completamente diferentes (o, al menos, funcionan de maneras completamente diferentes).

A lo largo de mi tiempo trabajando con las interfaces de Python, me encontré con toneladas de estas herramientas .*ython. Pero no fue hasta hace poco que me tomé el tiempo de comprender qué son, cómo funcionan y por qué son necesarios (a su manera).

En este tutorial, comenzaré desde cero y avanzaré a través de las diversas implementaciones de Python, y concluiré con una introducción completa a PyPy, que creo que es el futuro del lenguaje.

Todo comienza con una comprensión de lo que realmente es 'Python'.

Si tiene una buena comprensión del código de la máquina, las máquinas virtuales y similares, no dude en pasar a continuación.

"¿Python es interpretado o compilado?"

Este es un punto común de confusión para los principiantes de Python.

Lo primero que hay que tener en cuenta al hacer una comparación es que 'Python' es una interfaz . Hay una especificación de lo que debe hacer Python y cómo debe comportarse (como con cualquier interfaz). Y hay múltiples implementaciones (como con cualquier interfaz).

La segunda cosa a tener en cuenta es que 'interpretado' y 'compilado' son propiedades de una implementación , no de una interfaz .

Entonces, la pregunta en sí no está realmente bien formulada.

¿Python es interpretado o compilado? La pregunta no está realmente bien formulada.

Dicho esto, para la implementación de Python más común (CPython: escrito en C, a menudo denominado simplemente 'Python', y seguramente lo que está usando si no tiene idea de lo que estoy hablando), la respuesta es: interpretado , con alguna compilación. CPython compila * el código fuente de Python en bytecode, y luego interpreta este bytecode, ejecutándolo a medida que avanza.

* Nota: esto no es 'compilación' en el sentido tradicional de la palabra. Por lo general, diríamos que la "compilación" toma un lenguaje de alto nivel y lo convierte en código de máquina. Pero es una especie de 'compilación'.

Veamos esa respuesta más de cerca, ya que nos ayudará a comprender algunos de los conceptos que surgen más adelante en la publicación.

Código de bytes frente a código de máquina

Es muy importante comprender la diferencia entre el código de bytes y el código de máquina (también conocido como código nativo), quizás mejor ilustrado con un ejemplo:

  • C compila en código de máquina, que luego se ejecuta directamente en su procesador. Cada instrucción le indica a su CPU que mueva cosas.
  • Java se compila en código de bytes, que luego se ejecuta en la máquina virtual de Java (JVM), una abstracción de una computadora que ejecuta programas. Luego, cada instrucción es manejada por la JVM, que interactúa con su computadora.

En términos muy breves: el código de máquina es mucho más rápido, pero el código de bytes es más portátil y seguro .

El código de la máquina se ve diferente según su máquina, pero el código de bytes se ve igual en todas las máquinas. Se podría decir que el código de máquina está optimizado para su configuración.

Volviendo a la implementación de CPython, el proceso de la cadena de herramientas es el siguiente:

  1. CPython compila su código fuente de Python en bytecode.
  2. Ese código de bytes luego se ejecuta en la máquina virtual CPython.
Los principiantes a menudo asumen que Python está compilado debido a los archivos .pyc. Hay algo de verdad en eso: el archivo .pyc es el código de bytes compilado, que luego se interpreta. Entonces, si ha ejecutado su código Python antes y tiene el archivo .pyc a mano, se ejecutará más rápido la segunda vez, ya que no tiene que volver a compilar el código de bytes.

Máquinas virtuales alternativas: Jython, IronPython y más

Como mencioné anteriormente, Python tiene varias implementaciones. Nuevamente, como se mencionó anteriormente, el más común es CPython, pero hay otros que deben mencionarse por el bien de esta guía de comparación. Esta es una implementación de Python escrita en C y considerada la implementación 'predeterminada'.

Pero, ¿qué pasa con las implementaciones alternativas de Python? Uno de los más destacados es Jython, una implementación de Python escrita en Java que utiliza JVM. Mientras que CPython produce código de bytes para ejecutar en la VM de CPython, Jython produce código de bytes de Java para ejecutar en la JVM (esto es lo mismo que se produce cuando compila un programa Java).

El uso de código de bytes de Java por parte de Jython se muestra en este diagrama de implementación de Python.

“¿Por qué usaría alguna vez una implementación alternativa?”, podría preguntarse. Bueno, por un lado, estas diferentes implementaciones de Python funcionan bien con diferentes pilas de tecnología .

CPython hace que sea muy fácil escribir extensiones C para su código Python porque al final es ejecutado por un intérprete de C. Jython, por otro lado, hace que sea muy fácil trabajar con otros programas de Java: puede importar cualquier clase de Java sin esfuerzo adicional, invocando y utilizando sus clases de Java desde dentro de sus programas Jython. (Aparte: si no lo ha pensado detenidamente, esto es realmente una locura. Estamos en el punto en el que puede mezclar y mezclar diferentes lenguajes y compilarlos todos en la misma sustancia. (Como lo mencionó Rostin, los programas que mix Fortran y el código C han existido por un tiempo. Entonces, por supuesto, esto no es necesariamente nuevo. Pero aún así es genial.))

Como ejemplo, este es un código Jython válido:

 [Java HotSpot(TM) 64-Bit Server VM (Apple Inc.)] on java1.6.0_51 >>> from java.util import HashSet >>> s = HashSet(5) >>> s.add("Foo") >>> s.add("Bar") >>> s [Foo, Bar]

IronPython es otra implementación popular de Python, escrita completamente en C# y dirigida a la pila .NET. En particular, se ejecuta en lo que podría llamarse .NET Virtual Machine, Common Language Runtime (CLR) de Microsoft, comparable a JVM.

Se podría decir que Jython : Java :: IronPython : C# . Se ejecutan en las mismas máquinas virtuales respectivas, puede importar clases C# desde su código IronPython y clases Java desde su código Jython, etc.

Es totalmente posible sobrevivir sin siquiera tocar una implementación de Python que no sea CPython. Pero hay ventajas al cambiar, la mayoría de las cuales dependen de su pila de tecnología. ¿Usas muchos lenguajes basados ​​en JVM? Jython podría ser para ti. ¿Todo sobre la pila .NET? Tal vez deberías probar IronPython (y tal vez ya lo hayas hecho).

Este cuadro de comparación de Python demuestra las diferencias entre las implementaciones de Python.

Por cierto: si bien esta no sería una razón para usar una implementación diferente, tenga en cuenta que estas implementaciones en realidad difieren en el comportamiento más allá de cómo tratan su código fuente de Python. Sin embargo, estas diferencias suelen ser menores y se disuelven o emergen con el tiempo a medida que estas implementaciones se encuentran en desarrollo activo. Por ejemplo, IronPython usa cadenas Unicode de forma predeterminada; CPython, sin embargo, tiene como valor predeterminado ASCII para las versiones 2.x (falla con un UnicodeEncodeError para caracteres que no son ASCII), pero admite cadenas Unicode de forma predeterminada para 3.x.

Compilación justo a tiempo: PyPy y el futuro

Así que tenemos una implementación de Python escrita en C, una en Java y otra en C#. El siguiente paso lógico: una implementación de Python escrita en… Python. (El lector educado notará que esto es un poco engañoso).

Aquí es donde las cosas pueden volverse confusas. Primero, analicemos la compilación justo a tiempo (JIT).

JIT: el por qué y el cómo

Recuerde que el código de máquina nativo es mucho más rápido que el código de bytes. Bueno, ¿y si pudiéramos compilar parte de nuestro código de bytes y luego ejecutarlo como código nativo? Tendríamos que pagar un precio para compilar el código de bytes (es decir, el tiempo), pero si el resultado final fuera más rápido, ¡sería fantástico! Esta es la motivación de la compilación JIT, una técnica híbrida que combina los beneficios de los intérpretes y compiladores. En términos básicos, JIT quiere utilizar la compilación para acelerar un sistema interpretado.

Por ejemplo, un enfoque común adoptado por los JIT:

  1. Identifique el código de bytes que se ejecuta con frecuencia.
  2. Compílelo en código de máquina nativo.
  3. Guarda en caché el resultado.
  4. Siempre que se configure el mismo código de bytes para ejecutarse, en su lugar, tome el código de máquina precompilado y obtenga los beneficios (es decir, aumentos de velocidad).

De esto se trata la implementación de PyPy: llevar JIT a Python (consulte el Apéndice para conocer los esfuerzos anteriores). Hay, por supuesto, otros objetivos: PyPy tiene como objetivo ser multiplataforma, memoria ligera y soporte sin pilas. Pero JIT es realmente su punto de venta. Como promedio de varias pruebas de tiempo, se dice que mejora el rendimiento en un factor de 6,27. Para ver un desglose, consulte este gráfico del PyPy Speed ​​Center:

Llevar JIT a la interfaz de Python mediante la implementación de PyPy se traduce en mejoras de rendimiento.

PyPy es difícil de entender

PyPy tiene un gran potencial y, en este punto, es altamente compatible con CPython (por lo que puede ejecutar Flask, Django, etc.).

Pero hay mucha confusión en torno a PyPy (ver, por ejemplo, esta propuesta sin sentido para crear un PyPyPy...). En mi opinión, eso se debe principalmente a que PyPy es en realidad dos cosas:

  1. Un intérprete de Python escrito en RPython (no Python (mentí antes)). RPython es un subconjunto de Python con escritura estática. En Python, es "prácticamente imposible" razonar rigurosamente sobre los tipos (¿Por qué es tan difícil? Considere el hecho de que:

     x = random.choice([1, "foo"])

    sería un código Python válido (crédito a Ademan). ¿Cuál es el tipo de x ? ¿Cómo podemos razonar sobre los tipos de variables cuando los tipos ni siquiera se aplican estrictamente?). Con RPython, sacrifica algo de flexibilidad, pero en cambio hace que sea mucho, mucho más fácil razonar sobre la administración de memoria y demás, lo que permite optimizaciones.

  2. Un compilador que compila código RPython para varios objetivos y agrega JIT. La plataforma predeterminada es C, es decir, un compilador RPython-to-C, pero también puede apuntar a JVM y otros.

Solo para mayor claridad en esta guía de comparación de Python, me referiré a estos como PyPy (1) y PyPy (2).

¿Por qué necesitaría estas dos cosas y por qué bajo el mismo techo? Piénselo de esta manera: PyPy (1) es un intérprete escrito en RPython. Por lo tanto, toma el código Python del usuario y lo compila en un código de bytes. Pero el intérprete en sí (escrito en RPython) debe ser interpretado por otra implementación de Python para ejecutarse, ¿no?

Bueno, podríamos usar CPython para ejecutar el intérprete. Pero eso no sería muy rápido.

En cambio, la idea es que usemos PyPy (2) (conocido como la cadena de herramientas RPython) para compilar el intérprete de PyPy en código para otra plataforma (por ejemplo, C, JVM o CLI) para ejecutar en nuestra máquina, agregando JIT como bien. Es mágico: PyPy agrega dinámicamente JIT a un intérprete, ¡generando su propio compilador! ( Nuevamente, esto es una locura: estamos compilando un intérprete, agregando otro compilador independiente e independiente).

Al final, el resultado es un ejecutable independiente que interpreta el código fuente de Python y aprovecha las optimizaciones JIT. ¡Que es justo lo que queríamos! Es un bocado, pero tal vez este diagrama ayude:

Este diagrama ilustra la belleza de la implementación de PyPy, que incluye un intérprete, un compilador y un ejecutable con JIT.

Para reiterar, la verdadera belleza de PyPy es que podemos escribir un montón de diferentes intérpretes de Python en RPython sin preocuparnos por JIT. PyPy luego implementaría JIT para nosotros utilizando RPython Toolchain/PyPy (2).

De hecho, si nos volvemos aún más abstractos, teóricamente podría escribir un intérprete para cualquier idioma, enviarlo a PyPy y obtener un JIT para ese idioma. Esto se debe a que PyPy se enfoca en optimizar al intérprete real, en lugar de los detalles del idioma que está interpretando.

En teoría, podría escribir un intérprete para cualquier idioma, enviarlo a PyPy y obtener un JIT para ese idioma.

Como breve digresión, me gustaría mencionar que el JIT en sí mismo es absolutamente fascinante. Utiliza una técnica llamada rastreo, que se ejecuta de la siguiente manera:

  1. Ejecute el intérprete e interprete todo (sin agregar JIT).
  2. Realice un perfilado ligero del código interpretado.
  3. Identifique las operaciones que ha realizado antes.
  4. Compile estos bits de código hasta el código de máquina.

Para más información, este documento es muy accesible y muy interesante.

Para concluir: usamos el compilador RPython-to-C (u otra plataforma de destino) de PyPy para compilar el intérprete implementado por RPython de PyPy.

Terminando

Después de una larga comparación de las implementaciones de Python, tengo que preguntarme: ¿Por qué es tan bueno? ¿Por qué vale la pena seguir con esta loca idea? Creo que Alex Gaynor lo expresó bien en su blog: "[PyPy es el futuro] porque [ofrece] mejor velocidad, más flexibilidad y es una mejor plataforma para el crecimiento de Python".

En breve:

  • Es rápido porque compila el código fuente a código nativo (usando JIT).
  • Es flexible porque agrega el JIT a su intérprete con muy poco trabajo adicional.
  • Es flexible (nuevamente) porque puede escribir sus intérpretes en RPython , que es más fácil de extender que, digamos, C (de hecho, es tan fácil que hay un tutorial para escribir sus propios intérpretes).

Apéndice: Otros nombres de Python que puede haber escuchado

  • Python 3000 (Py3k): un nombre alternativo para Python 3.0, una versión importante de Python incompatible con versiones anteriores que llegó al escenario en 2008. El equipo de Py3k predijo que esta nueva versión tardaría unos cinco años en adoptarse por completo. Y aunque la mayoría de los desarrolladores de Python (advertencia: afirmación anecdótica) siguen usando Python 2.x, la gente es cada vez más consciente de Py3k.

  • Cython: un superconjunto de Python que incluye enlaces para llamar a funciones C.
    • Objetivo: permitirle escribir extensiones C para su código Python.
    • También le permite agregar escritura estática a su código Python existente, lo que le permite compilarse y alcanzar un rendimiento similar al de C.
    • Esto es similar a PyPy, pero no lo mismo. En este caso, está obligando a escribir el código del usuario antes de pasarlo a un compilador. Con PyPy, escribe Python simple y antiguo, y el compilador maneja cualquier optimización.

  • Numba: un "compilador especializado justo a tiempo" que agrega JIT al código Python anotado . En los términos más básicos, le das algunas pistas y acelera partes de tu código. Numba viene como parte de la distribución Anaconda, un conjunto de paquetes para el análisis y la gestión de datos.

  • IPython: muy diferente a cualquier otra cosa discutida. Un entorno informático para Python. Interactivo con soporte para kits de herramientas GUI y experiencia de navegador, etc.

  • Psyco: un módulo de extensión de Python y uno de los primeros esfuerzos JIT de Python. Sin embargo, desde entonces ha sido marcado como "sin mantenimiento y muerto". De hecho, el desarrollador principal de Psyco, Armin Rigo, ahora trabaja en PyPy.

Enlaces del lenguaje Python

  • RubyPython: un puente entre las máquinas virtuales Ruby y Python. Le permite incrustar código Python en su código Ruby. Usted define dónde comienza y se detiene Python, y RubyPython clasifica los datos entre las máquinas virtuales.

  • PyObjc: enlaces de lenguaje entre Python y Objective-C, actuando como un puente entre ellos. En la práctica, eso significa que puede utilizar bibliotecas de Objective-C (incluido todo lo que necesita para crear aplicaciones OS X) desde su código de Python y módulos de Python desde su código de Objective-C. En este caso, es conveniente que CPython esté escrito en C, que es un subconjunto de Objective-C.

  • PyQt: mientras que PyObjc le brinda enlace para los componentes de la GUI de OS X, PyQt hace lo mismo para el marco de la aplicación Qt, lo que le permite crear interfaces gráficas ricas, acceder a bases de datos SQL, etc. Otra herramienta destinada a llevar la simplicidad de Python a otros marcos.

Marcos JavaScript

  • pyjs (Pyjamas): un marco para crear aplicaciones web y de escritorio en Python. Incluye un compilador de Python a JavaScript, un conjunto de widgets y algunas herramientas más.

  • Brython: una máquina virtual Python escrita en JavaScript para permitir que el código Py3k se ejecute en el navegador.