Skip to content
This page has been auto-translated and may contain errors.View in English

Clases y objetos

docs.scrimba.com

Cada tipo con el que has trabajado hasta ahora (strings, listas, diccionarios) es en realidad una clase. Cuando llamas a "hola".upper(), estás llamando a un método en un objeto string. Las clases te permiten definir tus propios tipos con sus propios datos y comportamiento. Una clase Jugador puede contener un nombre, una puntuación y un nivel, y saber cómo mostrarse a sí misma.

Las clases son el mecanismo para tipos definidos por el usuario. Una clase define una plantilla: los datos que cada instancia contiene (sus atributos, los valores almacenados en ella) y las operaciones que soporta (sus métodos, las funciones asociadas a ella). En lugar de rastrear valores en variables paralelas y pasarlos en todas partes, los agrupar en un objeto con una interfaz clara.

Una declaración class construye un nuevo tipo (una clase personalizada de valor, lo mismo que str e int) y lo vincula a un nombre, como cualquier otra asignación. Haces una instancia (un objeto concreto de ese tipo) llamando a la clase: Jugador("María"). Detrás de esa llamada, Python asigna un objeto en blanco y ejecuta __init__ para llenarlo. El modelo es uniforme en todos los niveles: una clase es en sí misma un objeto que puedes almacenar en una variable, pasar a una función o inspeccionar en tiempo de ejecución (mientras el programa se está ejecutando), lo que hace posibles los constructores alternativos y los registros más adelante en este capítulo.

Plantilla e instancias

Una clase es una plantilla. Una instancia es algo específico hecho a partir de esa plantilla. Puedes hacer tantas instancias como necesites, cada una con sus propios datos pero compartiendo los mismos métodos definidos en la clase.

Una clase define estructura y comportamiento. Las instancias son objetos creados a partir de esa clase: cada una lleva sus propios datos pero comparte los objetos método de la clase. Crear una instancia llama a la clase como una función: Perro() crea una nueva instancia de Perro.

Llamar a una clase hace dos cosas en orden: asigna una instancia en blanco y luego ejecuta __init__ en ella para llenarla con datos. El objeto terminado recuerda su clase (puedes leerla como type(rex)), y ese vínculo es cómo Python encuentra un método cuando llamas a rex.ladrar(): primero busca en la instancia, luego en la clase. La conclusión práctica es que los métodos viven en un solo lugar (en la clase, compartidos) mientras que los datos viven por instancia, así que mil objetos Perro te cuestan mil conjuntos de datos pero solo una copia de cada método.

python
class Perro:
    def ladrar(self):
        print("¡Guau!")

rex = Perro()
luna = Perro()

rex.ladrar()    # "¡Guau!"
luna.ladrar()   # "¡Guau!"

Perro es la clase. rex y luna son instancias: dos perros diferentes, cada uno compartiendo el mismo comportamiento definido en la clase.

JunoPlantilla e instancias Una clase es la plantilla, una instancia es una cosa construida a partir de ella. Llama a la clase como una función, Perro(), y obtienes una instancia nueva. Cada instancia comparte los métodos de la clase pero mantiene sus propios datos, así que rex y luna pueden comportarse igual siendo perros separados.
JunoPlantilla e instancias Llamar a Perro() crea una instancia: sus propios datos, los métodos compartidos de la clase. Ese empaquetado es el punto completo, valores relacionados y el código que actúa sobre ellos viajan juntos en lugar de como variables sueltas que pasas manualmente.
JunoPlantilla e instancias Llamar a una clase asigna un objeto en blanco y luego ejecuta __init__ en él. Los métodos viven una sola vez en la clase y se comparten, los datos viven por instancia, así que diez mil instancias siguen siendo baratas.

__init__ y self

__init__ es el método que Python llama automáticamente cuando creas una nueva instancia. Es donde estableces los datos iniciales para el objeto. self es cómo un método se refiere a la instancia específica en la que está operando, y siempre es el primer parámetro.

__init__ inicializa una instancia recién asignada. self es un nombre convencional para el primer parámetro de cada método de instancia; Python pasa la instancia automáticamente cuando llamas a alice.mostrar(). Los atributos establecidos en self dentro de __init__ son atributos de instancia: cada instancia tiene su propia copia.

