R

ReflexKR

Documentación de Reflex

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

El aeropuerto de las etiquetas: el modelo de objetos de Python
Lección 6

Tiempo 4:10–5:00

Equipaje de mano: clausuras

Parte de equipaje perdido

LOST-006

Tres pasajeros, todos a la misma puerta

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.

6-1. Funciones que devuelven funciones (7 min)

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>.tag
  • Cada vez que llamas a make_tag, se ejecuta def tag(): y se crea un objeto función nuevo.
  • En el momento en que llamas a 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.
  • Sin embargo, t() recuerda "kim". ¿Cómo conserva la función interior un nombre de una función que ya terminó?

6-2. El equipaje de mano: celdas y __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',)
AtributoSignificado
inner.__code__.co_freevarsLos 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_contentsEl objeto al que apunta la celda en este momento
outer.__code__.co_cellvarsLos 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)

Cuando make_tag termina, su casilla desaparece, pero la cell que lleva la función tag sigue sujetando 'kim'.
  • En el equipaje de mano solo entran los nombres que la función interior usa de verdad. Los demás nombres locales de la función exterior se limpian cuando esta termina.
  • Al revés, lo que entra se queda mientras la función exista. Si la función interior usa una lista grande aunque sea una vez, esa lista se queda también.
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.

6-3. Enlace tardío: por qué los tres pasajeros fueron a C3 (10 min)

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])
# True
  • Los bucles no crean zona (lección 5). Durante las tres vueltas, gate es un único nombre dentro de make_guides, y la celda también es una sola.
  • Las tres lambda se llevan esa misma celda. Cuando termina el bucle, el valor de la celda es "C3".
  • El cuerpo de la 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])
# True

Ni 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()())
# Z9

En 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)

Con lambda: gate, el gate de las tres funciones apunta a la misma cell. Corrigiéndolo con gate=gate, cada función guarda en un argumento por defecto el valor que había al crearla.

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.
  • El método del argumento por defecto tiene una pega: quien llama puede sobrescribir el valor, como en f(5).

6-4. Funciones con estado: un contador con 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 4
  • Cada llamada a counter() crea una celda nueva. c y c2 no saben nada del número del otro.
  • Sin 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)

Cada vez que llamas a tick, el valor de dentro de la cell pasa de 0 a 1 y luego a 2. Los enteros son inmutables: no cambia el entero, sino que la etiqueta de la celda se muda a un entero nuevo.

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
ClausuraClase
Dónde está el estadoEn la celdaEn el __dict__ de la instancia
Verlo desde fueraSolo con __closure__Directamente con k.n
Cuándo encajaUna acción, poco estadoVarias acciones, mucho estado

6-5. Un primer vistazo a los decoradores (7 min)

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.
  • Por eso 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 board

Los temas más profundos, como pasar argumentos a un decorador o escribir decoradores con clases, se tratan en el curso de generadores y decoradores.

Caso cerrado (10 min)

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.

Ejercicios

  1. Si cambias [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.
  2. Modifica counter() para que devuelva dos funciones a la vez, tick() y reset(). Comprueba con is si las dos usan la misma celda.
  3. Escribe un decorador 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.

Datos: tu correo electrónico · Finalidad: avisos de nuevas lecciones o de la publicación de este curso · Conservación: hasta que pidas que lo borremos. No lo usamos para nada más ni lo compartimos. Responde a cualquier aviso y lo borraremos enseguida.

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.

Se muestra como texto plano, sin enlaces ni formato.

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
한국어English日本語简体中文EspañolDeutsch

© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0

Built with Reflex