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 7:30–8:20
Parte de equipaje perdido
LOST-010
Lo reporta
Oh, del servicio técnico del sistema de pesaje
Qué se perdió
El registro de la báscula. Aunque se añadió una comprobación de peso, en el registro figura una maleta de -10 kg.
Para cerrar el caso
Encontrar el camino que se salta la comprobación, y hacer que cualquier camino de entrada pase por el control.
Aeropuerto Internacional Haneulseom · Centro de equipajes
Para impedir pesos negativos, Oh hizo que Bag comprobara el peso al crearse. Pero en el registro hay una maleta de -10 kg. La supervisora Do Miran hojea el registro de accesos. «Pusimos un control en la puerta principal, y ha entrado por la puerta lateral.»
Hoy vemos el control por el que pasa Python en el momento de leer y escribir obj.x. Es también la imagen completa de la regla de lectura que en la lección 7 resumimos como «instancia → clase».
@property: poner un control en un atributo (9 min)Este es el código del parte. La comprobación solo está en __init__.
class Bag:
def __init__(self, kg):
if kg < 0:
raise ValueError("negative weight")
self.kg = kg
b = Bag(20)
b.kg -= 30
print(b.kg)
# -10Al crearse pasó la comprobación, pero el b.kg -= 30 de después escribe el valor directamente en el __dict__ de la instancia. Es la puerta lateral. Para poner la comprobación en la propia escritura del atributo, se usa @property.
class Bag:
def __init__(self, kg):
self.kg = kg
@property
def kg(self):
return self._kg
@kg.setter
def kg(self, value):
if value < 0:
raise ValueError(f"negative weight: {value}")
self._kg = value
b = Bag(20)
print(b.kg, vars(b))
# 20 {'_kg': 20}
b.kg -= 30
# ValueError: negative weight: -10b.kg se llama a la primera función kg, y al escribir b.kg = v, a la función @kg.setter. Desde fuera se usa como un atributo normal.self.kg = kg de __init__ también pasa por el setter. Basta con tener la comprobación en un solo sitio._kg. El nombre kg es un objeto property que está en la clase, no en la instancia.Si no le das setter, el atributo es de solo lectura. Es adecuado para valores que se calculan a partir de otros atributos.
class Box:
def __init__(self, w, h):
self.w, self.h = w, h
@property
def area(self):
return self.w * self.h
x = Box(3, 4)
print(x.area, type(Box.__dict__["area"]).__name__)
# 12 property
x.area = 5
# AttributeError: property 'area' of 'Box' object has no setterPin dice
→ Un guion bajo como en _kg es solo un acuerdo de «no lo toques», no un candado. No puedes impedir b._kg = -5. El control protege el camino normal (b.kg = …).
La maleta del parte no solo tiene peso (kg), sino también longitud (cm). Si copias una property para cada atributo, el código de comprobación se multiplica. Puedes crear un solo control y ponerlo en varios atributos. Eso es un descriptor.
Un descriptor es un objeto que tiene al menos uno de __get__ y __set__ (o __delete__). Si lo pones como atributo de clase, Python llama a esos métodos cuando se lee o escribe ese nombre desde una instancia.
class Positive:
def __set_name__(self, owner, name):
print(f"set_name: {owner.__name__}.{name}")
self.name = name
def __get__(self, obj, owner=None):
if obj is None:
return self
return obj.__dict__[self.name]
def __set__(self, obj, value):
if value < 0:
raise ValueError(f"{self.name} must be >= 0: {value}")
obj.__dict__[self.name] = value
class Bag:
kg = Positive()
cm = Positive()
def __init__(self, kg, cm):
self.kg, self.cm = kg, cm
# set_name: Bag.kg
# set_name: Bag.cm
b = Bag(20, 55)
print(b.kg, b.cm, vars(b))
# 20 55 {'kg': 20, 'cm': 55}
print(type(Bag.kg).__name__)
# Positive
b.cm = -1
# ValueError: cm must be >= 0: -1| Cuándo | Lo que llama Python |
|---|---|
| Al terminar de crear la clase | Positive.__set_name__(control, Bag, "kg") |
Al leer b.kg | Positive.__get__(control, b, Bag) |
Al leer Bag.kg | Positive.__get__(control, None, Bag) |
Al escribir b.kg = 20 | Positive.__set__(control, b, 20) |
__set_name__ (3.6) le dice al descriptor, en el momento en que termina la sentencia class, con qué nombre lo han puesto. Por eso el mismo Positive puede ponerse tanto en kg como en cm.__dict__ de cada maleta.__dict__ con el mismo nombre, "kg", al leerlo se pasa por el control y no por __dict__. Ese orden es el tema de 10-4.@property también es un descriptor que Python trae ya hecho.Tablero de etiquetas
Paso 1 / 0
Lo siguiente que se ejecuta: 0
Preparando el laboratorio…
Acaba de cambiar
Maleta sellada (inmutable)
En la lección 7 vimos que bag.m is bag.m es False. El motivo es que las funciones también son descriptores. Una función es un descriptor sin datos (non-data descriptor): solo tiene __get__, no __set__.
class Bag:
def weigh(self):
return 20
b = Bag()
f = Bag.__dict__["weigh"]
print(type(f).__name__, hasattr(f, "__get__"), hasattr(f, "__set__"))
# function True False
m = f.__get__(b, Bag)
print(type(m).__name__, m.__self__ is b, m())
# method True 20
print(b.weigh is b.weigh, b.weigh == b.weigh, Bag.weigh is f)
# False True TrueCada vez que se lee b.weigh, Python llama a f.__get__(b, Bag) y crea un método ligado nuevo: un objeto pequeño que une la función y b. Por eso is da False y == da True. También gracias a esa unión se puede llamar a b.weigh() sin pasar self. Si se lee desde la clase (Bag.weigh), no hay instancia que unir y sale la función tal cual.
staticmethod y classmethod son descriptores que cambian lo que une __get__.
class Bag:
@staticmethod
def kg_to_lb(kg):
return round(kg * 2.2046, 1)
@classmethod
def empty(cls):
return cls()
class Suitcase(Bag):
pass
print(Bag.kg_to_lb(20), Bag().kg_to_lb(20))
# 44.1 44.1
print(type(Suitcase.empty()).__name__, Suitcase.empty.__self__ is Suitcase)
# Suitcase True
print(type(Bag.__dict__["empty"]).__name__)
# classmethodLo que une __get__ | Primer argumento | |
|---|---|---|
| Función normal | La instancia | self |
staticmethod | Nada | Ninguno |
classmethod | La clase (type(obj)) | cls |
classmethod recibe la clase de quien lo llama, así que Suitcase.empty() crea un Suitcase. Se usa para «constructores alternativos» que, aunque se herede, crean objetos con la especificación correcta.
Al leer obj.x, Python (en concreto, object.__getattribute__) mira en este orden:
| Orden | Dónde mira | Ejemplo |
|---|---|---|
| 1 | Un descriptor de datos en la clase (MRO) (uno con __set__ o __delete__) | property, Positive |
| 2 | El __dict__ de la instancia | b.label = "ICN" |
| 3 | Un descriptor sin datos en la clase (solo __get__) | Funciones, staticmethod, classmethod |
| 4 | Un atributo normal de la clase | kind = "bag" |
| 5 | __getattr__ (si existe) | |
| 6 | AttributeError |
Como los descriptores de datos van antes que el __dict__ de la instancia, el control se llama antes que el valor guardado en la maleta. Por eso no pasaba nada cuando el Positive de 10-2 guardaba el valor en __dict__ con el mismo nombre. Al revés, las funciones van después de __dict__, así que si usas el mismo nombre en la instancia, el método queda tapado.
Elige los atributos uno a uno en el diagrama de abajo. Te muestra en qué paso sale la respuesta.
Flujo del control
1
class Positive:
2
def __set_name__(self, owner, name):
3
self.name = name
4
def __get__(self, obj, owner):
5
return obj.__dict__[self.name]
6
def __set__(self, obj, value):
7
obj.__dict__[self.name] = value
8
class Bag:
9
kg = Positive()
10
kind = "bag"
11
def weigh(self):
12
return self.kg
13
def __repr__(self):
14
return "Bag()"
15
b = Bag()
16
b.kg = 20
17
b.label = "ICN"
b.
¿Descriptor de datos en la clase (__get__ + __set__)?
¿En el __dict__ de la instancia?
¿Descriptor sin datos en la clase (solo __get__, como una función)?
¿Un atributo normal de la clase?
¿Hay __getattr__?
AttributeError
Resultado: b. →
Ahora vamos a meter a propósito los mismos nombres en el __dict__ de la instancia. La última línea es la «puerta lateral»: escribe directamente en __dict__ para esquivar el control.
Flujo del control
1
class Bag:
2
kind = "bag"
3
def weigh(self):
4
return 20
5
@property
6
def tag(self):
7
return "T-" + self.kind
8
def __getattr__(self, name):
9
return f"<no {name}>"
10
b = Bag()
11
b.weigh = "shadowed"
12
b.kind = "suitcase"
13
b.__dict__["tag"] = "fake"
b.
¿Descriptor de datos en la clase (__get__ + __set__)?
¿En el __dict__ de la instancia?
¿Descriptor sin datos en la clase (solo __get__, como una función)?
¿Un atributo normal de la clase?
¿Hay __getattr__?
AttributeError
Resultado: b. →
property sin setter también es un descriptor de datos. property siempre tiene __set__, y si no hay setter, lanza un AttributeError desde dentro. Por eso no se puede tapar con __dict__.b.x = v) es más corto. Si en la clase hay un descriptor con __set__, se llama a ese; si no, se escribe en el __dict__ de la instancia.Pin dice
→ Para memorizar el orden en una línea: «descriptor de datos > __dict__ de la instancia > descriptor sin datos, como las funciones > atributo de clase > __getattr__».
__getattr__, __getattribute__ y type (5 min)Estas dos asas de nombre parecido se llaman en momentos distintos.
__getattribute__: se llama siempre, en cada lectura de atributo. Todo el orden de 10-4 está dentro de object.__getattribute__. Si lo sobrescribes es fácil caer en una recursión infinita, así que casi nunca se usa.__getattr__: el último equipo de rescate, que solo se llama cuando no se ha encontrado nada siguiendo ese orden.class Loud:
def __init__(self):
self.kg = 20
def __getattribute__(self, name):
print("getattribute", name)
return super().__getattribute__(name)
def __getattr__(self, name):
print("getattr", name)
return None
x = Loud()
v = x.kg
# getattribute kg
v = x.color
# getattribute color
# getattr colorPor último: si las clases también son objetos, ¿cuál es el tipo de una clase? type. La sentencia class equivale, en realidad, a llamar a type(nombre, bases, dict_de_atributos).
class Bag:
kind = "bag"
print(type(Bag()).__name__, type(Bag), type(type))
# Bag <class 'type'> <class 'type'>
Dyn = type("Dyn", (Bag,), {"kind": "dyn"})
print(Dyn.kind, [c.__name__ for c in Dyn.__mro__])
# dyn ['Dyn', 'Bag', 'object']Heredar de type para cambiar el propio proceso de creación de clases se llama metaclase. Para casi todo bastan __init_subclass__ (lección 9) y los descriptores, así que en este curso quédate solo con el nombre.
Si trasladamos la comprobación al control Positive, tanto al crear, como al modificar, como al restar con -=, todo pasa por el mismo control.
class Positive:
def __set_name__(self, owner, name):
self.name = name
def __get__(self, obj, owner=None):
return self if obj is None else obj.__dict__[self.name]
def __set__(self, obj, value):
if value < 0:
raise ValueError(f"{self.name} must be >= 0: {value}")
obj.__dict__[self.name] = value
class Bag:
kg = Positive()
cm = Positive()
def __init__(self, kg, cm):
self.kg, self.cm = kg, cm
b = Bag(20, 55)
for change in (lambda: setattr(b, "kg", -1), lambda: Bag(-3, 40)):
try:
change()
except ValueError as e:
print("blocked:", e)
# blocked: kg must be >= 0: -1
# blocked: kg must be >= 0: -3
b.kg -= 30
# ValueError: kg must be >= 0: -10Con setattr, creando una maleta nueva o restando con -=, todo queda bloqueado. b.kg -= 30 son dos pasos, leer b.kg (__get__) y volver a escribir el resultado de la resta (__set__), así que no puede esquivar el control.
Oh vuelve a pasar el registro de la báscula. Ya no aparecen pesos negativos. La supervisora comenta: «Mejor que cerrar puertas es hacer que todos los caminos pasen por el control.»
Informe de cierre — LOST-010. La comprobación de peso solo estaba en init, así que el b.kg -= 30 posterior se la saltaba. Resuelto trasladando la comprobación a un descriptor de datos (Positive) para que toda escritura pase por el control.
Positive para que reciba un límite inferior (kg = AtLeast(0), cm = AtLeast(10)). Basta con usar en la comprobación el valor recibido en __init__.b.weigh = "x", ¿en qué paso sale la respuesta de b.weigh? ¿Y con b.kg = "x"?__getattr__, comprueba con print si se llama a __getattr__ al leer un atributo asignado en __init__.La libreta de la supervisora
Con @property se pone una comprobación al leer y escribir un atributo. La comprobación, en un solo sitio: el setter. Sin setter, solo lectura.
Un descriptor es un atributo de clase con __get__/__set__. Con __set_name__ sabe con qué nombre lo han puesto y se reutiliza en varios atributos.
Las funciones son descriptores sin datos que solo tienen __get__. Cada lectura de b.m crea un método ligado nuevo.
staticmethod no une nada; classmethod une la clase.
Orden de lectura: descriptor de datos > __dict__ de la instancia > descriptor sin datos > atributo de clase > __getattr__.
__getattribute__ siempre; __getattr__ solo cuando no se encuentra. La clase de una clase es type.
← Anterior
Lección 9 · La especificación de la especificación: herencia y MRO
Siguiente →
Lección 11 · Maletas sin dueño: memoria y GC
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.