__init__ se ejecuta en la instancia ya asignada y la configura. self no es una palabra clave, es el primer parámetro, y Python lo rellena para ti: cuando llamas a alice.mostrar(), Python pasa alice como self detrás de escenas. Establecer self.attr = valor escribe en esa instancia específica, así que dos objetos Jugador nunca pisan los datos del otro. Una cosa que sorprende a las personas que vienen de lenguajes más estrictos: Python te permite agregar atributos en cualquier lugar, no solo en __init__, así que un error tipográfico como self.puntaje = 0 crea un nuevo atributo en lugar de lanzar un error. Establece cada atributo que una instancia jamás usará en __init__, aunque sea a None, para que la forma del objeto se declare en un solo lugar y un error tipográfico en otro lugar sobresalga.

python
class Jugador:
    def __init__(self, nombre, puntaje=0):
        self.nombre = nombre
        self.puntaje = puntaje

    def agregar_puntos(self, puntos):
        self.puntaje += puntos

    def mostrar(self):
        print(f"{self.nombre}: {self.puntaje} puntos")

maría = Jugador("María")
carlos = Jugador("Carlos", puntaje=50)

maría.agregar_puntos(30)
maría.mostrar()   # "María: 30 puntos"
carlos.mostrar()  # "Carlos: 50 puntos"

self.nombre y self.puntaje son atributos de instancia: pertenecen al objeto específico, no a la clase misma. Cada instancia de Jugador tiene su propio nombre y puntaje.

Juno__init__ y self__init__ se ejecuta en el momento en que creas una instancia, así que es donde estableces los datos iniciales con self.nombre = valor. self es la instancia con la que Python está trabajando, y siempre es el primer parámetro de un método, entregado a ti automáticamente. Nunca pasas self tú mismo cuando llamas a alice.mostrar().
Juno__init__ y self__init__ configura una instancia nueva, y cualquier cosa que almacenes en self es un atributo de instancia, una copia por objeto. self es el primer parámetro de cada método de instancia; Python pasa la instancia para ti cuando escribes alice.mostrar(), solo lo nombras en la definición.
Juno__init__ y selfself es la instancia, pasada para ti en cada llamada, y escribe en self.attr aterriza en ese objeto específico. Python no te detendrá agregando atributos fuera de __init__, así que un self.puntuaje mal escrito hace silenciosamente un atributo nuevo. Declara cada atributo en __init__, incluso como None, y los errores tipográficos no tienen dónde esconderse.

Métodos

Cualquier función definida dentro de una clase es un método. Los métodos de instancia siempre tienen self como el primer parámetro; Python lo pasa automáticamente. Los métodos pueden leer y cambiar los datos de la instancia a través de self.

Los métodos de instancia son funciones regulares almacenadas en la clase. Cuando accedes a instancia.metodo, Python te devuelve un método vinculado: la misma función con la instancia ya conectada como self, así que solo pasas los argumentos restantes. Devolver self de un método permite encadenamiento: obj.escalar(2).rotar(90).

Un método es una función en la clase, compartida por cada instancia. Acceder a c.escalar no te entrega la función cruda, te entrega un método vinculado: esa misma función con c ya conectada como self, que es cómo se pasa self sin que lo hagas. Dos patrones dan resultado en código real. Devolver self de un método mutante te da una interfaz fluida (llamadas encadenadas como c.escalar(2).escalar(0.5)); guárdalo para objetos estilo constructor donde el encadenamiento lee bien, no para todo, ya que un método que devuelve self y uno que devuelve un valor nuevo se ven idénticos en el sitio de llamada. Y porque un método vinculado es un valor de primera clase, puedes almacenar uno (handler = obj.guardar) y llamarlo después, que es en lo que se basan los callbacks y controladores de eventos.

python
class Círculo:
    def __init__(self, radio):
        self.radio = radio

    def área(self):
        return 3.14159 * self.radio ** 2

    def escalar(self, factor):
        self.radio *= factor
        return self    # devolver self permite encadenamiento: c.escalar(2).escalar(0.5)

