Asignaciones como expresiones

Asignaciones como expresiones



En Python las asignaciones no son expresiones (una expresión es cualquier porción de código que retorne un valor) sino sentencias. Esto implica que, por ejemplo, intentar asignar a una variable el resultado de otra asignación sea un error de sintaxis:

>>> b = (a = 5)
  File "<stdin>", line 1
    b = (a = 5)
           ^
SyntaxError: invalid syntax

En otros lenguajes, como en C, esto es perfectamente legal. Observemos:

int a, b;
b = (a = 5);

Luego de la ejecución de estas dos líneas, a y b tienen el valor 5. Esto es porque (a = 5) es al mismo tiempo una asignación (establece el valor de a en 5) como una expresión (retorna ese valor asignado).

A menudo esta dualidad nos permite ahorrar algunas líneas de código. Por ejemplo, convertir

int result;
result = add(7, 5);
if (result > 10) {
    printf("Resultado: %d\n", result);
}

en

int result;
if ((result = add(7, 5)) > 10) {
    printf("Resultado: %d\n", result);
}

En Python el mismo código se vería más o menos así:

result = add(7, 5)
if result > 10:
    print("Resultado:", result)

Para simplificar el bloque como lo hicimos en C, Python 3.8 incluye el operador := que funciona como asignación y expresión al mismo tiempo.

if (result := add(7, 5)) > 10:
    print("Resultado:", result)

Esta pequeña introducción de sintaxis no solamente nos permite compactar el código, sino hacerlo más eficiente. Consideremos, por ejemplo, la siguiente lista de listas de números:

list_of_numbers = [
    [1, 2, 3, 4],
    [5, 6, 7, 8],
    [9, 10, 11, 12],
    [13, 14, 15, 16],
    [17, 18, 19, 20],
    [21, 22, 23, 24]
]

Ahora queremos imprimir una nueva lista cuyos elementos sean la suma de cada una de las listas de números, siempre y cuando el resultado sea múltiplo de cinco. Bien podríamos hacer:

# Imprime [10, 90].
print([sum(numbers) for numbers in list_of_numbers
       if sum(numbers) % 5 == 0])

El problema con esta solución es que sum(numbers) se ejecuta una vez para cada elemento de list_of_numbers y dos veces para cada elemento que cumpla la condición. Hasta ahora, la única forma de evitar ese doble cómputo era volver a un bucle tradicional y crear una variable que almacene sum(numbers).

new_list = []
for numbers in list_of_numbers:
    current_sum = sum(numbers)
    if current_sum % 5 == 0:
        new_list.append(current_sum)
print(new_list)

Pero claro, perdemos la elegancia de la comprensión de listas.

Ahora, a partir de Python 3.8, el nuevo operador permite combinar eficiencia y elegancia en la siguiente solución:

print([current_sum for numbers in list_of_numbers
       if (current_sum := sum(numbers)) % 5 == 0])

La sintaxis variable := expresión puede emplearse en cualquier parte del código en donde quepa una expresión.

El documento PEP 572 — Assignment Expressions, que reúne la historia, discusión y sintaxis de las asignaciones como expresiones, ilustra gratamente cómo fue utilizado para la simplificación de porciones de código de la librería estándar. Por ejemplo, parte la función copy.copy() rezaba así:

reductor = dispatch_table.get(cls)
if reductor:
    rv = reductor(x)
else:
    reductor = getattr(x, "__reduce_ex__", None)
    if reductor:
        rv = reductor(4)
    else:
        reductor = getattr(x, "__reduce__", None)
        if reductor:
            rv = reductor()
        else:
            raise Error(
                "un(deep)copyable object of type %s" % cls)

Introduciendo el nuevo operador obtenemos:

if reductor := dispatch_table.get(cls):
    rv = reductor(x)
elif reductor := getattr(x, "__reduce_ex__", None):
    rv = reductor(4)
elif reductor := getattr(x, "__reduce__", None):
    rv = reductor()
else:
    raise Error("un(deep)copyable object of type %s" % cls)

Mucho más sencillo, compacto y legible.



Deja un comentario