El Modelo de Objetos de Python
Las clases, los métodos dunder, la herencia, las dataclasses y los protocolos parecen características separadas, pero en realidad son diferentes puntos de entrada a un único modelo de objetos.
Busca en todas las páginas de la documentación
Las clases, los métodos dunder, la herencia, las dataclasses y los protocolos parecen características separadas, pero en realidad son diferentes puntos de entrada a un único modelo de objetos.
Ese modelo tiene dos partes móviles: cómo Python busca un atributo en un objeto, y cómo los métodos dunder especiales permiten que la sintaxis ordinaria (+, len(), for, with) opere en objetos que tú defines.
Esta página explica ambos, para que Métodos Dunder / Mágicos, Herencia y MRO, Propiedades y Descriptores, y Protocolos y Tipado Estructural se lean como variaciones de un tema en lugar de una pila de sintaxis no relacionadas para memorizar.
obj.nombre) sigue un único algoritmo de búsqueda: primero el __dict__ de la instancia, luego el orden de resolución de métodos (MRO) de la clase, y los métodos dunder son la forma en que los objetos se enganchan a la sintaxis incorporada.__dict__ de instancia, orden de resolución de métodos (MRO), protocolo de descriptor, despacho de métodos dunder.@property y los métodos ligados se comportan como lo hacen.En Python, una clase es en sí misma un objeto - específicamente, una instancia de type (o una metaclass personalizada) - por eso puedes adjuntar atributos directamente al cuerpo de una clase, y por qué type(MiClase) devuelve type mientras que type(instancia) devuelve MiClase.
Crear una instancia (MiClase()) construye un nuevo objeto con su propio __dict__ (su espacio de nombres personal para atributos de instancia), más un enlace oculto de vuelta a la clase que lo creó.
Acceder a instancia.nombre activa una búsqueda: Python primero comprueba el __dict__ de la propia instancia, y si el nombre no está allí, recorre el orden de resolución de métodos (MRO) de la clase - la clase misma, luego sus bases, en un orden bien definido - buscando el nombre en el __dict__ de cada clase.
Una analogía simple: el __dict__ de la instancia es una libreta de notas adhesivas pegada a un objeto específico, mientras que la clase (y sus bases) es un manual de referencia compartido que todos de ese tipo consultan cuando sus propias notas adhesivas no tienen la respuesta.
Los métodos funcionan a través de esta misma búsqueda: def saludar(self) dentro del cuerpo de una clase es solo un objeto función almacenado en el __dict__ de la clase, y acceder a él a través de una instancia es lo que lo convierte en un "método ligado" que automáticamente suministra self.
El mecanismo que convierte una función simple en un método ligado - y que potencia @property - es el protocolo de descriptor: cualquier objeto con __get__ (y opcionalmente __set__/__delete__) definido en su clase puede interceptar el acceso a atributos en lugar de ser devuelto tal cual.
Los descriptores que definen __set__ se llaman descriptores de datos, y tienen prioridad sobre la entrada de la propia instancia en su __dict__ con el mismo nombre - esa es la razón exacta por la que @property puede forzar validación en cada asignación aunque la instancia técnicamente "tenga" ese atributo.
class Celsius:
def __get__(self, obj, owner):
return obj._celsius
def __set__(self, obj, value):
if value < -273.15:
raise ValueError("por debajo del cero absoluto")
obj._celsius = value
class Weather:
temperature = Celsius() # un descriptor de datos tiene prioridad sobre la búsqueda del __dict__ de la instanciaLos métodos dunder son la segunda mitad del modelo: la sintaxis de Python nunca trata tus clases directamente de forma especial - len(obj) llama a obj.__len__(), obj + otro llama a obj.__add__(otro), for x in obj llama a obj.__iter__(), y with obj: llama a obj.__enter__()/obj.__exit__().
Es por eso que implementar un puñado de métodos dunder permite que una clase personalizada participe plenamente en la sintaxis de Python - el lenguaje fue diseñado en torno a un sistema de "ganchos" pequeño y consistente en lugar de un gran conjunto de casos especiales incorporados.
super() y el MRO interactúan con esta misma búsqueda: super().metodo() no llama "a la clase padre" ingenuamente, llama a la siguiente clase después de la actual en el MRO, que es lo que hace que la herencia múltiple cooperativa (mixins) funcione correctamente en lugar de saltarse o llamar dos veces a bases compartidas.
dataclasses y typing.Protocol parecen alternativas a la OOP "real", pero ambos operan completamente dentro de este mismo modelo en lugar de reemplazarlo: @dataclass es un decorador de clase que genera los métodos dunder __init__, __repr__ y __eq__ por ti en el momento de la creación de la clase, ahorrando el código repetitivo de escribirlos a mano.
typing.Protocol invierte la dirección del mismo modelo de búsqueda: en lugar de requerir que el MRO de un objeto incluya una clase base específica, un Protocol comprueba (estáticamente, o en tiempo de ejecución con @runtime_checkable) si la clase de un objeto resulta definir los métodos correctos - el mismo mecanismo de búsqueda de atributos, solo que comprobado estructuralmente en lugar de por la cadena de herencia.
Las metaclasses (type subclasses, o __init_subclass__ como la alternativa de menor peso) se sitúan un nivel más arriba: dado que las clases son objetos creados por type, una metaclass personalizada puede interceptar la creación de la clase misma, que es cómo los frameworks como los ORM registran automáticamente las clases modelo o validan los cuerpos de clase en el momento de la definición.
| Enfoque | Fortaleza | Debilidad | Mejor ajuste |
|---|---|---|---|
| Herencia (clase base) | Comparte implementación y fuerza una relación "es-un" real | Las jerarquías profundas se vuelven rígidas y difíciles de cambiar de forma segura | Una relación de especialización genuina con comportamiento compartido |
| Composición (tiene-un) | Flexible, intercambiable en tiempo de ejecución, evita jerarquías frágiles | Más código repetitivo para delegar llamadas explícitamente | Combinar comportamientos independientes sin forzar una jerarquía de tipos |
typing.Protocol (estructural) | No requiere herencia; funciona con tipos de terceros que no puedes modificar | Orientado al verificador estático; las comprobaciones en tiempo de ejecución solo cubren la existencia del método, no el comportamiento | Definir una interfaz para código que no posees, o acoplamiento mínimo |
@dataclass | Genera métodos dunder repetitivos automáticamente | Sigue siendo una clase normal por debajo - no reemplaza las decisiones de herencia | Registros centrados en datos sin __init__/__eq__ escritos a mano |
El modo de fallo práctico que este modelo explica son los problemas de clase base frágil: debido a que la búsqueda de atributos recorre un MRO en vivo en el momento de la llamada en lugar de enlazarse estáticamente, cambiar un método de una clase base puede cambiar silenciosamente el comportamiento en todas las subclases que dependían de la versión anterior, lo cual es un argumento central para favorecer la composición o los mixins pequeños y enfocados sobre los árboles de herencia profundos.
type), por eso puedes pasar una clase, almacenarla en una variable o adjuntarle atributos directamente.super().metodo() llama a la clase padre inmediata." Llama a la siguiente clase en el MRO después de la actual, que en herencia múltiple no siempre es lo mismo que "el padre" en un sentido simple.isinstance() siempre significa herencia real." Con Protocolos @runtime_checkable, las comprobaciones de isinstance() pueden pasar puramente porque un objeto resulta tener los nombres de método correctos, sin ninguna clase base compartida en absoluto.@dataclass es un tipo diferente de clase." Es una clase ordinaria con métodos dunder generados; todo sobre la búsqueda de atributos y el MRO descrito aquí todavía se aplica a ella sin cambios.Sí - una clase es una instancia de type (o una metaclass personalizada), por eso las clases pueden tener sus propios atributos y ser pasadas como cualquier otro valor.
Comprueba primero el __dict__ de la propia instancia, luego recorre el orden de resolución de métodos de la clase (la clase misma, luego sus bases en orden MRO), buscando en el __dict__ de cada una en turno - a menos que un descriptor de datos intercepte la búsqueda primero.
Un objeto cuya clase define __get__ (y opcionalmente __set__/__delete__) para que acceder a él como un atributo de clase ejecute ese código en lugar de devolverlo directamente - es el mecanismo detrás de los métodos ligados, @property y @staticmethod.
Porque property es un descriptor de datos (define __set__), y los descriptores de datos tienen prioridad sobre las entradas del __dict__ de la instancia en el orden de búsqueda de atributos, que es exactamente lo que permite que una propiedad valide o transforme valores en cada asignación.
La sintaxis de Python no trata los tipos incorporados de forma especial - a + b llama a a.__add__(b), len(obj) llama a obj.__len__(), y for x in obj llama a obj.__iter__() - por lo que implementar los métodos dunder correctos hace que cualquier clase personalizada participe en esa sintaxis.
No - llama a la siguiente clase en el MRO después de la actual, que en herencia múltiple puede ser un mixin hermano en lugar de lo que informalmente llamarías "el padre".
Una clase base requiere una relación explícita "es-un" declarada en la definición de la clase; un Protocol en cambio comprueba estructuralmente si la clase de un objeto resulta definir los nombres de método correctos, sin requerir ninguna relación de herencia.
No - es un decorador que genera métodos dunder comunes (__init__, __repr__, __eq__, y más) en el momento de la creación de la clase; la clase resultante todavía sigue las mismas reglas exactas de búsqueda de atributos y MRO que cualquier otra clase.
Dado que las clases son objetos creados por type, una metaclass personalizada puede interceptar la creación de la clase - validando un cuerpo de clase, registrando automáticamente la clase o inyectando atributos - antes de que exista cualquier instancia de ella.
Debido a que la búsqueda de atributos recorre un MRO en vivo en el momento de la llamada en lugar de resolverse estáticamente, cambiar un método en una clase base puede cambiar silenciosamente el comportamiento en todas las subclases que dependen de él, lo cual se vuelve más difícil de razonar a medida que la jerarquía se profundiza.
Cuando quieras combinar comportamientos independientes sin comprometerte con una jerarquía rígida de "es-un" - la composición delega explícitamente a otros objetos, lo cual es más verboso pero mucho más fácil de cambiar de forma segura que una cadena de herencia profunda o múltiple.
self y atributos de instanciaVersiones de Stack: Esta página fue escrita para Python 3.14 (estable) y Python 3.13 (mantenimiento); el protocolo de descriptor, el MRO y el despacho dunder descritos aquí han sido comportamiento estable de CPython en lanzamientos recientes de la serie 3.x.
Revisado por Chris St. John·Última actualización: 15 jul 2026