c = Círculo(5)
print(c.área())    # 78.53975
c.escalar(2)
print(c.área())    # 314.159
JunoMétodos Un método es una función definida dentro de una clase, y su primer parámetro siempre es self, la instancia con la que está trabajando. Python pasa self para ti, así que llamas a c.área() sin nada extra. A través de self un método lee y cambia los datos propios de ese objeto.
JunoMétodos Los métodos son funciones simples en la clase; alcanzarlos a través de una instancia vincula self automáticamente, así que pasas solo el resto. Devuelve self cuando quieres encadenamiento como c.escalar(2).escalar(0.5). De lo contrario devuelve el valor que el llamador pidió.
JunoMétodosobj.metodo te da un método vinculado, la función con obj conectada como self, que es por qué nunca lo pasas. Devolver self te compra encadenamiento fluido, vale la pena para constructores, confuso si cada método lo hace. Y como un método vinculado es un valor, handler = obj.guardar lo guarda para un callback después.

Variables de clase vs variables de instancia

Las variables definidas directamente en la clase (no dentro de __init__) son variables de clase. Todas las instancias comparten la misma variable de clase. Las variables establecidas en self dentro de __init__ son variables de instancia, únicas para cada objeto.

Una variable de clase se almacena una sola vez en la clase y se comparte por cada instancia. Una variable de instancia se almacena en el objeto individual. Cuando lees self.attr, Python verifica primero la instancia, luego la clase. Cuando escribes self.attr = valor, siempre crea o actualiza la copia propia de la instancia, ocultando la variable de clase para ese objeto solo.

Leer self.attr verifica primero la instancia, luego la clase, así que una variable de clase actúa como un predeterminado compartido hasta que alguna instancia escribe su propio valor y lo oculta. La trampa que muerde en producción es una variable de clase mutable (una que puedes cambiar en lugar, como una lista o diccionario). Asignar self.x = ... hace una copia privada, pero self.x.append(...) no: alcanza el objeto único compartido y cada instancia ve el cambio. Si quieres una lista fresca por instancia, constrúyela en __init__ con self.historial = [], nunca como historial = [] a nivel de clase. Reserva variables de clase para cosas que realmente se comparten, y mantenlas inmutables: constantes, predeterminados, contadores.

python
class Jugador:
    max_vidas = 3    # variable de clase, igual para cada Jugador

    def __init__(self, nombre):
        self.nombre = nombre   # variable de instancia, única para cada Jugador
        self.vidas = Jugador.max_vidas

    def morir(self):
        self.vidas -= 1

maría = Jugador("María")
carlos = Jugador("Carlos")

Jugador.max_vidas = 5    # cambia para todas las instancias presentes y futuras

Usa variables de clase para valores compartidos entre todas las instancias: constantes, contadores, predeterminados. Usa variables de instancia para datos que difieren por objeto.

JunoVariables de clase vs variables de instancia Una variable establecida directamente en la clase se comparte por cada instancia; una variable establecida en self en __init__ pertenece a ese objeto específico. Escribir self.attr = valor siempre crea o actualiza la copia propia de la instancia. Así que recurre a variables de clase cuando un valor es igual para todos, variables de instancia cuando difiere por objeto.
JunoVariables de clase vs variables de instancia Leer self.attr verifica la instancia, luego la clase, así que una variable de clase es un predeterminado compartido. Escribir self.attr = valor siempre aterriza en la instancia y oculta la de clase para ese objeto. Mantén variables de clase para constantes y predeterminados, estado por objeto va en self.
JunoVariables de clase vs variables de instancia El error silencioso es una variable de clase mutable: self.x = ... hace una copia privada pero self.x.append(...) muta el objeto único compartido, así que cada instancia lo ve. Quieres una lista por instancia, constrúyela en __init__ con self.historial = [], nunca historial = [] a nivel de clase.

__str__ y __repr__

__str__ controla lo que print() y f-strings muestran para tu objeto. __repr__ controla la vista de desarrollador mostrada en la consola y para depuración. Siempre define __repr__. Define __str__ cuando quieres una visualización limpia orientada al usuario separada de la vista de depuración.

__str__ es llamado por str(), print() y los f-strings del capítulo de salida, el texto orientado al usuario. __repr__ es llamado por repr() y se muestra cuando un objeto se imprime en la consola, la vista de desarrollador. Si solo se define __repr__, Python lo usa para ambos. La convención: __repr__ devuelve una cadena que se parece al código para recrear el objeto; __str__ devuelve un resumen legible.

