El aeropuerto de las etiquetas: el modelo de objetos de Python
·
Resumen del curso
1
La etiqueta no es la maleta: nombres y objetos
2
Maletas selladas: mutable e inmutable
3
Maletas dentro de maletas: contenedores y copias
4
Al entregar una maleta: funciones y argumentos
5
Zonas y pases: ámbitos
6
Equipaje de mano: clausuras
7
La especificación de la maleta: clases e instancias
8
Procedimientos estándar del aeropuerto: métodos dunder
9
La especificación de la especificación: herencia y MRO
10
El control: el secreto del acceso a atributos
11
Maletas sin dueño: memoria y GC
12
Números de taquilla: hash, igualdad y repaso final
Tiempo 4:10–5:00
Parte de equipaje perdido
LOST-006
Lo reporta
Seo, responsable de los avisos de embarque por megafonía
Qué se perdió
Tres funciones de aviso de puerta de embarque. Creó en un bucle las de A1, B7 y C3, pero las tres mandan a los pasajeros a C3.
Para cerrar el caso
Comprobar qué llevan consigo las tres funciones, y hacer que cada una recuerde su propia puerta.
Aeropuerto Internacional Haneulseom · Centro de equipajes
Justo después del aviso de la mañana, tres pasajeros que debían ir a puertas distintas están todos delante de C3. Seo insiste en que creó tres funciones de aviso, así que debería haber tres avisos distintos.
La supervisora levanta una maleta de mano. «Las funciones también llevan equipaje. La cuestión es si lo que llevan dentro es un valor o una etiqueta.» Hoy abrimos la pequeña maleta que llevan consigo las funciones: la clausura.
Como vimos en la lección 1, las funciones también son objetos. Puedes atarlas a un nombre, meterlas en una lista, pasarlas a otra función y devolverlas.
def make_tag(owner):
def tag():
return "owner: " + owner
return tag
t = make_tag("kim")
print(t())
# owner: kim
print(t.__name__, t.__qualname__)
# tag make_tag.<locals>.tagmake_tag, se ejecuta def tag(): y se crea un objeto función nuevo.t(), make_tag ya ha terminado. Según las reglas de la lección 5, el nombre local owner de make_tag debería haber desaparecido con ella.t() recuerda "kim". ¿Cómo conserva la función interior un nombre de una función que ya terminó?__closure__ (9 min)Cuando una función interior usa un nombre de la función exterior, Python guarda ese nombre en una pequeña maleta llamada celda (cell). La casilla de la función exterior y la función interior apuntan a la misma celda. Aunque la función exterior termine y su casilla desaparezca, la función interior sigue sujetando la celda, así que la celda y su valor se quedan. Una función que lleva consigo este paquete de celdas es una clausura.
def make_tag(owner):
def tag():
return "owner: " + owner
return tag
t = make_tag("kim")
print(t.__code__.co_freevars)
# ('owner',)
print(type(t.__closure__[0]).__name__)
# cell
print(t.__closure__[0].cell_contents)
# kim
print(make_tag.__code__.co_cellvars)
# ('owner',)| Atributo | Significado |
|---|---|
inner.__code__.co_freevars | Los nombres que la función interior toma prestados de fuera (variables libres) |
inner.__closure__ | Una tupla de celdas, una por cada uno de esos nombres |
cell.cell_contents | El objeto al que apunta la celda en este momento |
outer.__code__.co_cellvars | Los nombres que la función exterior ha convertido en celdas |
En el tablero, mientras make_tag se ejecuta, el owner de su casilla se muestra con el valor que hay dentro de la celda. Fíjate en que, incluso después de que la casilla desaparezca, la fila owner de function tag sigue sujetando 'kim' a través de cell.
Tablero de etiquetas
Paso 1 / 0
Lo siguiente que se ejecuta: 0
Preparando el laboratorio…
Acaba de cambiar
Maleta sellada (inmutable)
def make_tag(owner):
notes = ["memo"] * 1000
def tag():
return owner
return tag
print(make_tag("kim").__code__.co_freevars)
# ('owner',)Pin dice
→ Lo que lleva una clausura no es una copia del valor, sino una celda: el sitio del que cuelga la etiqueta. Por eso, si fuera mueves ese nombre, el valor que ve la clausura también cambia.
Este es el código del parte:
def make_guides(gates):
guides = []
for gate in gates:
guides.append(lambda: gate)
return guides
gs = make_guides(["A1", "B7", "C3"])
print([g() for g in gs])
# ['C3', 'C3', 'C3']
print(gs[0].__closure__[0] is gs[2].__closure__[0])
# Truegate es un único nombre dentro de make_guides, y la celda también es una sola.lambda se llevan esa misma celda. Cuando termina el bucle, el valor de la celda es "C3".lambda, gate, abre la celda en el momento de la llamada. No apunta el valor al crearse. Esto se llama enlace tardío (late binding).Con una comprensión de listas pasa lo mismo.
fs = [lambda: i for i in range(3)]
print([f() for f in fs])
# [2, 2, 2]
print(fs[0].__closure__[0] is fs[1].__closure__[0])
# TrueNi siquiera hace falta un bucle. Si después de crear la función mueves el nombre fuera de ella, el valor que ve la función también cambia.
def make():
gate = "A1"
def guide():
return gate
gate = "Z9"
return guide
print(make()())
# Z9En el tablero de abajo, fíjate en cómo las flechas de las tres funciones convergen en una sola celda. En la versión corregida, cada función tiene su propia tupla defaults.
Tablero de etiquetas
Paso 1 / 0
Lo siguiente que se ejecuta: 0
Preparando el laboratorio…
Acaba de cambiar
Maleta sellada (inmutable)
La solución es guardar aparte en la función «el valor del momento en que se crea».
fs = [lambda i=i: i for i in range(3)]
print([f() for f in fs])
# [0, 1, 2]
from functools import partial
def guide(gate):
return "go to " + gate
guides = [partial(guide, gate) for gate in ["A1", "B7", "C3"]]
print([g() for g in guides])
# ['go to A1', 'go to B7', 'go to C3']lambda i=i: i: los argumentos por defecto se evalúan una sola vez, al definir la función (lección 4). El i de ese momento se guarda en el __defaults__ de la función.functools.partial(guide, gate): crea un nuevo objeto invocable que une la función con el argumento de ese momento. Su intención es más clara que la del truco del argumento por defecto.f(5).nonlocal (7 min)Las celdas no son solo de lectura. Si declaras el nombre con nonlocal (lección 5), la función interior puede mover la etiqueta que hay dentro de la celda. Así la función tiene un estado que se mantiene de una llamada a otra.
def counter():
n = 0
def tick():
nonlocal n
n += 1
return n
return tick
c = counter()
c()
c()
print(c(), c.__closure__[0].cell_contents)
# 3 3
c2 = counter()
print(c2(), c())
# 1 4counter() crea una celda nueva. c y c2 no saben nada del número del otro.nonlocal n, n += 1 convierte n en un nombre local y salta un UnboundLocalError (la misma causa que en la lección 5).Tablero de etiquetas
Paso 1 / 0
Lo siguiente que se ejecuta: 0
Preparando el laboratorio…
Acaba de cambiar
Maleta sellada (inmutable)
Lo mismo se puede hacer con una clase. El estado se guarda en los atributos de la instancia (lección 7).
class Counter:
def __init__(self):
self.n = 0
def tick(self):
self.n += 1
return self.n
k = Counter()
k.tick()
print(k.tick(), k.n)
# 2 2| Clausura | Clase | |
|---|---|---|
| Dónde está el estado | En la celda | En el __dict__ de la instancia |
| Verlo desde fuera | Solo con __closure__ | Directamente con k.n |
| Cuándo encaja | Una acción, poco estado | Varias acciones, mucho estado |
Una clausura que recibe una función y devuelve una función nueva se llama decorador. La función original se guarda en la celda de la función interior.
def log(func):
def wrapper(*args, **kwargs):
print("call", func.__name__)
return func(*args, **kwargs)
return wrapper
@log
def board(seat):
return "seat " + seat
print(board("12A"))
# call board
# seat 12A
print(board.__name__)
# wrapper@log equivale a board = log(board). El nombre board apunta ahora a la función wrapper, y el board original está en la celda func de wrapper.board.__name__ pasa a ser wrapper, lo cual molesta al depurar y al documentar.Una sola línea con functools.wraps traslada el nombre, la documentación y demás de la función original.
import functools
def log(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("call", func.__name__)
return func(*args, **kwargs)
return wrapper
@log
def board(seat):
return "seat " + seat
print(board.__name__, board.__wrapped__.__name__)
# board boardLos temas más profundos, como pasar argumentos a un decorador o escribir decoradores con clases, se tratan en el curso de generadores y decoradores.
Basta con que cada función guarde la puerta que le tocaba al crearse.
from functools import partial
def announce(gate):
return "go to " + gate
def make_guides(gates):
return [partial(announce, gate) for gate in gates]
gs = make_guides(["A1", "B7", "C3"])
print([g() for g in gs])
# ['go to A1', 'go to B7', 'go to C3']
def make_guides_v2(gates):
return [lambda gate=gate: "go to " + gate for gate in gates]
print([g() for g in make_guides_v2(["A1", "B7", "C3"])])
# ['go to A1', 'go to B7', 'go to C3']Desde el siguiente aviso, cada pasajero va a su propia puerta. Cuando Seo se marcha, la supervisora hojea el registro de la cinta 13 y murmura: «Lo que deja una función que ya terminó no se tira si alguien lo lleva en su equipaje de mano. Puede que alguien esté sujetando también esas dos maletas.»
Informe de cierre — LOST-006. Las tres funciones de aviso compartían la única celda de la variable de bucle gate y, al llamarlas, leían el último valor, C3 (enlace tardío). Resuelto guardando el valor del momento de creación con partial y con argumentos por defecto.
[lambda: i for i in range(3)] por [lambda: i * 10 for i in range(3)], ¿qué resultado obtienes? Comprueba con __closure__ cuántas celdas hay.counter() para que devuelva dos funciones a la vez, tick() y reset(). Comprueba con is si las dos usan la misma celda.count_calls que cuente cuántas veces se llama a una función. ¿Guardarás el número en un atributo de wrapper o en una celda? Prueba las dos formas.La libreta de la supervisora
Las funciones también son objetos. Se pueden crear y devolver funciones.
Los nombres de fuera que usa una función interior se guardan en celdas, y la función lleva esas celdas en __closure__.
Lo que lleva una clausura no es el valor, sino la celda (el sitio de la etiqueta). Se abre al llamar: enlace tardío.
Las funciones creadas en un bucle comparten una sola celda. Solución: lambda i=i: i, functools.partial.
Mover con nonlocal la etiqueta de la celda da una función con estado. Si hay mucho estado, una clase.
Un decorador es una clausura que recibe una función y devuelve una función. No olvides functools.wraps.
← Anterior
Lección 5 · Zonas y pases: ámbitos
Siguiente →
Lección 7 · La especificación de la maleta: clases e instancias
Recibe avisos de nuevas lecciones
Te avisaremos por correo cuando este curso tenga lecciones nuevas o se abra un curso de continuación.
R
ReflexKR
Aprende Python y Reflex haciendo. Todos los ejemplos se ejecutaron y verificaron en Reflex 0.10.0.
ReflexKR es gratis y sin banners publicitarios. Si te ayudó, puedes apoyarlo con un café. ☕ Invítanos un café
Aviso de afiliados© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
Preguntas y respuestas
Pregunta lo que quieras sobre esta lección. Aquí se reúnen las preguntas de todas las versiones de idioma de esta página.