Title: Nuevas características de python 3.8

Date: 2019-12-13
Content:

Hace un par de meses el equipo que está detrás de Python anunció una nueva subversión de Python3 llamada python3.8. En esta nueva subversión de incluyeron unas cuantas características y algunas de ellas me parecieron especialmente interesantes entre las cuales se encuentran:

1) Expresiones de asignación

Esta fue, quizás, la característica que mas me hace ilusión ya que por fin tenemos una manera elegante de poder la operación la cual denomino "asignar y retornar". En una gran cantidad de lenguajes de scripting (PHP, JS, Ruby por poner un ejemplo) es común ver que la operación de asignación también retorna el valor que estamos asignando y en Python esto no ocurría. Para ello, el equipo de Python decidió implementar un nuevo operador :=... Es idéntico al operador al operador de asignación de Go, pero su funcionamiento es muy distinto. Observen que es lo que sucede cuando usamos el clásico = y el nuevo :=:

> var1 = 10
    <══ La operación de asignación no retorna nada

> (var2 := 20)
        ^
        ╚══ Nuevo operador
20  <══ La nueva expresión de asignación retorna el valor asignado a la variable "var2"

Como se puede ver en el ejemplo, su funcionamiento, en principio, es idéntico al operador de asignación de toda la vida, lo único que cambió es que ahora como resultado de la operación, el interprete retorna lo asignado en la variable. Ahora que ya está todo claro seguramente se preguntarán "Y esto ¿qué uso práctico tiene?", bueno, a juzgar por la documentación oficial[2] su implementación se debe a que ahorita en el lenguaje se escriben demasiados while True: e if's con una variable justo arriba. . Aquí algún ejemplo práctico que se me ocurre a mi y otro que se puede encontrar en la documentación:

meter números a una lista y parar cuando se introduzca un 0

# Ahora
lista = []
while x := int(input()):
      ^ ^
      ║ ╚══ Nuevo operador de asignación
      ╚════ Variable a la cual se le asignará el valor
    lista.append(x)

# Antes
lista = []
while True:
    x = int(input())
    if not x:
        break
    lista.append(x)

En este primer ejercicio se puede ver claramente como se redujo el número de lineas pasando de 6 a unicamente 3 mejorando el rendimiento pues solo es necesario hacer una comparación (la del while) en lugar de 2 (la de while que siempre iba a dar True y la de if para romper el ciclo) sin perder legibilidad en el código.

Sumar zona horaria (Ejemplo extraído de la documentación oficial)

# Ahora
s = _format_time(self._hour, self._minute,
                 self._second, self._microsecond,
                 timespec)
if tz := self._tzstr():
   ^  ^
   ║  ╚══ Nuevo operador de asignación
   ╚════ Variable a la cual se le asignará el valor
    s += tz
return s

# Antes
s = _format_time(self._hour, self._minute,
                 self._second, self._microsecond,
                 timespec)
tz = self._tzstr()
if tz:
    s += tz
return s

En este otro ejemplo extraído de la documentación oficial se puede ver como se evita declarar variables antes del if para reducir la cantidad de lineas sin tener perjudicar a la legibilidad del código.

2) Argumentos unicamente posicionales

Este cambio, a diferencia del anterior, lo veo como una mejoría unicamente a nivel de "belleza" del código, ya que esto no se verá reflejado al momento de que el código esté siendo interpretado. El cambio consiste en que ahora se podrá hacer que las funciones y métodos acepten los argumentos obligatoriamente en orden... ¿Esto con qué fin? unicamente estético a nivel de código y eso conlleve en una tortura mas ligera al momento de darle mantenimiento a código legacy (En el futuro claro, ahorita el código legacy seguramente estará escrito en python2). Para ello unicamente tendremos que colocar un / después de los argumentos que tendrán un orden obligatorio, aquí unos ejemplos:

Función para elevar un número al cuadrado

def pow(num, exp, /):
                  ^
                  ╚══ Parámetro "/" para indicar argumentos
                      Unicamente posicionales
    return num ** exp

# Uso correcto
>>> pow(5, 2)
25

>>> pow(exp=2, num=5)
Traceback (most recent call last):
...
TypeError: pow() got some positional-only arguments passed as keyword arguments: 'num, exp'

>>> pow(num=5, exp=2)
Traceback (most recent call last):
...
TypeError: pow() got some positional-only arguments passed as keyword arguments: 'num, exp'

Como se puede apreciar en el ejemplo, arroja un error cuando mandamos llamar a la función utilizando los nombres de los parámetros, aunque intentemos pasarlos en el orden que fueron declarados en un principio. Pero aquí no acaba la cosa, ya que posterior a la / podremos declarar mas parámetros y estos volverán a tener el comportamiento habitual como en versiones anteriores de Python.

Sacar la edad de una persona y saludarla

from datetime import datetime
def say_hello(year, month, day, /, first_name, last_name):
                                ^
                                ╚══ Parámetro "/" para indicar argumentos
                                    Unicamente posicionales
    date = datetime(year, month, day)
    diference = datetime.now() - date
    years = int(diference.days // 365.25)
    print(f"hello {first_name} {last_name} you're {years} years old")

>>> say_hello(1810, 11, 23, "foo", "bar")
hello foo bar you're 208 years old

>>> say_hello(1910, 5, 4, last_name="foo", first_name="bar")
hello bar foo you're 109 years old

>> say_hello(day=28, month=7, year=2013, first_name="python", last_name="3.8")
Traceback (most recent call last):
...
TypeError: say_hello() got some positional-only arguments passed as keyword arguments: 'year, month, day'

Como nos podemos dar cuenta, en la cabecera de la función los parámetros first_name y last_name están después de la / y solo por eso ya se pueden utilizar de la manera antigua pudiendo asignarlos utilizando su nombre (ver caso 2).

3) Especificador "=" en f-strings