Estos son dos métodos dunder (abreviatura de doble guion bajo: métodos nombrados con guiones bajos iniciales y finales __ que Python llama para ti en el momento correcto). print(obj) y str(obj) llaman a __str__, retrocediendo a __repr__ cuando no hay __str__. repr(obj), y un objeto mostrado desnudo en la consola, llaman a __repr__. Así que __repr__ es el que siempre tiene un trabajo, que es por qué la regla es: siempre define __repr__, agrega __str__ solo cuando el texto orientado al usuario debe diferir del texto de depuración. Haz que __repr__ se parezca a la llamada del constructor que construyó el objeto (Jugador(nombre='María', puntaje=87)) y usa !r en los campos, así que los strings mantienen sus comillas y un espacio o salto de línea perdido muestra en lugar de ocultarse. Un buen __repr__ es lo que hace que una línea de registro o un seguimiento de pila sea legible a las 2 de la mañana.

python
class Jugador:
    def __init__(self, nombre, puntaje):
        self.nombre = nombre
        self.puntaje = puntaje

    def __str__(self):
        return f"{self.nombre} ({self.puntaje} pts)"

    def __repr__(self):
        return f"Jugador(nombre={self.nombre!r}, puntaje={self.puntaje})"

maría = Jugador("María", 87)
print(maría)        # "María (87 pts)"   (usa __str__)
repr(maría)         # "Jugador(nombre='María', puntaje=87)"  (usa __repr__)

Siempre define __repr__. Define __str__ cuando quieres una representación limpia orientada al usuario separada de la vista de depuración. Si solo se define __repr__, Python lo usa para ambos.

Juno__str__ y __repr____str__ es lo que print() y f-strings muestran, la versión amable. __repr__ es la vista de desarrollador que ves en la consola. Siempre escribe __repr__; es el que tiene un trabajo incluso cuando olvidas __str__. Agrega __str__ solo cuando el texto orientado al usuario debe leer diferente.
Juno__str__ y __repr____str__ es la versión legible para print() y f-strings; __repr__ es la versión de depuración, y Python retrocede a ella cuando no hay __str__. Siempre define __repr__, hazlo parecer la llamada que construyó el objeto, y te ahorras problemas después cuando un objeto aparece en un registro.
Juno__str__ y __repr____repr__ siempre tiene un trabajo, __str__ solo cuando la vista de usuario debe diferir, así que define __repr__ primero y siempre. Dale forma de constructor y pon !r en los campos, así que un espacio o salto de línea perdido muestra en lugar de ocultarse. Ese único hábito es lo que hace que un seguimiento de pila a las 2 de la mañana sea legible.

Convención privada

Python no tiene variables privadas reales, pero un solo guion bajo al inicio de un nombre (_saldo) es una convención que señala "esto es interno, no lo uses directamente desde fuera de la clase". No se aplica por el lenguaje; es una comunicación a otros desarrolladores.

Un solo guion bajo (_attr) es una convención que señala uso interno. Python no lo aplica, pero todos los linters, IDEs y desarrolladores lo respetan. Un doble guion bajo (__attr) desencadena ofuscación de nombres: Python lo reescribe como _NombreClase__attr, lo que previene colisión accidental en subclases. No es privacidad verdadera; es un mecanismo de evitación de colisiones.

Un solo guion bajo es convención solamente: nada lo aplica, pero linters (herramientas que analizan tu código para estilo y bugs probables), editores y revisores todos leen _saldo como "interno, no toques desde fuera". Un doble guion bajo desencadena ofuscación de nombres: Python reescribe __attr a _NombreClase__attr así una subclase que define su propio __attr no puede chocar con la del padre por accidente. Es un guardián de colisiones, no privacidad: el nombre ofuscado sigue siendo alcanzable desde fuera si alguien insiste. Recurre a él solo en una clase base destinada a ser subclasificada ampliamente, donde necesitas un atributo que las subclases no puedan pisar; en código ordinario un solo guion bajo es el predeterminado correcto y el doble guion bajo mayormente crea confusión. Si quieres detener completamente atributos arbitrarios que se establezcan (el self.puntuaje mal escrito de antes), define __slots__ con la lista fija de nombres de atributos: le dice a Python que esos son los únicos atributos que una instancia puede tener, así que cualquier otra asignación lanza un error en lugar de silenciosamente pegarse, y limpia memoria por instancia como bonificación.

