CodeGym /Curso de Java /Python SELF ES /Error estándar, parte 2

Error estándar, parte 2

Python SELF ES
Nivel 20 , Lección 2
Disponible

8.1 Incomprensión de las reglas de alcance en Python

El alcance en Python se basa en la regla llamada LEGB, que es una abreviatura:

  • Local (nombres asignados de cualquier forma dentro de una función (def o lambda), y no declarados como globales en esa función);
  • Enclosing (nombres en el ámbito local de cualquier función incluyente estática (def o lambda), de adentro hacia afuera);
  • Global (nombres asignados en el nivel superior del archivo del módulo, o mediante la ejecución de la instrucción global en def dentro del archivo);
  • Built-in (nombres predefinidos en el módulo de nombres incorporados: open, range, SyntaxError, y otros).
  • Parece bastante sencillo, ¿verdad?

    Sin embargo, hay algunas sutilezas en cómo funciona esto en Python, lo que nos lleva a un problema complejo de programación en Python. Consideremos el siguiente ejemplo:

    
    x = 10
    def foo():
        x += 1
        print(x)
                
    foo()
    Traceback (most recent call last):
        File "<stdin>", line 1, in <module>
        File "<stdin>", line 2, in foo
    UnboundLocalError: local variable 'x' referenced before assignment

    ¿Cuál es el problema?

    El error anterior ocurre porque, cuando asignas un valor a una variable en el alcance, Python automáticamente la considera local para ese ámbito y oculta cualquier variable con el mismo nombre en cualquier alcance superior.

    Por eso, muchos se sorprenden cuando obtienen un UnboundLocalError en el código que antes funcionaba, cuando se modifica añadiendo una declaración de asignación en alguna parte del cuerpo de la función.

    Esta característica especialmente confunde a los desarrolladores cuando trabajan con listas. Consideremos el siguiente ejemplo:

    
    lst = [1, 2, 3]
    def foo1():
        lst.append(5)  # Esto funciona bien...
            
    foo1()
    print(lst)
    [1, 2, 3, 5]
            
    lst = [1, 2, 3]
    def foo2():
        lst += [5]  # ... pero esto falla!
            
    foo2()
    Traceback (most recent call last):
        File "", line 1, in 
        File "", line 2, in foo
    UnboundLocalError: local variable 'lst' referenced before assignment

    ¿Por qué foo2 falla, mientras que foo1 funciona bien?

    La respuesta es la misma que en el ejemplo anterior, pero, según la opinión general, la situación aquí es más delicada. foo1 no aplica un operador de asignación a lst, mientras que foo2 sí lo hace. Recordando que lst += [5] es simplemente un atajo para lst = lst + [5], vemos que estamos intentando asignar un valor a lst (por eso Python supone que está en el alcance local). Sin embargo, el valor que queremos asignar a lst se basa en el propio lst (de nuevo, ahora se supone que está en el ámbito local), que aún no ha sido definido. Y obtenemos un error.

    8.2 Modificación de una lista mientras se itera sobre ella

    El problema en el siguiente trozo de código debería ser bastante obvio:

    
    odd = lambda x: bool(x % 2)
    numbers = [n for n in range(10)]
        
    for i in range(len(numbers)):
        if odd(numbers[i]):
            del numbers[i]  # MALO: Eliminando elemento de una lista mientras se itera sobre ella
        
    Traceback (most recent call last):
            File "", line 2, in 
    IndexError: list index out of range

    Eliminar un elemento de una lista o matriz mientras se itera sobre ella es un problema en Python que es bien conocido por cualquier desarrollador de software experimentado. Pero, aunque el ejemplo anterior puede ser bastante obvio, incluso los desarrolladores experimentados pueden caer en esta trampa en un código mucho más complicado.

    Afortunadamente, Python incluye una serie de paradigmas de programación elegantes que, cuando se usan correctamente, pueden llevar a una simplificación y optimización significativas del código. Una consecuencia agradable adicional de esto es que, en un código más simple, la probabilidad de encontrarse con el error de eliminación accidental de un elemento de una lista mientras se itera sobre ella es significativamente menor.

    Uno de esos paradigmas son las comprensiones de listas. Además, entender cómo funcionan las comprensiones de listas es especialmente útil para evitar este problema en particular, como se muestra en esta implementación alternativa del código anterior, que funciona perfectamente:

    
    odd = lambda x: bool(x % 2)
    numbers = [n for n in range(10)]
    numbers[:] = [n for n in numbers if not odd(n)]  # simplemente elegimos nuevos elementos
    print(numbers)
    # [0, 2, 4, 6, 8]

    ¡Importante! Aquí no ocurre la asignación de un nuevo objeto de lista. Usar numbers[:] es una asignación en bloque de nuevos valores a todos los elementos de la lista.

    8.3 Incomprensión de cómo Python vincula las variables en cierres

    Consideremos el siguiente ejemplo:

    
    def create_multipliers():
        return [lambda x: i * x for i in range(5)]  #Devuelve una lista de funciones!
    
    for multiplier in create_multipliers():
        print(multiplier(2))

    Puedes esperar la siguiente salida:

    
    0
    2
    4
    6
    8
    

    Pero en realidad obtendrás lo siguiente:

    
    8
    8
    8
    8
    8
    

    ¡Sorpresa!

    Esto sucede debido a la vinculación tardía en Python, que significa que los valores de las variables utilizadas en cierres se buscan en el momento en que se llama a la función interna.

    Por lo tanto, en el código anterior, cada vez que se llama a alguna de las funciones devueltas, el valor de i se busca en el ámbito circundante en el momento en que se llama (y para entonces el ciclo ya ha terminado, por lo que i ya ha sido asignado al resultado final, que es 4).

    La solución a este problema común con Python es la siguiente:

    
    def create_multipliers():
        return [lambda x, i = i : i * x for i in range(5)]
    for multiplier in create_multipliers():
        print(multiplier(2))
    
    # 0
    # 2
    # 4
    # 6
    # 8

    ¡Voilà! Usamos aquí argumentos por defecto para generar funciones anónimas para lograr el comportamiento deseado. Algunos podrían llamar a esta solución elegante. Algunos, sutil. Algunos odian estas cosas. Pero si eres un desarrollador de Python, es importante entender esto de todas formas.

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION