Novedades y mejoras de Python 3.11

Introducción

En los últimos años, hemos visto una gran evolución en el lenguaje de programación Python a medida que este ha ido ganando popularidad hasta posicionarse en el podio de los lenguajes de programación más usados. 

La implicación de la comunidad con el desarrollo de la principal implementación del lenguaje (CPython), creando un ciclo de lanzamiento anual, unida a la reciente incorporación del creador del lenguaje (Guido Van Rosum) a Microsoft en el proyecto Faster CPython, parecen indicar una evolución aún mayor en los próximos años y un esfuerzo adicional en la mejora de la eficiencia de CPython y la sintaxis del lenguaje para adaptarse a proyectos cada vez más complejos.

En los siguientes apartados se exploran las características más importantes añadidas en la nueva versión de Python 3.11 con fecha de salida oficial a 24/10/2022.

Mejora de la eficiencia

Se espera que Python aumente su eficiencia entre un 10% y 60% teniendo un incremento del 25% de media.

Esto se logra principalmente gracias a la introducción del intérprete adaptativo especializado. La idea detrás de esta implementación parte de que, a pesar de que Python es un lenguaje de tipado dinámico, existen muchas zonas de código donde los tipos son estables. Por ese motivo, el intérprete tratará de adaptarse al código ejecutado buscando patrones y estabilidad de tipos sustituyendo operaciones genéricas por específicas para dicho tipo.

Esta especialización se dará en fragmentos pequeños de código ejecutados frecuentemente, evitando así el coste de adaptación para código que solo se va a ejecutar una vez. Por este motivo, esta mejora tendrá un impacto mucho más significativo en programas que contengan bloques ejecutados con frecuencia.

El intérprete también se puede desespecializar y volver a utilizar operaciones genéricas en el caso de que el uso del código cambie o que se vuelva un fragmento más dinámico, adaptándose así en función de las necesidades del programa en ejecución.

Vamos a ilustrar un caso en el que esta mejora es perceptible con el siguiente código:

x = 1
for i in range(300):
   x += i

Como observamos, siempre se trabaja con tipos enteros y, por tanto, tenemos un tipo estable para el cual podemos utilizar operaciones optimizadas de adición. La ejecución de este fragmento 1.000.000 de veces en Python 3.10 tiene una duración de 8.822 segundos; mientras que en la versión 3.11, es de 7.110 segundos, suponiendo un incremento significativo del 20%.

Por otra parte, se mejora la eficiencia en tiempo y memoria para llamadas de funciones en tiempo de ejecución y se incrementa la profundidad a la que pueden llegar las funciones recursivas de código Python.

Finalmente, otro aspecto a destacar es el incremento del 10% al 15% en la inicialización de los programas debido a la optimización de tiempos de carga de los módulos base del lenguaje. Esto es especialmente relevante en el caso de scripts de corta duración o que son ejecutados frecuentemente.

Mejoras en la señalización de errores

Un gran progreso en el intérprete para la depuración de código consiste en la señalización de los errores por mensajes de consola. A partir de la versión 3.11 se marcará la expresión exacta que causó la excepción en lugar de solo la línea.

Por ejemplo, para el siguiente código:

def division(x: int, y: int):
   return x / y


print(division(10, 0))

Se nos muestra la siguiente excepción:

Mejoras en gestión de excepciones

Se añade el concepto de Exception Groups, que nos permite agrupar múltiples excepciones y usar la nueva sintaxis except* para gestionarlas.

Así, podemos ejecutar diversos bloques except* para un solo bloque try, pudiendo gestionar múltiples excepciones concurrentes e incluso anidadas.

Por ejemplo, en el siguiente código se puede ver cómo se gestionan dos excepciones agrupadas:

try:
   raise ExceptionGroup("group", [TypeError(), ValueError()])
except* ValueError:
   print("This will be handled first.")
except* TypeError:
   print("Then this will be handled.")