python
class CuentaBancaria:
    def __init__(self, saldo):
        self._saldo = saldo    # _ significa "no toques"

    def depositar(self, cantidad):
        if cantidad > 0:
            self._saldo += cantidad

    def saldo(self):
        return self._saldo

Un doble guion bajo (__nombre) desencadena ofuscación de nombres; Python renombra el atributo a _NombreClase__nombre para evitar conflictos en subclases. Es raramente necesario. Un solo guion bajo es la convención en la mayoría del código.

JunoConvención privada Python no tiene variables privadas reales, pero un guion bajo inicial (_saldo) es la señal acordada para "interno, déjalo en paz desde fuera". Nada te detiene alcanzando adentro, es un mensaje a otros desarrolladores, incluyendo tú futuro. Un doble guion bajo es una herramienta más rara para evitar choques de nombres en subclases; el único guion bajo es lo que usarás día a día.
JunoConvención privada Un solo guion bajo significa "interno", y cada herramienta y revisor lo respeta aunque Python no lo haga. Un doble guion bajo desencadena ofuscación de nombres a _NombreClase__attr, que guarda contra choques de subclases, no contra acceso. Recurre al único guion bajo por defecto; el doble es raro.
JunoConvención privada Un solo guion bajo es convención, el doble guion bajo ofusca a _NombreClase__attr para detener choques de subclases, ninguno es privacidad real. Mantén __attr para clases base ampliamente subclasificadas; en otro lugar solo agrega confusión. Quieres prohibir atributos extraviados completamente y atrapar el `self.puntuaje` mal escrito, establece `__slots__` a la lista fija.

Herencia

Una clase puede heredar de otra clase, obteniendo automáticamente todos sus atributos y métodos. Luego puedes anular métodos específicos en la subclase para cambiar su comportamiento. Esto te permite reutilizar una base común y especializar donde sea necesario.

La herencia modela una relación "es-a": un Perro es un Animal. La subclase obtiene todos los métodos y atributos del padre y puede anular cualquiera de ellos definiendo su propia versión. Cuando llamas a un método, Python busca primero en la subclase, luego sube al padre, así que un método no anulado cae automáticamente. Esa ruta de búsqueda es el orden de resolución de métodos (MRO), la lista ordenada de clases que Python busca.

Cuando llamas a obj.metodo, Python busca las clases en un orden fijo, el MRO (orden de resolución de métodos: la lista aplanada de una clase y sus ancestros, legible como Perro.__mro__), y usa la primera coincidencia. La herencia simple es directa, subclase luego padre. La razón por la que existe el MRO es la herencia múltiple y el caso diamante (dos padres que ambos heredan de un abuelo común): Python ordena las clases así el abuelo aparece una sola vez, después de ambos padres, y resuelve llamadas determinísticamente. La guía práctica: prefiere jerarquías planas y herencia simple, porque los árboles profundos o de herencia múltiple hacen "qué método realmente corrió" difícil de razonar. Cuando sí mezclas múltiples bases, encamina cada __init__ a través de super() (siguiente sección) así cada uno corre exactamente una vez en ese orden.

python
class Animal:
    def __init__(self, nombre):
        self.nombre = nombre

    def hablar(self):
        return "..."

class Perro(Animal):
    def hablar(self):
        return f{self.nombre} ladra Guau!"

class Gato(Animal):
    def hablar(self):
        return f{self.nombre} maúlla Miau!"

mascotas = [Perro("Rex"), Gato("Luna"), Perro("Max")]
for mascota in mascotas:
    print(mascota.hablar())

Perro y Gato heredan __init__ de Animal, así que no necesitan el propio. Anulan hablar() con su comportamiento específico.

