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 7

Tiempo 5:00–5:50

La especificación de la maleta: clases e instancias

Parte de equipaje perdido

LOST-007

Una pegatina puesta una vez que apareció en todas

Lo reporta

Moon, responsable del transporte de la gira de una orquesta

Qué se perdió

Puso una pegatina de «frágil» en un estuche de instrumento, y esa pegatina aparece en todos los estuches hechos con la misma especificación.

Para cerrar el caso

Encontrar dónde está realmente la lista de pegatinas, y hacer que cada estuche tenga su propia lista.

Aeropuerto Internacional Haneulseom · Centro de equipajes

En el cajón del escritorio de la supervisora Do Miran hay un fajo de especificaciones de maletas. Con una sola hoja de especificación se pueden fabricar tantas maletas iguales como quieras. «La especificación es una hoja; las maletas, una a una. Las dos son cosas.»

Moon dice que solo puso «frágil» en el estuche del violín, pero que la misma pegatina se ve también en el del violonchelo. La supervisora no abre los estuches, sino la especificación. La pegatina estaba pegada ahí.

7-1. La especificación también es una maleta: el objeto clase (7 min)

La sentencia class es una asignación que crea un objeto clase y lo ata a un nombre (igual que el def de la lección 1). Una clase también es un objeto con su tipo, y ese tipo es type.

class Case:
    size = "M"
    def __init__(self, owner):
        self.owner = owner

print(type(Case), isinstance(Case, type))
# <class 'type'> True
b = Case("cello")
print(type(b).__name__, type(b) is Case)
# Case True
print(vars(b))
# {'owner': 'cello'}
print([k for k in vars(Case) if not k.startswith("__")])
# ['size']
print(b.size, "size" in vars(b))
# M False
Qué guardaCómo verlo
Clase (especificación)Los nombres creados en el cuerpo de la clase: atributos de clase, métodosvars(Case), Case.__dict__
Instancia (maleta)Los atributos propios de esa maleta. Normalmente con self.x = ... en __init__vars(b), b.__dict__
  • En el __dict__ de b solo está owner. Aun así, b.size se lee como "M".
  • Al leer un atributo, primero se mira el __dict__ de la instancia y, si no está, la clase. Si no está en la maleta, se consulta la especificación.
  • El __dict__ de la clase es una ventana de solo lectura llamada mappingproxy. Para cambiar un atributo de clase, asígnalo directamente en la clase, como Case.size = "L".

7-2. Atributos de clase que comparten todos (10 min)

Esta es la especificación del parte:

class Case:
    stickers = []
    def __init__(self, owner):
        self.owner = owner

a = Case("violin")
b = Case("cello")
a.stickers.append("FRAGILE")
print(b.stickers, Case.stickers)
# ['FRAGILE'] ['FRAGILE']
print(a.stickers is b.stickers, "stickers" in vars(a))
# True False
  • stickers = [] se ejecuta una sola vez en el cuerpo de la clase. Solo hay una lista, pegada a la especificación.
  • a.stickers no está en el __dict__ de a, así que se busca en la clase, y append modifica esa única lista (cambiar la maleta).
  • Es un error con la misma forma que el de los argumentos por defecto de la lección 4: «un objeto mutable creado una vez y compartido por muchos».

Lo que cada instancia debe tener por separado se le pone a self en __init__. Compara abajo las tres versiones.

Tablero de etiquetas

Paso 1 / 0

Lo siguiente que se ejecuta: 0

Preparando el laboratorio…

Acaba de cambiar

Maleta sellada (inmutable)

En la primera versión, la lista solo está en la caja class Case, y las dos cajas de instancia solo tienen owner. Con __init__, cada instancia tiene su propia lista; en la última versión, solo a tiene lista propia.

Como en la tercera versión, si asignas a la instancia un atributo con el mismo nombre, el atributo de clase se queda como estaba y aparece un nombre nuevo en el __dict__ de la instancia. En esa instancia, el atributo de clase queda tapado (con la misma forma que tapar nombres integrados en la lección 5).

class Case:
    size = "M"

a = Case()
b = Case()
a.size = "L"
print(a.size, b.size, vars(a))
# L M {'size': 'L'}
Case.size = "S"
print(a.size, b.size)
# L S
del a.size
print(a.size)
# S
  • Si cambias el atributo de clase, el cambio se ve al instante en todas las instancias que no lo tapan (b).
  • Si quitas el nombre de la instancia con del a.size, vuelve a verse el valor de la clase.

