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 10

Tiempo 7:30–8:20

El control: el secreto del acceso a atributos

Parte de equipaje perdido

LOST-010

Una maleta de peso negativo

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

10-1. @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)
# -10

Al 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: -10
  • Al leer b.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.
  • El self.kg = kg de __init__ también pasa por el setter. Basta con tener la comprobación en un solo sitio.
  • El valor real está en _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 setter

Pin 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 = …).

10-2. Descriptores: el control, en la especificación (10 min)

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ándoLo que llama Python
Al terminar de crear la clasePositive.__set_name__(control, Bag, "kg")
Al leer b.kgPositive.__get__(control, b, Bag)
Al leer Bag.kgPositive.__get__(control, None, Bag)
Al escribir b.kg = 20Positive.__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.
  • El objeto control es uno solo en la clase y lo comparten todas las maletas. Por eso el valor no se apunta en el control, sino en el __dict__ de cada maleta.
  • Aunque el valor se guardó en __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 caja de la clase Bag, kg y cm son controles Positive, cada uno con su propio name. En la última línea, el valor de kg entra en el __dict__ de la maleta, y cm = -1 se detiene en el control.

10-3. Cómo una función se convierte en método (7 min)

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 True

Cada 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__)
# classmethod
Lo que une __get__Primer argumento
Función normalLa instanciaself
staticmethodNadaNinguno
classmethodLa 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.

10-4. El orden de paso por el control (9 min)

Al leer obj.x, Python (en concreto, object.__getattribute__) mira en este orden:

OrdenDónde miraEjemplo
1Un descriptor de datos en la clase (MRO) (uno con __set__ o __delete__)property, Positive
2El __dict__ de la instanciab.label = "ICN"
3Un descriptor sin datos en la clase (solo __get__)Funciones, staticmethod, classmethod
4Un atributo normal de la clasekind = "bag"
5__getattr__ (si existe)
6AttributeError

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.

1

¿Descriptor de datos en la clase (__get__ + __set__)?

2

¿En el __dict__ de la instancia?

3

¿Descriptor sin datos en la clase (solo __get__, como una función)?

4

¿Un atributo normal de la clase?

5

¿Hay __getattr__?

6

AttributeError

Resultado: b. →

kg también está en __dict__, pero la respuesta sale antes, en el paso 1 (descriptor de datos). label sale en el paso 2; weigh, en el paso 3 (función); kind, en el paso 4; y color no está en ninguna parte: AttributeError.

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.

1

¿Descriptor de datos en la clase (__get__ + __set__)?

2

¿En el __dict__ de la instancia?

3

¿Descriptor sin datos en la clase (solo __get__, como una función)?

4

¿Un atributo normal de la clase?

5

¿Hay __getattr__?

6

AttributeError

Resultado: b. →

Aunque en __dict__ esté 'fake', en tag gana la property (paso 1). En weigh y kind, __dict__ (paso 2) tapa la función y el atributo de clase. color no está en ninguna parte, así que responde __getattr__ (paso 5).
  • Una 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__.
  • El orden para escribir (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__».

10-5. __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 color

Por ú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.

Caso cerrado (10 min)

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: -10

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

Ejercicios

  1. Modifica 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__.
  2. Si en el código del primer diagrama de 10-4 añades b.weigh = "x", ¿en qué paso sale la respuesta de b.weigh? ¿Y con b.kg = "x"?
  3. En una clase que solo define __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.

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