JunoHerencia Una subclase hereda todo lo que el padre tiene, luego anula solo los métodos que quiere cambiar. Cualquier cosa que no anulesretrocede al padre gratis, que es el punto completo: escribe el comportamiento compartido una vez, especializa donde difiere. Perro y Gato reutilizan `__init__` de `Animal` y solo redefinen `hablar()`.
JunoHerencia La herencia es "es-a": la subclase obtiene los métodos del padre y anula lo que necesita, y las llamadas no anuladas retroceden. Python encuentra un método caminando el MRO, subclase primero, luego arriba al padre. Reutiliza la base compartida, redefine solo lo que difiere.
JunoHerencia Python resuelve un método caminando el MRO y tomando la primera coincidencia, que es lo que hace el caso diamante determinístico. Mantén jerarquías planas y confía en herencia simple; los árboles profundos o de herencia múltiple convierten "qué método corrió" en un juego de adivinanzas. Cuando debes mezclar bases, encadena cada `__init__` a través de `super()`.

super()

super() llama a un método de la clase padre. Úsalo cuando quieras extender el comportamiento del padre en lugar de reemplazarlo completamente: llama a __init__ del padre para ejecutar su configuración, luego agrega cualquier cosa que tu subclase necesite encima.

super() devuelve un objeto proxy que delega llamadas de métodos a la siguiente clase en el MRO. Siempre llama a super().__init__() desde un __init__ de subclase cuando el padre tiene uno. Saltarlo significa que el código de configuración del padre no se ejecuta, lo que puede dejar el objeto en un estado roto.

super() no significa "mi padre directo", significa "la siguiente clase a lo largo del MRO" (el orden de búsqueda de la sección anterior). En una subclase simple esa siguiente clase es el padre, así la distinción se ve académica, pero es exactamente lo que hace funcionar la herencia múltiple: si cada clase en el árbol llama a super().__init__(...), cada __init__ se ejecuta una vez, en orden MRO, sin que ninguna clase codifique un padre por nombre. Eso es por qué escribes super().__init__() y no Animal.__init__(self): nombrar el padre directamente rompe la cadena en el momento en que se mezcla otra clase, y puede ejecutar una base compartida dos veces. La forma super() desnuda sin argumentos es la forma moderna; la más antigua super(Perro, self) es lo mismo deletreado, que todavía verás en código que apunta a versiones más antiguas.

python
class Animal:
    def __init__(self, nombre, sonido):
        self.nombre = nombre
        self.sonido = sonido

class Perro(Animal):
    def __init__(self, nombre):
        super().__init__(nombre, "Guau")   # llama a Animal.__init__
        self.trucos = []                    # agrega algo extra

    def aprender(self, truco):
        self.trucos.append(truco)

rex = Perro("Rex")
rex.aprender("sentarse")
print(rex.trucos)   # ["sentarse"]

Siempre llama a super().__init__() cuando tu subclase tiene su propio __init__ y el padre también.

Junosuper()super() alcanza la clase padre, así que super().__init__() ejecuta la configuración del padre antes de agregar la tuya. Úsalo cuando tu subclase escribe su propio `__init__` y el padre tiene uno también. Sáltalo y la configuración del padre nunca se ejecuta, dejando el objeto medio construido.
Junosuper()super() delega a la siguiente clase arriba, así que llama a super().__init__() desde un `__init__` de subclase siempre que el padre tenga uno. Olvídalo y la configuración del padre se salta, lo que puede dejar el objeto en un estado roto. Es la línea que conecta tu subclase al padre que extiende.
Junosuper()super() significa "siguiente clase en el MRO", no "mi padre", que es por qué codificar `Animal.__init__(self)` rompe la cadena una vez que una clase se mezcla. Si cada `__init__` llama a `super()`, cada uno se ejecuta una vez en orden. La forma `super()` desnuda es la moderna; `super(Perro, self)` es lo mismo deletreado.

Métodos de clase y métodos estáticos

@classmethod crea un método que recibe la clase misma en lugar de una instancia. Es útil para constructores alternativos: crear una instancia desde una cadena, un archivo u otro formato. @staticmethod es una función simple que vive dentro de la clase por razones organizacionales; no recibe ni la instancia ni la clase.

@classmethod recibe cls (la clase) como su primer argumento, no una instancia. El uso principal es constructores alternativos que crean instancias a partir de formatos de entrada diferentes. @staticmethod es una función regular espaciada bajo la clase; no tiene acceso a la clase o instancia. Usa @classmethod para constructores, @staticmethod para funciones de utilidad lógicamente vinculadas a la clase.

