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.
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.
Resumen
- Cada acceso a un atributo en Python (
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. - Por Qué Importa: Una vez que ves la búsqueda de atributos y el despacho dunder como los dos mecanismos compartidos, la herencia, las propiedades y la sobrecarga de operadores dejan de ser temas independientes y se convierten en aplicaciones predecibles de las mismas reglas.
- Conceptos Clave: clase como objeto,
__dict__de instancia, orden de resolución de métodos (MRO), protocolo de descriptor, despacho de métodos dunder. - Cuándo Usar: Para depurar por qué un atributo se resuelve a un valor inesperado, decidir entre herencia y composición, o entender por qué
@propertyy los métodos ligados se comportan como lo hacen. - Limitaciones / Compensaciones: La flexibilidad de la búsqueda dinámica de atributos y la sobrecarga de operadores tiene un costo en predictibilidad: dos objetos que parecen similares pueden comportarse de manera muy diferente dependiendo de qué métodos dunder implementen.
- Temas Relacionados: metaclasses, tipado estructural, el protocolo de descriptor, composición versus herencia.
Fundamentos
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.
Mecánicas e Interacciones
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.
Consideraciones Avanzadas y Aplicaciones
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.
Conceptos Erróneos Comunes
- "Las clases son solo planos, no objetos reales." Una clase es un objeto de primera clase (una instancia de
type), por eso puedes pasar una clase, almacenarla en una variable o adjuntarle atributos directamente. - "Cada llamada a un método busca la función fresca en la instancia." Los métodos residen en la clase, no en la instancia; acceder a ellos a través de una instancia activa el protocolo de descriptor para producir un método ligado, no copia la función a ningún sitio.
- "
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 deisinstance()pueden pasar puramente porque un objeto resulta tener los nombres de método correctos, sin ninguna clase base compartida en absoluto. - "
@dataclasses 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.
Preguntas Frecuentes
¿Es una clase de Python realmente un objeto en sí misma?
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.
¿Cuál es el orden exacto que Python usa para buscar `instancia.nombre`?
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.
¿Qué es un descriptor, en términos sencillos?
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.
¿Por qué `@property` tiene prioridad sobre la entrada de la propia instancia en `__dict__` con el mismo nombre?
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.
¿Cómo permiten los métodos dunder que mi clase use `+`, `len()` o `for`?
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.
¿`super().metodo()` llama siempre a la clase padre de la que heredé directamente?
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".
¿Cómo se diferencia `typing.Protocol` de heredar una clase base?
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.
¿Reemplaza `@dataclass` las mecánicas normales de clase?
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.
¿Qué te permite hacer realmente una metaclass?
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.
¿Por qué las jerarquías de herencia profundas tienden a volverse frágiles?
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.
¿Cuándo debería optar por la composición en lugar de la herencia?
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.
Relacionados
- Conceptos Básicos de OOP - clases prácticas,
selfy atributos de instancia - Métodos Dunder / Mágicos - los ganchos específicos en los que se basa el modelo de despacho de esta página
- Herencia y MRO - el orden de resolución de métodos en detalle
- Propiedades y Descriptores - el protocolo de descriptor que esta página introduce
- Protocolos y Tipado Estructural - comprobaciones estructurales contra este mismo modelo de búsqueda
- dataclasses - métodos dunder generados en clases ordinarias
Versiones 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.