No olvides que += también es una asignación. Si usas += con un atributo de clase numérico, aparece un nombre nuevo en la instancia.

class Gate:
    count = 0

g = Gate()
g.count += 1
print(g.count, Gate.count, vars(g))
# 1 0 {'count': 1}

g.count += 1 es g.count = g.count + 1. El lado derecho lee 0 de la clase y el izquierdo pone 1 en el __dict__ de g. Si es un número que cuentan todas las instancias juntas, hay que asignarlo directamente en la clase, como en Gate.count += 1.

Pin dice

→ Si pones una lista o un diccionario en el cuerpo de la clase, lo comparten todas las instancias. Deja en la clase solo lo que de verdad se comparte, como constantes o valores por defecto, y lo que cambia de maleta a maleta pónselo a self en __init__.

7-3. El orden de lectura: primero la instancia, luego la clase (7 min)

Veamos en el «Flujo del control» el camino que recorre Python al leer b.x. En esta lección solo hace falta fijarse en el paso 2 (el __dict__ de la instancia) y el paso 4 (un atributo normal de la clase). Los pasos 1, 3 y 5 se ven en la lección 10.

El b de abajo tiene size tanto en la clase como en su propio __dict__.

Flujo del control

1

class Case:

2

size = "M"

3

stickers = []

4

def __init__(self, owner):

5

self.owner = owner

6

def label(self):

7

return "case of " + self.owner

8

9

b = Case("cello")

10

b.size = "L"

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

owner y size se encuentran en el paso 2, el __dict__ de la instancia; stickers, en el paso 4, la clase. label es una función del paso 3, y weight no está en ninguna parte: AttributeError.
  • size también está en la clase, pero se encuentra antes en el paso 2, así que vale 'L'. El Case.size entre paréntesis en la fila del resultado indica que «la clase también tiene un nombre igual».
  • label es una función que está en la clase. Las funciones son objetos especiales que se detienen en el paso 3: al sacarlas no se obtiene una función sin más, sino un bound method. Es el tema del siguiente apartado.

7-4. Métodos ligados: se crean de nuevo cada vez que se sacan (6 min)

Un método es una función normal que está en la clase. Cuando lo sacas a través de una instancia, Python crea un objeto nuevo que une la función y la instancia: un método ligado (bound method).

class Bag:
    def __init__(self, owner):
        self.owner = owner
    def label(self):
        return "bag of " + self.owner

bag = Bag("kim")
print(bag.label is bag.label, bag.label == bag.label)
# False True
print(bag.label.__self__ is bag, bag.label.__func__ is Bag.label)
# True True
print(bag.label(), Bag.label(bag))
# bag of kim bag of kim
print(type(bag.label).__name__, type(Bag.label).__name__)
# method function
  • Cada vez que escribes bag.label se crea un método ligado nuevo. Por eso is da False. Como une la misma función y la misma instancia, == da True.
  • __self__ es la instancia ligada y __func__, la función original de la clase.
  • bag.label() es lo mismo que Bag.label(bag). self no es más que el primer argumento, que el método ligado rellena por ti.
  • Cómo una función se convierte en método al sacarla lo desvelaremos en la lección 10 (descriptores).

Tablero de etiquetas

Paso 1 / 0

Lo siguiente que se ejecuta: 0

Preparando el laboratorio…

Acaba de cambiar

Maleta sellada (inmutable)

m1 y m2 son cajas bound method distintas, pero su __self__ apunta a la misma instancia y su __func__, a la misma función que hay en class Bag.

7-5. Manejar nombres como cadenas: vars, getattr, setattr, hasattr (5 min)

A veces el nombre de un atributo hay que decidirlo como cadena durante la ejecución: las claves de un archivo de configuración, el nombre de una columna elegida en pantalla…

class Bag:
    def __init__(self, owner):
        self.owner = owner

bag = Bag("kim")
setattr(bag, "color", "red")
print(getattr(bag, "color"), hasattr(bag, "weight"), getattr(bag, "weight", 0))
# red False 0
print(vars(bag))
# {'owner': 'kim', 'color': 'red'}
vars(bag)["tag"] = "T1"
print(bag.tag, vars(bag) is bag.__dict__)
# T1 True
bag.weight
# AttributeError: 'Bag' object has no attribute 'weight'
  • setattr(obj, "x", v) equivale a obj.x = v, y getattr(obj, "x"), a obj.x. El tercer argumento de getattr es el valor por defecto que se devuelve si el atributo no existe.
  • hasattr decide intentando de verdad un getattr y mirando si salta AttributeError.
  • El vars() de una instancia no es una instantánea, sino el __dict__ de verdad. No es como el locals() dentro de una función de la lección 5.
  • A un objeto de Python se le pueden añadir atributos nuevos en cualquier momento. Es cómodo, pero una errata (bag.colour = ...) también se convierte en silencio en un atributo nuevo.