@classmethod recibe la clase como su primer argumento (cls) en lugar de una instancia, y el detalle que gana su valor es que cls es la clase en la que se llamó el método, no la que se definió. Así que si Jugador.desde_cadena construye con cls(...) y una subclase JugadorPro llama a JugadorPro.desde_cadena(...), construye un JugadorPro, no un Jugador. Eso es exactamente por qué los constructores alternativos usan @classmethod en lugar de codificar el nombre de la clase: mantienen funcionando bajo herencia. @staticmethod no toma ni la instancia ni la clase; es una función simple estacionada dentro de la clase por espaciado de nombres, cuando la lógica pertenece a la clase pero no necesita ninguno de sus datos. Recurre a un classmethod cuando estés haciendo una instancia desde algún otro formato, un staticmethod para un auxiliar relacionado, y un método normal para cualquier cosa que toque datos de un objeto.

python
class Jugador:
    def __init__(self, nombre, puntaje):
        self.nombre = nombre
        self.puntaje = puntaje

    @classmethod
    def desde_cadena(cls, datos):
        nombre, puntaje = datos.split(",")
        return cls(nombre, int(puntaje))

maría = Jugador.desde_cadena("María,87")
python
class Jugador:
    @staticmethod
    def es_nombre_válido(nombre):
        return nombre.isalpha() and len(nombre) >= 2

Jugador.es_nombre_válido("María")   # True
Jugador.es_nombre_válido("M1")      # False

Usa @classmethod para constructores alternativos. Usa @staticmethod para funciones de utilidad que lógicamente pertenecen a la clase pero no necesitan datos de instancia o clase.

JunoMétodos de clase y métodos estáticos@classmethod te entrega la clase en lugar de una instancia, que la hace la opción de ir para constructores alternativos: construye un `Jugador` desde una cadena, un archivo, cualquier formato que tengas. @staticmethod es una función ordinaria metida dentro de la clase para limpieza; no obtiene ni la clase ni la instancia. Los métodos simples tocan datos de un objeto, estos dos no.
JunoMétodos de clase y métodos estáticos@classmethod toma cls, la clase misma, así que úsalo para constructores alternativos que devuelven cls(...). @staticmethod toma nada implícito, un auxiliar espaciado bajo la clase. Método para datos por objeto, classmethod para construir instancias, staticmethod para un auxiliar relacionado.
JunoMétodos de clase y métodos estáticos El punto de un constructor classmethod es que `cls` es quien lo llamó, así que `JugadorPro.desde_cadena(...)` construye un `JugadorPro`, no un `Jugador`. Codifica el nombre de la clase y pierdes eso. Classmethod para construir, staticmethod para un auxiliar relacionado que no necesita datos, método normal para cualquier cosa que toque la instancia.

@property

@property te permite acceder a un método como un atributo, sin paréntesis necesarios. Úsalo para valores que se calculan a partir de otros atributos y se sienten naturales de leer como acceso de atributo simple.

@property convierte un método en un atributo de solo lectura. El método se ejecuta cuando se accede al atributo. Esto es útil para valores calculados derivados de datos almacenados, y para agregar validación al acceso de atributos sin cambiar la interfaz pública. Un @nombre.setter emparejado hace el atributo escribible.

@property convierte un método en algo que lees como un atributo: c.área, sin paréntesis, ejecuta el método cada vez. Agrega @area.setter y se vuelve escribible también, con tu código ejecutándose de camino, que es donde va la validación (rechaza un radio negativo antes de que se almacene). El valor real es que esto no cambia nada para los llamadores: puedes empezar con un atributo self.radio simple y luego promoverlo a una propiedad con validación sin tocar una sola línea que lo lea o escriba. Eso es por qué Python no tiene ceremonia getter/setter, expones atributos simples y recurres a una propiedad solo cuando uno necesita computación o protección. Dos precauciones para producción: el acceso se ve libre pero ejecuta código, así que mantenlo barato (cachea un resultado caro en lugar de recomputar en cada lectura), y una propiedad que silenciosamente hace trabajo pesado o lanza sorprende a quien asumió que era un campo simple.

python
class Círculo:
    def __init__(self, radio):
        self.radio = radio

    @property
    def área(self):
        return 3.14159 * self.radio ** 2

    @property
    def diámetro(self):
        return self.radio * 2

