Ámbito y valores por defecto mutables
Dónde vive un nombre y quién lo ve, la diferencia entre leer y escribir un nombre exterior, UnboundLocalError, y la trampa más famosa de Python — la lista vacía como valor por defecto.
¿Cómo quieres aprender?
El problema que vamos a resolver
La lista "cuando falla" del capítulo diecinueve dejó una línea sin resolver: "Una variable dentro de la función no se ve fuera — eso es normal, y por qué es el tema del capítulo veintiuno."
Este es ese capítulo.
def add_tax(price):
tax = price * 0.15
return price + tax
print(add_tax(100))
print(tax)115.0
NameError: name 'tax' is not defined. Did you mean: 'max'?La primera línea funcionó, así que tax debía existir. Y fuera, no existe. (Antes de la línea del error Python también imprime unas líneas de rastreo; en este capítulo solo se muestra la última.)
Esto es protección, no limitación. En un programa con cincuenta funciones, si cada nombre interno se filtrara hacia afuera, las variables total de dos funciones se sobrescribirían — y nadie lo notaría. Los nombres dentro de una función se quedan dentro, así que escribir una función no exige conocer los nombres de las otras cuarenta y nueve.
Este capítulo es esa regla, y una consecuencia de ella que es la trampa más famosa de Python.
Al terminar este capítulo podrás
- Decir dónde "vive" un nombre y quién puede verlo
- Leer un nombre exterior desde dentro de una función, y explicar por qué no puedes escribir uno
- Leer un
UnboundLocalErrory decir qué pasó - Explicar por qué una lista pasada a una función puede volver cambiada
- Reconocer y evitar la trampa más famosa de Python: el valor por defecto mutable
Requisitos previos: Argumentos de función.
Lo de dentro se queda dentro
Todo nombre creado dentro de una función —parámetros incluidos— vive solo durante esa llamada. Cuando la función termina, los nombres desaparecen.
A esto se le llama ámbito: la región en la que un nombre es conocido.
Leer un nombre exterior funciona
RATE = 0.15
def add_tax(price):
return price + price * RATE
print(add_tax(100))115.0No hay RATE dentro de la función, así que Python busca fuera y lo encuentra. Leer un nombre exterior desde dentro funciona.
El nombre en mayúsculas es una convención, no una regla de Python — le dice al lector "esto es una constante, no cambia". Usado así, un nombre exterior es cuando está más seguro.
Pero escribir uno no funciona
count = 10
def bump():
count = 99
return count
print(bump())
print(count)99
10El count = 99 interior nunca tocó el count exterior. Creó un nombre nuevo, local que vive solo dentro de la función y escondió el exterior mientras duró.
Esa es la regla, y es deliberada: una función no puede cambiar algo fuera por accidente.
Y de ahí sale un error de aspecto extraño:
count = 10
def bump():
count = count + 1
return count
print(bump())UnboundLocalError: cannot access local variable 'count' where it is not associated with a valueLa línea se lee como si tomara el count exterior y le sumara uno. Pero Python inspecciona la función entera antes de ejecutarla, ve que algo asigna a count, y decide que count es local a esta función de principio a fin.
Luego, al evaluar count + 1 a la derecha, busca el count local, que aún no tiene valor — y se para.
El mensaje es por tanto inusualmente preciso: el nombre es local, y aún no se le ha asignado ningún valor.
La solución casi siempre es la misma, y no es global: toma el valor como argumento y return el resultado.
def bump(count):
return count + 1
count = bump(count)Python tiene una palabra clave global, y este curso no la enseña, porque el problema para el que la gente la busca tiene una respuesta mejor. Una función que cambia estado exterior no se puede entender desde donde se la llama — y esa es la definición de código difícil de probar y poco fiable.
Pero lo que pasaste puede cambiar
El ámbito protege nombres, no objetos. La distinción es sutil e importante.
def add_item(items):
items.append("pen")
return items
basket = []
add_item(basket)
add_item(basket)
print(basket)['pen', 'pen']El basket exterior cambió, aunque no se capturó el return.
La razón es la del capítulo doce de nuevo: el nombre items es nuevo, pero apunta a la misma lista. append modifica esa lista, y ambos nombres muestran el cambio.
Compara:
def replace(items):
items = ["new"]
return items
basket = ["old"]
result = replace(basket)
print(basket)
print(result)['old']
['new']Aquí items = [...] asignó un valor nuevo, apuntando el nombre local a otra lista. El basket exterior sigue donde estaba.
La regla en una línea: modificar el objeto (append, sort, [i] =) es visible fuera; apuntar el nombre a otro objeto (=) no.
La trampa más famosa de Python
def add_item(item, basket=[]):
basket.append(item)
return basket
print(add_item("pen"))
print(add_item("bag"))
print(add_item("ink"))['pen']
['pen', 'bag']
['pen', 'bag', 'ink']Tres llamadas separadas, y cada una guarda lo que las anteriores dejaron.
Entiende la razón y no la olvidarás: el valor por defecto se evalúa una vez, cuando se ejecuta la línea def — no cuando se llama a la función. Esa lista vacía es una única lista adjunta a la función, y todas la comparten.
La solución es convencional y simple:
def add_item(item, basket=None):
if basket is None:
basket = []
basket.append(item)
return basket
print(add_item("pen"))
print(add_item("bag"))['pen']
['bag']None es inmutable, así que compartirlo no cuesta nada, y la lista nueva se crea fresca en cada llamada.
Como regla: nunca escribas una lista, diccionario o conjunto como valor por defecto. Números, texto, True/False y None son seguros, precisamente porque no se pueden cambiar.
¿Por quéis Noney no== None?ispregunta "¿es este el mismísimo objeto?", y solo hay unNoneen todo el programa.==prueba igualdad, que algunos tipos definen de formas sorprendentes. ParaNone,ises a la vez convencional y fiable.
Un ejemplo completo
basket.py:
# Where a name lives decides who can see it and who can change it
TAX_RATE = 0.15
def line_total(price, quantity):
"""Pure: takes values, returns a value, touches nothing outside."""
return round(price * quantity * (1 + TAX_RATE), 2)
def add_line(basket, name, price, quantity):
"""Impure on purpose: it changes the basket it was handed."""
basket.append({"name": name, "total": line_total(price, quantity)})
return basket
def summarise(basket, note=None):
"""`None` as the default, so no list or dict is shared between calls."""
notes = [] if note is None else [note]
total = sum(line["total"] for line in basket)
notes.append(f"{len(basket)} lines, {total:.2f} in total")
return notes
shopping = []
add_line(shopping, "pen", 15.0, 3)
add_line(shopping, "bag", 850.0, 1)
for line in shopping:
print(f"{line['name']:<6} {line['total']:>8.2f}")
print()
for note in summarise(shopping, "morning order"):
print(note)
print()
print("basket outside the function:", len(shopping), "lines")pen 51.75
bag 977.50
morning order
2 lines, 1029.25 in total
basket outside the function: 2 linesTres funciones con tres relaciones distintas con el mundo exterior, y eso es en lo que hay que fijarse.
line_total no toca nada. Toma valores y devuelve un valor. Lee TAX_RATE y nunca la cambia. A una función así se la llama pura — la misma entrada siempre da la misma salida, sin efectos secundarios en ninguna parte. Es la más fácil de probar y en la que es más difícil equivocarse.
add_line cambia algo fuera a propósito. Le entregan una lista y le añade — y la última línea demuestra que el shopping exterior de verdad tiene dos filas. No hay nada malo en eso, mientras el nombre lo diga. Leer add_line te dice que algo se va a añadir.
summarise usa None como valor por defecto. Escrito note=[], cada llamada compartiría una lista, y el segundo informe arrastraría la nota del primero.
Cuando falla
NameError: name 'tax' is not defined Un nombre de dentro de una función se usó fuera. return lo que haga falta fuera.
UnboundLocalError: cannot access local variable ... where it is not associated with a value Un nombre se asigna dentro de la función y se lee antes de esa asignación. Python decidió que era local. Toma el valor como argumento y return el resultado.
Mi lista cambió después de pasársela a una función Ese es el comportamiento esperado — el nombre es nuevo, la lista es la misma. Para evitarlo, escribe items = items.copy() al principio de la función, o construye y return una lista nueva.
La función recuerda sus resultados anteriores Un valor por defecto mutable. No def f(x, acc=[]) sino def f(x, acc=None), con if acc is None: acc = [] dentro.
Usé el mismo nombre en dos funciones — ¿es un problema? No lo es, y ese es todo el propósito del ámbito. Las variables total de dos funciones están completamente separadas.
Paso 4 de 6 — Predice
Comprueba lo que has entendido
Se asigna un valor a count dentro de la función. ¿Qué imprimen las dos líneas?
count = 10
def bump():
count = 99
return count
print(bump())
print(count)- A10 10
- B99 10
- CUn `UnboundLocalError`
- D99 99
No hay return, aun así se imprime la lista exterior. ¿Qué aparece?
def add_item(items):
items.append("pen")
basket = []
add_item(basket)
add_item(basket)
print(basket)- A[]
- B['pen']
- CNone
- D['pen', 'pen']
Dos llamadas separadas, con una lista vacía como valor por defecto. ¿Qué se imprime?
def collect(item, box=[]):
box.append(item)
return box
print(collect("pen"))
print(collect("bag"))- A['pen', 'bag'] ['pen', 'bag']
- B['pen'] ['bag']
- C['pen'] ['pen', 'bag']
- D['pen'] ['pen']
Para responder necesitas una cuenta
Inicia sesión para comprobar tus respuestas
Las preguntas están arriba, y resolverlas mentalmente es lo que de verdad importa. Inicia sesión para ver las respuestas, las explicaciones y las pistas de tres niveles.
Tu turno
Escribe un archivo llamado cart.py con tres funciones:
line_total(price, quantity, tax_rate=0.15)— pura, sin tocar nadaadd_to(cart, name, price, quantity)— añade alcartque le entreganreceipt(cart, header=None)— devuelve una lista de líneas, inventando unheadercuando no se da ninguno
Luego crea un cart vacío, añade unas cosas, e imprime el receipt.
Finalmente, tres experimentos, todos dignos de verse con tus propios ojos:
- Cambia el valor por defecto de
receiptaheader=[], luego llama a la función dos veces e imprime ambos resultados. ¿Qué ves la segunda vez? - Añade
cart = cart.copy()como primera línea deadd_to, luego imprime el cart exterior. ¿Cuántas cosas hay en él, y por qué? - Asigna
TAX_RATE = 0.2dentro de una función, luego imprimeTAX_RATEfuera. ¿Qué valor ves?
Las tres respuestas juntas son el capítulo entero: qué nombres viven dónde, y qué cambios llegan afuera.
Paso 6 de 6
Reto — el quiz del capítulo
Diez preguntas de fácil a difícil. Las últimas son difíciles a propósito.
Inicia sesión para hacer el quiz