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 5:50–6:40
Parte de equipaje perdido
LOST-008
Lo reporta
Nam, responsable del transporte de una exposición
Qué se perdió
La clase Crate, para las cajas de las piezas de la exposición. Al poner una caja en la cinta, len(), for y + dan error, y print() solo imprime una dirección incomprensible.
Para cerrar el caso
Dotar a Crate de los procedimientos estándar del aeropuerto (métodos dunder) para que las funciones integradas y los operadores reconozcan la caja.
Aeropuerto Internacional Haneulseom · Centro de equipajes
Nam gestiona las cajas de las piezas de la exposición con una clase escrita a mano. Las cajas están perfectas, pero en cuanto se suben a la cinta, todo se para. La supervisora Do Miran le da una vuelta a la caja. «La cinta no mira la forma de la caja. La agarra por las asas. Y esta caja no tiene ni una.»
La sintaxis y las funciones integradas de Python funcionan agarrando a los objetos por métodos con nombres fijos. Como llevan dos guiones bajos delante y detrás del nombre, se llaman «métodos dunder» (double underscore). Hoy vamos a ponerle esas asas una a una.
Así es la caja del parte. Le pedimos cuatro cosas a la misma caja.
class Crate:
def __init__(self, items):
self.items = list(items)
c = Crate(["vase", "lamp"])
for expr in ["len(c)", "c[0]", "c + c", "iter(c)"]:
try:
eval(expr)
except TypeError as e:
print(f"{expr:7} → {e}")
# len(c) → object of type 'Crate' has no len()
# c[0] → 'Crate' object is not subscriptable
# c + c → unsupported operand type(s) for +: 'Crate' and 'Crate'
# iter(c) → 'Crate' object is not iterablelen(c) no es magia. Python llama a type(c).__len__(c). Como ese método no existe, salta el error de que «no tiene len». Si le pones __len__, funciona al momento.
class Crate:
def __init__(self, items):
self.items = list(items)
def __len__(self):
return len(self.items)
c = Crate(["vase", "lamp"])
print(len(c), type(c).__len__(c))
# 2 2
c.__len__ = lambda: 99
print(len(c), c.__len__())
# 2 99Las dos últimas líneas son importantes. Aunque le pongas __len__ a la instancia, len(c) sigue dando 2. Los métodos dunder que llama la sintaxis se buscan en el tipo (la clase), no en la instancia. La cinta solo sigue los procedimientos escritos en la especificación.
| Sintaxis | Lo que se llama de verdad |
|---|---|
len(x) | type(x).__len__(x) |
x[i], x[i] = v | __getitem__, __setitem__ |
a + b | a.__add__(b) y, si no funciona, b.__radd__(a) |
a == b | __eq__ |
v in x | __contains__ (si no existe, recorre y compara) |
for v in x | __iter__ (si no existe, __getitem__) |
bool(x), if x: | __bool__ (si no existe, __len__) |
print(x), str(x) | __str__ (si no existe, __repr__) |
x() | __call__ |
with x: | __enter__, __exit__ |
A una clase con estas asas bien puestas se la suele llamar «clase pythónica». No hace falta memorizar métodos aparte como size() o merge(): se maneja con len y +, que ya conoces.
La centralita de abajo mete una línea de sintaxis en un Crate con asas y muestra, por orden, los métodos que llamó Python de verdad.
Centralita dunder
1
class Crate:
2
def __init__(self, items):
3
self.items = list(items)
4
def __repr__(self):
5
return f"Crate({self.items})"
6
def __len__(self):
7
return len(self.items)
8
def __getitem__(self, i):
9
return self.items[i]
10
def __add__(self, other):
11
return Crate(self.items + other.items)
12
13
c = Crate(["vase", "lamp"])
14
d = Crate(["cup"])
bool(c) pregunta a __len__ porque no hay __bool__. Si la longitud no es 0, es verdadero.for x in c se llama a __getitem__ lo veremos en 8-3.Pin dice
→ Los métodos dunder solo se definen; para usarlos, usa la sintaxis: len(c) en vez de c.__len__(). La sintaxis los busca en el tipo y, si algo falla, también prueba los otros caminos (pasar a __radd__, preguntar a __len__ en lugar de __bool__).
__repr__ y __str__ (7 min)Cuando Nam imprimía una caja para depurar, solo salía esto:
class Crate:
def __init__(self, items):
self.items = list(items)
print(Crate(["vase"]))
# p. ej.: <__main__.Crate object at 0x000001F4C2A8B2C0>El __repr__ por defecto heredado de object solo da el nombre del tipo y la dirección. No hay forma de saber qué hay dentro de la caja. Hay dos asas para convertir a texto:
| Para quién es el texto | Dónde se usa | |
|---|---|---|
__repr__ | Para el desarrollador. A ser posible, con una forma que permita recrearlo, como Crate([...]) | repr(), el intérprete interactivo, el depurador, los elementos de listas y dicts, el !r de los f-strings |
__str__ | Para el usuario. Una frase fácil de leer | print(), str(), el {x} de los f-strings |
class Crate:
def __init__(self, items):
self.items = list(items)
def __repr__(self):
return f"Crate({self.items})"
def __str__(self):
return f"crate of {len(self.items)}"
c = Crate(["vase", "lamp"])
print(c)
# crate of 2
print(repr(c))
# Crate(['vase', 'lamp'])
print([c])
# [Crate(['vase', 'lamp'])]
print(f"{c} / {c!r}")
# crate of 2 / Crate(['vase', 'lamp'])print([c]) no se usó __str__, sino __repr__. Al convertir una lista en texto, los elementos se escriben siempre con repr.__str__, str() y print() usan __repr__. Al revés, no. Por eso, si vas a poner solo una, empieza por __repr__.repr. Lo que ves cuando persigues un error es __repr__.Centralita dunder
1
class Crate:
2
def __init__(self, items):
3
self.items = list(items)
4
def __repr__(self):
5
return f"Crate({self.items})"
6
def __str__(self):
7
return f"crate of {len(self.items)}"
8
9
c = Crate(["vase", "lamp"])
Para que una caja se comporte como un contenedor bastan cinco asas: __len__, __getitem__, __iter__, __contains__ y __bool__. Pero en 8-1 el for funcionó aunque no había __iter__.
class OldCrate:
def __init__(self, items):
self.items = list(items)
def __getitem__(self, i):
print("getitem", i)
return self.items[i]
for x in OldCrate(["vase", "lamp"]):
pass
# getitem 0
# getitem 1
# getitem 2
print("lamp" in OldCrate(["vase", "lamp"]))
# getitem 0
# getitem 1
# TrueSi no hay __iter__, iter() crea un iterador que llama a __getitem__ con 0, 1, 2… por orden. Cuando salta un IndexError, lo interpreta como «fin» y se detiene sin hacer ruido. Por eso se imprime hasta getitem 2 y termina sin error. Si no hay __contains__, in saca así los elementos uno a uno y los compara con ==. Si lo encuentra, se detiene ahí.
Este camino es para compatibilidad con código antiguo. En las clases nuevas, pon las asas como es debido.
class Crate:
def __init__(self, items):
self.items = list(items)
def __len__(self):
return len(self.items)
def __iter__(self):
yield from self.items
def __contains__(self, x):
return x in self.items
c, e = Crate(["vase", "lamp"]), Crate([])
print(list(c), "lamp" in c, bool(c), bool(e))
# ['vase', 'lamp'] True True False
if not e:
print("empty crate")
# empty crate__iter__ tiene que devolver un iterador. Sirve return iter(self.items), y si usas yield como arriba, la función se convierte en un generador que crea el iterador por sí solo. Los generadores se tratan más a fondo en el curso de generadores y decoradores.__contains__, in no recorre nada: le pregunta directamente a ese método.__bool__, la verdad se decide según si __len__ es 0. Si no hay ninguno de los dos, el objeto es siempre verdadero. Por eso, con solo poner __len__, if not e: funciona con naturalidad también para un Crate vacío.Centralita dunder
1
class Crate:
2
def __init__(self, items):
3
self.items = list(items)
4
def __len__(self):
5
return len(self.items)
6
def __iter__(self):
7
yield from self.items
8
def __contains__(self, x):
9
return x in self.items
10
11
c = Crate(["vase", "lamp"])
12
e = Crate([])
__add__, __radd__, NotImplemented (8 min)a + b se resuelve por pasos:
type(a).__add__(a, b).NotImplemented, significa «no sé qué hacer con esta pareja». Python se lo pasa al otro lado: type(b).__radd__(b, a).NotImplemented, es un TypeError.class Crate:
def __init__(self, items):
self.items = list(items)
def __repr__(self):
return f"Crate({self.items})"
def __add__(self, other):
if not isinstance(other, Crate):
return NotImplemented
return Crate(self.items + other.items)
def __radd__(self, other):
if other == 0:
return self
return NotImplemented
c, d = Crate(["vase"]), Crate(["cup"])
print(c + d)
# Crate(['vase', 'cup'])
print(sum([c, d]))
# Crate(['vase', 'cup'])
c + 1
# TypeError: unsupported operand type(s) for +: 'Crate' and 'int'sum() empieza en 0 y calcula primero 0 + c. int.__add__(0, c) no sabe qué es una caja y devuelve NotImplemented, así que se llama a Crate.__radd__(c, 0). Como __radd__ acepta el 0, sum funciona.NotImplemented. Así el otro lado tiene su oportunidad. No es lo mismo que NotImplementedError (una excepción).Centralita dunder
1
class Crate:
2
def __init__(self, items):
3
self.items = list(items)
4
def __repr__(self):
5
return f"Crate({self.items})"
6
def __add__(self, other):
7
if not isinstance(other, Crate):
8
return NotImplemented
9
return Crate(self.items + other.items)
10
def __radd__(self, other):
11
if other == 0:
12
return self
13
return NotImplemented
14
15
c = Crate(["vase"])
16
d = Crate(["cup"])
Aquí se resuelven las dos caras de += de la lección 2. a += b busca primero __iadd__ (suma en el sitio) y, si no existe, lo convierte en a = a + b y mueve la etiqueta a un objeto nuevo. list tiene __iadd__; tuple y str, no.
c = Crate(["vase"])
alias = c
c += Crate(["cup"])
print(c is alias, alias)
# False Crate(['vase'])
class Crate2(Crate):
def __iadd__(self, other):
self.items += other.items
return self
c = Crate2(["vase"])
alias = c
c += Crate2(["cup"])
print(c is alias, alias)
# True Crate(['vase', 'cup'])Si existe __iadd__, modifica el contenido de la maleta y se devuelve a sí mismo. Python vuelve a colgar la etiqueta c del valor devuelto, pero como es la misma maleta, la etiqueta no se mueve. (class Crate2(Crate) es una especificación que hereda de Crate. Es el tema de la lección 9.)
Pin dice
→ Si en __iadd__ se te olvida el return self, después de c += d tendrás c igual a None. Python vuelve a colgar la etiqueta de lo que devuelve __iadd__.
__call__, with, @dataclass (7 min)Con __call__, un objeto se puede llamar como si fuera una función. Sirve cuando necesitas una función que lleve consigo valores de configuración (lo mismo que las clausuras de la lección 6, pero con una clase).
class Scale:
def __init__(self, limit):
self.limit = limit
def __call__(self, kg):
return kg <= self.limit
check = Scale(23)
print(check(20), check(30), callable(check))
# True False Truewith llama a __enter__ y __exit__. Aunque salte una excepción dentro del bloque, __exit__ se llama siempre. Por eso se usa para liberar recursos; lo volveremos a ver en la lección 11.
class Belt:
def __enter__(self):
print("belt on")
return self
def __exit__(self, exc_type, exc, tb):
print("belt off", exc_type)
return False
with Belt() as b:
print("moving")
# belt on
# moving
# belt off NoneSi __exit__ devuelve True, se traga la excepción. Casi siempre lo correcto es False (o no devolver nada).
Las asas que se escriben siempre de forma parecida, como __init__, __repr__ o __eq__, te las genera @dataclass.
from dataclasses import dataclass
@dataclass
class Tag:
code: str
kg: int = 0
t = Tag("ICN", 12)
print(t, t == Tag("ICN", 12))
# Tag(code='ICN', kg=12) True
print(Tag.__hash__)
# NoneFíjate en la última línea. Si defines __eq__, Python pone __hash__ a None. Con una clase escrita a mano pasa lo mismo.
class Crate:
def __init__(self, items):
self.items = list(items)
def __eq__(self, other):
return isinstance(other, Crate) and self.items == other.items
print(Crate(["vase"]) == Crate(["vase"]), Crate.__hash__)
# True None
{Crate(["vase"])}
# TypeError: cannot use 'Crate' as a set element (unhashable type: 'Crate')Si decides que «son iguales cuando el valor es igual» pero el hash sigue basándose en la identidad (la dirección), dos cajas iguales acabarían en casillas distintas de un set. Para no romper esa promesa, se desactiva el hash por completo. La promesa entre hash e igualdad la veremos en detalle en la lección 12.
Este es el Crate con todas las asas puestas.
class Crate:
def __init__(self, items=()):
self.items = list(items)
def __repr__(self):
return f"Crate({self.items})"
def __len__(self):
return len(self.items)
def __getitem__(self, i):
return self.items[i]
def __iter__(self):
return iter(self.items)
def __contains__(self, x):
return x in self.items
def __add__(self, other):
if not isinstance(other, Crate):
return NotImplemented
return Crate(self.items + other.items)
def __radd__(self, other):
return self if other == 0 else NotImplemented
a, b = Crate(["vase", "lamp"]), Crate(["frame"])
print(len(a), a[0], "lamp" in a, [x.upper() for x in a])
# 2 vase True ['VASE', 'LAMP']
print(a + b, sum([a, b]))
# Crate(['vase', 'lamp', 'frame']) Crate(['vase', 'lamp', 'frame'])
print(a[-1], a[:1])
# lamp ['vase']En a[:1], __getitem__ recibió un objeto slice y devolvió tal cual el corte de la lista. Si quieres que devuelva una caja, basta con separar el caso con isinstance(i, slice).
Las cajas de Nam pasan sin problemas por la cinta. La supervisora comenta: «La forma es la misma. Solo le hemos puesto asas.»
Informe de cierre — LOST-008. Crate no tenía asas (métodos dunder), así que la sintaxis no reconocía la caja. Resuelto añadiendo repr, len, getitem, iter, contains, add y radd.
__eq__ al Crate del caso cerrado y comprueba qué pasa con {Crate(["vase"])}. Explica el porqué en una frase.__bool__ devuelva siempre False y, con un __len__ de 3, predice a cuál de los dos hace caso if obj:. Luego compruébalo.__getitem__ para que devuelva un Crate cuando isinstance(i, slice), de modo que a[:1] sea Crate(['vase']).La libreta de la supervisora
La sintaxis llama a métodos dunder. len(x) es type(x).__len__(x). Se buscan en el tipo, no en la instancia.
Si pones solo uno, __repr__. print usa __repr__ si no hay __str__, y los elementos de una lista siempre van con repr.
for usa __iter__ y, si no existe, __getitem__ desde 0 (se detiene en IndexError). bool usa __bool__ y, si no existe, __len__.
Ante una pareja desconocida, devuelve NotImplemented. Así el __radd__ del otro lado tiene su oportunidad.
a += b modifica en el sitio si hay __iadd__ (return self); si no, a = a + b.
Si defines __eq__, __hash__ pasa a ser None. Con @dataclass, igual.
← Anterior
Lección 7 · La especificación de la maleta: clases e instancias
Siguiente →
Lección 9 · La especificación de la especificación: herencia y MRO
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.