Este cambio me parece buenísimo y es muy fácil de explicar y rápido de entender, se trata de que ahora las operaciones que hagamos dentro de un f-string las podremos poner dentro del propio f-string sin tener que escribir 2 veces la misma operación.

# Ahora
>>> print(f"{10+5=}")
                 ^
                 ╚══ Signo "=" para indicar que es una
                     expresión de debugeo
10+5=15

>>> print(f"{10+5-22/34**55//5=}")
                              ^
                              ╚══ Signo "=" para indicar que es una
                              expresión de debugeo
10+5-22/34**55//5=15.0

>>> print(f"{(a:=10)=} \t {10*a=}")
                    ^          ^
                    ╚══════════╩══ Signo "=" para indicar que es una
                                   expresión de debugeo
(a:=10)=10 	 10*a=100

# Antes
>>> print(f"10+5={10+5}")
10+5=15

>>> print(f"10+5-22/34**55//5={10+5-22/34**55//5}")
10+5-22/34**55//5=15.0

Como pueden apreciar en el caso 3, se pueden declarar variables DENTRO del propio f-string gracias a nuestro precioso operador := y se visualizará exactamente como lo hayamos escrito. Para debugear con print's esto será una genialidad.

4) Nuevas validaciones para tipos de datos

Como bien sabemos, en Python existen una buena cantidad de herramientas para ESPECIFICAR (mas no validar) algunas pautas que el programador debe seguir al momento de estar de estar escribiendo su código. Estas pautas realmente no le dicen nada al interprete cuando nuestro programa se está ejecutando, solo sirve para que el linter (si lo soporta) nos arroje ciertos errores cuando estamos escribiendo código. En python3.8 incluyeron algunas nuevas características para marcar mas pautas y son las siguientes:

4.1) Calificador "final"

El paquete typing incluye una gran cantidad de herramientas para escribir unas pautas mas "estrictas" (estrictas entre comillas, ya que realmente al interprete le dan completamente igual). Entre esas características, en Python3.8 incluyeron un nuevo calificador (que así es como le llaman a las pautas) que permite decir de cual clase NO deberíamos heredar, cual método NO deberíamos sobrescribir, cual método SI deberíamos sobrescribir y cuales atributos NO deberíamos modificar. Para ello incluye un decorador y un tipo de dato, su uso es el siguiente: Las clases de las cuales no deberíamos heredar ahora se deberían decorar con @final como en el siguiente ejemplo:

Decorando clases

from typing import final

@final <══ Decorando la clase con el decorador "final" del paquete "typing"
class Base:
    pass

class A(Base):
        ^ ^
        ╚═╩═ Utilizando la clase "Base" para heredar de ellas
    pass

Como he repetido anteriormente, esto solo es una validación a nivel de linter y para una auto-documentación mas explícita ya que al ejecutar el código anteriormente descrito, este no presentará error alguno a nivel de interprete. El linter debería arrojar un error.

Decorando métodos

from typing import final

class Base:
    @final <══ Decorando el método con el decorador "final" del paquete "typing"
    def method_1(self):
        pass

class A(Base):
    def method_1(self):
        ^ ^ ^ ^
        ╚═╩═╩═╩══ Reescribiendo un método decorado con "final"
                  en la clase padre
        pass

Esto también debería arrojar un error al momento de ser inspeccionado por un linter. Pero repito, el interprete nos dejará sobrescribir estos métodos sin ningún mensaje de error.

Declarando atributos

from typing import Final
                   ^ ^ ^
                   ╚═╩═╩═ Importando el tipo "Final" (Con "F" mayúscula)
                          del paquete "typing"

var_1: Final = "Hello world"
       ^ ^ ^
       ╚═╩═╩╦═╦═╦══ Declarando las variables de tipo "Final"
            ║ ║ ║
class Base: v v v
    attr_1: Final = 10

var_1 = "Good bye world" <═╦══ Intentado asignarle nuevos valores a variables
Base.attr_1 = -1         <═╝   del tipo "Final"

También esto puede aplicar para variables sueltas del programa o para atributos internos de una clase. Al declararlas del tipo Final el linter debería arrojar un error cuando se intenten asignar nuevos valores.

Si quieres saber mas sobre decoradores puedes pasarte por mi entrada anterior.

Conclusiones

Estos no fueron todos los cambios, como dije al principio, solo fueron los que me parecieron mas útiles y que se podían explicar de una manera relativamente sencilla. Los otros eran mas cambios relacionados al multithreading y a la carga de información utilizando CPython... No digo que no sean irrelevantes esos cambios, solo que me quise centrar mas en cambios relacionados a cosas de sintaxis. Sin duda alguna mis cambios favoritos en esta versión fue el operador de asignación y retorno ya que, en mi opinión, hará posible la escritura de un código mas limpio. Puedes consultar todos los cambios en la sección de "Fuentes". Muchas gracias por leerme otro día mas.

Fuentes

  1. Changelog
  2. Ejemplos de implementación de la expresión de asignación
  3. Artículo oficial de Python sobre esta versión