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 8

Tiempo 5:50–6:40

Procedimientos estándar del aeropuerto: métodos dunder

Parte de equipaje perdido

LOST-008

La caja que la cinta no reconoce

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.

8-1. La sintaxis llama a métodos dunder (9 min)

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 iterable

len(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 99

Las 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.

SintaxisLo que se llama de verdad
len(x)type(x).__len__(x)
x[i], x[i] = v__getitem__, __setitem__
a + ba.__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"])

En c + d, __init__ termina primero porque __add__ crea una caja nueva. print(c) llama a __repr__ porque no hay __str__. for llama a __getitem__ con 0 y 1 porque no hay __iter__, y se detiene cuando el 2 da IndexError (el raise IndexError de la última línea de la lista).
  • bool(c) pregunta a __len__ porque no hay __bool__. Si la longitud no es 0, es verdadero.
  • Por qué en 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__).

8-2. __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 textoDó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 leerprint(), 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'])
  • En print([c]) no se usó __str__, sino __repr__. Al convertir una lista en texto, los elementos se escriben siempre con repr.
  • Si no hay __str__, str() y print() usan __repr__. Al revés, no. Por eso, si vas a poner solo una, empieza por __repr__.
  • La ventana de variables del depurador, las listas impresas en los logs y los valores dentro de los mensajes de error son todos 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"])

print(c) y f'{c}' llaman a __str__; repr(c) y f'{c!r}' llaman a __repr__. print([c]) llama a __repr__ porque c es un elemento dentro de una lista.

8-3. El protocolo de contenedor (9 min)

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
# True

Si 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.
  • Si existe __contains__, in no recorre nada: le pregunta directamente a ese método.
  • Si no hay __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([])

in va directo a __contains__, y for y max, directo a __iter__. list(c) también pregunta a __len__ para reservar espacio de antemano (CPython). bool(e) llama a __len__ porque no hay __bool__.

8-4. Operadores: __add__, __radd__, NotImplemented (8 min)

a + b se resuelve por pasos:

  1. Se llama a type(a).__add__(a, b).
  2. Si el resultado es NotImplemented, significa «no sé qué hacer con esta pareja». Python se lo pasa al otro lado: type(b).__radd__(b, a).
  3. Si los dos dan 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.
  • Ante una pareja desconocida, no lances una excepción: devuelve 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"])

En 0 + c, int no sabe qué es una caja y se pasa a Crate.__radd__. En c + 1, __add__ devuelve NotImplemented y int tampoco sabe qué hacer, así que salta TypeError. En c += d no hay __iadd__, así que __add__ crea una caja nueva.

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__.

8-5. __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 True

with 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 None

Si __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__)
# None

Fí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.

Caso cerrado (10 min)

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.

Ejercicios

  1. Añade __eq__ al Crate del caso cerrado y comprueba qué pasa con {Crate(["vase"])}. Explica el porqué en una frase.
  2. Crea una clase cuyo __bool__ devuelva siempre False y, con un __len__ de 3, predice a cuál de los dos hace caso if obj:. Luego compruébalo.
  3. Modifica __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.

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