Por otra parte, también se introduce la posibilidad de añadir notas a las excepciones para incrementar su información contextual. Para ello se utiliza el nuevo método add_note().

try:
   raise TypeError("Bad type")
except TypeError as e:
   e.add_note("Extra information")
   raise

Soporte para el formato TOML

Se añade un nuevo módulo tomllib encargado de “parsear” el popular formato TOML que se ha convertido en el nuevo formato de almacenamiento de metadatos para programas Python.  Es tan sencillo de utilizar como:

import tomllib

with open("pyproject.toml","rb") as f:
   data = tomllib.load(f)

También podemos trabajar con strings de python utilizando la función loads:

import tomllib

data = """
python-version = "3.11.0"

dependencies = [
   "numpy>=1.23.0"
]
"""

tomllib.loads(data)

Mejoras en el módulo Typing

Python cada vez está siendo utilizado para construir software más complejo. Debido a esto, se introdujo en la versión 3.5 el módulo typing, que nos ofrece sugerencias de tipos. En este nuevo lanzamiento, dicho módulo ha sido mejorado. 

Se añade el soporte para genéricos variádicos, es decir, tipos genéricos que contienen una cantidad arbitraria de tipos. Esta característica es especialmente útil a la hora de trabajar con tensores o arrays multidimensionales, ya que nos puede brindar información sobre el tipo de las dimensiones de nuestro objeto o el tipo resultante de éstas al aplicar transformaciones. 

También se introduce el tipo Self  pudiendo así anotar correctamente métodos que devuelven una instancia de la misma clase, siendo especialmente útil para anotaciones en casos de herencia.

import abc
from typing import Self

class Shape(abc.ABC):
  def set_scale(self, scale: float) -> Self:
      self.scale = scale
      return self

class Circle(Shape):
   ...

Circle().set_scale(1.0) # The return type is Circle

Finalmente, cabe destacar la introducción del nuevo tipo “LiteralString” que nos permite identificar strings literales. Un posible uso mostrado a continuación la protección contra vulnerabilidades de inyección SQL:

from typing import LiteralString

def execute_query(query: LiteralString):
   ...

execute_query("SELECT * FROM USERS") # Literal
execute_query(f"SELECT * FROM {table}") # Non literal

Otras nuevas características

La versión 3.11 viene con muchas novedades que no son posibles de analizar en detalle en un solo post. Sin embargo, es importante mencionar algunas de las que podrían tener más relevancia:

  • Se ha añadido un context manager para el método chdir permitiendo así cambiar de directorio sólo en el ámbito de dicho context manager
  • Nuevo alias datetime.UTC para datetetime.timezone.UTC.
  • Soporte para grupos atómicos (?>…) y para cuantificadores positivos (*+, ++, ?+, {m,n}+) en el módulo re.
  • Incremento de la precisión a nanosegundo (si está disponible en el sistema Unix) para el método time.sleep().
  • Adición de la clase TaskGroup para el módulo asyncio permitiendo así agrupar tareas y esperar a su finalización.

Conclusión

Las mejoras introducidas en el módulo typing, así como su popularidad, y la mejora en gestión de excepciones nos muestran una tendencia del lenguaje a ser usado por software cada vez más complejo.

Sin embargo, el centro de esta actualización ha sido la mejora en velocidad. A pesar de que su propósito no es ni será convertirse en un lenguaje cuya principal característica sea la eficiencia, vemos que los esfuerzos de la comunidad tienen la mejora de esta como uno de los principales puntos a desarrollar y esto se ve reflejado en la versión 3.11.

Posiblemente, en posteriores versiones, el incremento del rendimiento siga siendo uno de los principales objetivos.

¡Eso es todo! Si este artículo te ha resultado interesante, te animamos a visitar la categoría Software para ver todos los posts relacionados y a compartirlo en redes. ¡Hasta pronto!
Francisco Arenas
Francisco Arenas
Artículos: 4