c = Círculo(5)
print(c.área)      # 78.53975 (se parece a un atributo, corre como un método)
print(c.diámetro)  # 10

Las propiedades son útiles para valores calculados: cosas derivadas de otros atributos que se sienten naturales de acceder sin ().

Juno@property@property te permite leer un método como un atributo, sin paréntesis: c.área en lugar de c.área(). Se adapta a valores que se elaboran a partir de otros atributos y se sienten naturales de leer como datos simples. Detrás de escenas aún ejecuta tu método cada vez que lo accedes.
Juno@property@property lee un método como un atributo, y un @nombre.setter emparejado lo hace escribible así puedes validar de camino. La ganancia: puedes convertir un atributo simple en una propiedad después sin cambiar ningún llamador. Úsalo para valores calculados o protegidos, no como hábito en cada campo.
Juno@property La razón para amar @property: un atributo simple puede volverse uno computado-o-validado después sin cambio a los llamadores, así que saltas getters y setters hasta que realmente necesites uno. El acceso se ve libre pero ejecuta código, así que mantenlo barato, cachea los caros, y no ocultes trabajo pesado o un lanzamiento detrás de lo que se lee como un campo.

En práctica

Una clase Jugador con atributos de instancia, métodos, una @property, y __str__:

python
class Jugador:
    max_vidas = 3

    def __init__(self, nombre: str):
        self.nombre = nombre
        self.puntaje = 0
        self.vidas = Jugador.max_vidas

    def ganar_puntos(self, cantidad: int) -> None:
        self.puntaje += cantidad

    def recibir_golpe(self) -> bool:
        self.vidas -= 1
        return self.vidas > 0

    @property
    def está_vivo(self) -> bool:
        return self.vidas > 0

    def __str__(self) -> str:
        return f"{self.nombre} | Puntaje: {self.puntaje} | Vidas: {self.vidas}"

maría = Jugador("María")
maría.ganar_puntos(50)
maría.recibir_golpe()
print(maría)            # "María | Puntaje: 50 | Vidas: 2"
print(maría.está_vivo)   # True

Una clase Usuario que usa un atributo privado con un getter @property, un método desactivar, y un serializador a_dict:

python
class Usuario:
    def __init__(self, id_usuario: int, nombre_usuario: str, correo: str):
        self.id = id_usuario
        self.nombre_usuario = nombre_usuario
        self.correo = correo
        self._activo = True

    @property
    def activo(self) -> bool:
        return self._activo

    def desactivar(self) -> None:
        self._activo = False

    def a_dict(self) -> dict:
        return {
            "id": self.id,
            "nombre_usuario": self.nombre_usuario,
            "correo": self.correo,
            "activo": self._activo,
        }

    def __repr__(self) -> str:
        return f"Usuario(id={self.id}, nombre_usuario={self.nombre_usuario!r})"

maría = Usuario(1, "maría", "marí[email protected]")
print(maría.a_dict())
maría.desactivar()
print(maría.activo)   # False

Una clase DivisiónDatos que encapsula particionamiento train/validación detrás de propiedades, con __repr__ para salida de depuración limpia:

python
class DivisiónDatos:
    def __init__(self, datos: list, ratio_entrenamiento: float = 0.8):
        partición = int(len(datos) * ratio_entrenamiento)
        self._entrenamiento = datos[:partición]
        self._validación = datos[partición:]

    @property
    def entrenamiento(self) -> list:
        return self._entrenamiento

    @property
    def validación(self) -> list:
        return self._validación

    @property
    def tamaños(self) -> tuple[int, int]:
        return len(self._entrenamiento), len(self._validación)

    def __repr__(self) -> str:
        return f"DivisiónDatos(entrenamiento={len(self._entrenamiento)}, validación={len(self._validación)})"

datos = list(range(100))
división = DivisiónDatos(datos, ratio_entrenamiento=0.8)
print(división)         # DivisiónDatos(entrenamiento=80, validación=20)
print(división.tamaños)   # (80, 20)

El prefijo de guion bajo en _entrenamiento y _validación señala que los llamadores deberían ir a través de las propiedades en lugar de mutar las listas crudas directamente. Python no lo aplicará, pero establece un contrato claro.