7-6. Una especificación más rígida: __slots__ y otras novedades (5 min)

Si escribes los nombres de los atributos en __slots__, las instancias no tienen __dict__ y solo pueden usar esos nombres. En CPython, como cada instancia no lleva diccionario, también consume menos memoria.

class Slim:
    __slots__ = ("owner",)
    def __init__(self, owner):
        self.owner = owner

s = Slim("kim")
print(s.owner, hasattr(s, "__dict__"))
# kim False
s.color = "red"
# AttributeError: 'Slim' object has no attribute 'color' and no __dict__ for setting new attributes

Desde 3.13, las clases tienen __static_attributes__. Reúne, ordenados, los nombres a los que los métodos de la clase asignan un valor con self.nombre = ....

class Trunk:
    def __init__(self, owner):
        self.owner = owner
        self.tags = []
    def pack(self):
        self.weight = 10

print(Trunk.__static_attributes__)
# ('owner', 'tags', 'weight')

Por último, la evaluación diferida de anotaciones de 3.14 (PEP 649 y 749). Las anotaciones del cuerpo de una clase no se evalúan al crear la clase, sino la primera vez que se lee __annotations__. Por eso la clase se crea aunque la anotación use un nombre que todavía no está definido. Hasta 3.13, la línea friend: Missing lanzaba un NameError.

class Partner:
    friend: Missing

print("created", Partner.__name__)
# created Partner

class Missing:
    pass

print(Partner.__annotations__)
# {'friend': <class '__main__.Missing'>}

Caso cerrado (10 min)

Quitamos la lista de pegatinas de la especificación y le damos una a cada estuche. Lo que sí se comparte (el tamaño de la especificación) se queda en la clase.

class Case:
    size = "M"
    def __init__(self, owner):
        self.owner = owner
        self.stickers = []

a = Case("violin")
b = Case("cello")
a.stickers.append("FRAGILE")
print(a.stickers, b.stickers)
# ['FRAGILE'] []
print(vars(a))
# {'owner': 'violin', 'stickers': ['FRAGILE']}
print(Case.__static_attributes__)
# ('owner', 'stickers')

Ahora «frágil» solo aparece en el estuche del violín. Cuando Moon se marcha, Pin muestra en pantalla el registro de las maletas fantasma de la cinta 13. Las dos maletas se fabricaron con la misma especificación, y el atributo partner de una apunta a la otra, y el partner de la otra apunta de nuevo a la primera. La supervisora rodea esa línea con el bolígrafo.

Informe de cierre — LOST-007. La lista de pegatinas era un atributo de clase, así que todos los estuches compartían la misma lista. Resuelto creando una lista nueva para cada estuche con self.stickers = [] en __init__.

Ejercicios

  1. Dejando Case.stickers = [] en la clase, si ejecutas a.stickers += ["FRAGILE"], ¿cómo quedan b.stickers y vars(a)? (Pista: ocurren a la vez el += de la lección 2 y el += de 7-2.)
  2. Si en una clase con __slots__ = ("owner",) pones el atributo de clase size = "M", ¿se puede leer s.size desde una instancia? ¿Y s.size = "L"?
  3. Escribe un bucle que saque con getattr, por orden, los tres atributos ["owner", "color", "weight"] y los imprima, mostrando "-" para los que no existan.

La libreta de la supervisora

  • Las clases también son objetos. type(Case) es type. La sentencia class también es una asignación que ata un nombre.

  • Cada instancia tiene su propio __dict__. Se lee en el orden instancia → clase.

  • Los atributos de clase los comparten todas las instancias. Si pones un objeto mutable, el cambio en un sitio se ve en todos.

  • Asignar a la instancia un nombre igual tapa el atributo de clase. += también es una asignación.

  • bag.m da un método ligado nuevo cada vez que se saca (is da False, == da True). __self__, __func__.

  • getattr / setattr / hasattr manejan nombres como cadenas. vars(obj) es el __dict__ de verdad.

  • __slots__ impide atributos nuevos. __static_attributes__ (3.13), evaluación diferida de anotaciones (3.14).

← Anterior

Lección 6 · Equipaje de mano: clausuras

Siguiente →

Lección 8 · Procedimientos estándar del aeropuerto: métodos dunder

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