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
olambda
), y no declarados como globales en esa función); Enclosing
(nombres en el ámbito local de cualquier función incluyente estática (def
olambda
), de adentro hacia afuera);Global
(nombres asignados en el nivel superior del archivo del módulo, o mediante la ejecución de la instrucciónglobal
endef
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.
GO TO FULL VERSION