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

Clases y objetos ​

Todos los tipos con los que has trabajado hasta ahora (strings, listas, diccionarios) son en realidad clases. Cuando llamas a "hello".upper(), estás llamando a un método de un objeto string. Las clases te permiten definir tus propios tipos con su propia información y comportamiento. Una clase Player puede guardar un nombre, un puntaje y un nivel, y saber cómo mostrarse a sí misma.

Las clases son el mecanismo para crear tipos definidos por el usuario. Una clase define una plantilla: la información que guarda cada instancia (sus atributos, los valores almacenados en ella) y las operaciones que soporta (sus métodos, las funciones asociadas a ella). En vez de rastrear valores en variables paralelas y pasarlas por todas partes, las agrupas en un solo objeto con una interfaz clara.

Una declaración class crea un nuevo tipo (una clase personalizada de valor, del mismo tipo que str e int) y lo asocia a un nombre, igual que cualquier otra asignación. Creas una instancia (un objeto concreto de ese tipo) llamando a la clase: Player("Alice"). Detrás de esa llamada, Python asigna un objeto vacío y ejecuta __init__ para completarlo. El modelo es uniforme de principio a fin: una clase es en sí misma un objeto que puedes guardar en una variable, pasar a una función o inspeccionar en tiempo de ejecución (mientras el programa corre), y esto es justo lo que hace posibles los constructores alternativos y los registros que vienen más adelante en este capítulo.

Plano y instancias ​

Una clase es un plano de construcción. Una instancia es una cosa específica creada a partir de ese plano. Puedes crear tantas instancias como necesites, cada una con su propia información 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 tiene su propia información pero comparte los objetos de método de la clase. Crear una instancia es llamar a la clase como si fuera una función: Dog() crea una nueva instancia de Dog.

Llamar a una clase hace dos cosas en orden: asigna una instancia vacía y luego ejecuta __init__ sobre ella para completar la información. El objeto terminado recuerda a qué clase pertenece (puedes verlo con type(rex)), y ese vínculo es lo que le permite a Python encontrar un método cuando llamas a rex.bark(): primero busca en la instancia y 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 la información vive por instancia, así que mil objetos Dog te cuestan mil conjuntos de datos pero solo una copia de cada método.

python
class Dog:
    def bark(self):
        print("Woof!")

rex = Dog()
luna = Dog()

rex.bark()    # "Woof!"
luna.bark()   # "Woof!"

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

JunoPlano e instancias Una clase es el plano, una instancia es una cosa construida a partir de ella. Llama a la clase como si fuera una función, Dog(), y obtienes una instancia nueva. Cada instancia comparte los métodos de la clase pero mantiene su propia información, así que rex y luna pueden comportarse igual siendo perros distintos.
JunoPlano e instancias Llamar a Dog() crea una instancia: información propia, métodos compartidos de la clase. Ese agrupamiento es todo el punto: los valores relacionados y el código que actúa sobre ellos viajan juntos en vez de andar como variables sueltas que pasas a mano.
JunoPlano e instancias Llamar a una clase asigna un objeto vacío y luego ejecuta __init__ sobre él. Los métodos están una sola vez en la clase y se comparten, la información está por instancia, así que una llamada a un método en rex se resuelve buscando primero en la instancia y luego en la clase. Esa separación es la razón por la que diez mil instancias siguen siendo económicas.

__init__ y self ​

__init__ es el método que Python llama automáticamente cuando creas una nueva instancia. Es donde configuras los datos iniciales del objeto. self es la forma en que un método se refiere a la instancia específica sobre 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 todo método de instancia; Python pasa la instancia automáticamente cuando llamas a alice.display(). Los atributos que se configuran en self dentro de __init__ son atributos de instancia: cada instancia tiene su propia copia.

__init__ se ejecuta sobre la instancia ya asignada y la configura. self no es una palabra reservada, es el primer parámetro, y Python lo completa por ti: cuando llamas a alice.display(), Python pasa alice como self sin que lo veas. Escribir self.attr = value guarda el dato en esa única instancia, así que dos objetos Player nunca pisan la información del otro. Algo que sorprende a quienes vienen de lenguajes más estrictos: Python te deja agregar atributos en cualquier lugar, no solo en __init__, así que un error de tipeo como self.scor = 0 crea un nuevo atributo en vez de lanzar un error. Define en __init__ todos los atributos que la instancia vaya a usar alguna vez, aunque sea con None, para que la forma del objeto quede declarada en un solo lugar y un error de tipeo en otra parte resalte.

python
class Player:
    def __init__(self, name, score=0):
        self.name = name
        self.score = score

    def add_points(self, points):
        self.score += points

    def display(self):
        print(f"{self.name}: {self.score} points")

alice = Player("Alice")
bob = Player("Bob", score=50)

alice.add_points(30)
alice.display()   # "Alice: 30 points"
bob.display()     # "Bob: 50 points"

self.name y self.score son atributos de instancia: pertenecen al objeto específico, no a la clase en sí. Cada instancia de Player tiene su propio name y score.

Juno__init__ y self__init__ se ejecuta en el momento en que creas una instancia, así que ahí es donde configuras los datos iniciales con self.name = value. self es la instancia con la que Python está trabajando, y siempre es el primer parámetro de un método, entregado automáticamente. Nunca pasas self tú mismo cuando llamas a alice.display().
Juno__init__ y self__init__ configura una instancia recién creada, y todo lo que guardes en self ahí es un atributo de instancia, una copia por objeto. self es el primer parámetro de todo método de instancia; Python pasa la instancia por ti cuando escribes alice.display(), tú solo lo nombras en la definición.
Juno__init__ y selfself es la instancia, se pasa automáticamente en cada llamada, y las escrituras en self.attr quedan en ese único objeto. Python no te va a impedir agregar atributos fuera de __init__, así que un self.scor mal escrito crea silenciosamente un nuevo atributo. Declara cada atributo en __init__, aunque sea como None, y los errores de tipeo no tendrán 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 primer parámetro; Python lo pasa automáticamente. Los métodos pueden leer y modificar la información de la instancia a través de self.

Los métodos de instancia son funciones normales almacenadas en la clase. Cuando accedes a instance.method, Python te devuelve un método vinculado (bound method): la misma función con la instancia ya conectada como self, así que solo pasas los argumentos restantes. Devolver self desde un método permite encadenar llamadas: obj.scale(2).rotate(90).

Un método es una sola función en la clase, compartida por todas las instancias. Acceder a c.scale no te entrega la función en crudo, sino un método vinculado (bound method): esa misma función con c ya conectado como self, y así es como self se pasa sin que tú lo hagas. Dos patrones dan buenos resultados en código real. Devolver self desde un método que muta el objeto te da una interfaz fluida (llamadas encadenadas como c.scale(2).scale(0.5)); reserva esto para objetos de estilo builder donde encadenar tenga sentido, no para todo, porque un método que devuelve self y uno que devuelve un valor nuevo se ven idénticos en el punto de llamada. Y como un método vinculado es un valor de primera clase, puedes guardarlo (handler = obj.save) y llamarlo después, que es justo en lo que se basan los callbacks y los manejadores de eventos.

python
class Circle:
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.14159 * self.radius ** 2

    def scale(self, factor):
        self.radius *= factor
        return self    # devolver self permite encadenar: c.scale(2).scale(0.5)

c = Circle(5)
print(c.area())    # 78.53975
c.scale(2)
print(c.area())    # 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 por ti, así que llamas a c.area() sin nada extra. A través de self, un método lee y modifica la información propia de ese objeto.
JunoMétodos Los métodos son funciones normales en la clase; acceder a ellos a través de una instancia vincula self automáticamente, así que solo pasas el resto. Devuelve self cuando quieras encadenar llamadas como c.scale(2).scale(0.5). En cualquier otro caso, devuelve el valor que espera quien llama.
JunoMétodosobj.method te da un método vinculado, la función con obj conectado como self, por eso nunca lo pasas tú. Devolver self te da encadenamiento fluido, útil en builders, confuso si todos los métodos lo hacen. Y como un método vinculado es un valor, handler = obj.save lo guarda para usarlo después como callback.

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 que se configuran en self dentro de __init__ son variables de instancia, únicas para cada objeto.

Una variable de clase se guarda una sola vez en la clase y es compartida por todas las instancias. Una variable de instancia se guarda en el objeto individual. Cuando lees self.attr, Python primero revisa la instancia y luego la clase. Cuando escribes self.attr = value, siempre crea o actualiza la copia propia de la instancia, ocultando la variable de clase solo para ese objeto.

Leer self.attr revisa primero la instancia y luego la clase, así que una variable de clase funciona como un valor por defecto 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 su lugar, como una lista o un diccionario). Asignar self.x = ... crea una copia privada, pero self.x.append(...) no lo hace: alcanza el mismo objeto compartido y todas las instancias ven el cambio. Si quieres una lista nueva por instancia, constrúyela en __init__ con self.history = [], nunca como history = [] a nivel de clase. Reserva las variables de clase para lo que realmente se comparte, y mantenlas inmutables: constantes, valores por defecto, contadores.

python
class Player:
    max_lives = 3    # variable de clase, igual para todo Player

    def __init__(self, name):
        self.name = name   # variable de instancia, única para cada Player
        self.lives = Player.max_lives

    def die(self):
        self.lives -= 1

alice = Player("Alice")
bob = Player("Bob")

Player.max_lives = 5    # cambia para todas las instancias actuales y futuras

Usa variables de clase para valores compartidos entre todas las instancias: constantes, contadores, valores por defecto. Usa variables de instancia para información que difiere por objeto.

JunoVariables de clase vs. variables de instancia Una variable definida directamente en la clase se comparte entre todas las instancias; una variable definida en self dentro de __init__ pertenece a ese único objeto. Escribir self.attr = value siempre crea o actualiza la copia propia de la instancia. Usa variables de clase cuando un valor es igual para todos, y variables de instancia cuando difiere por objeto.
JunoVariables de clase vs. variables de instancia Leer self.attr revisa la instancia y luego la clase, así que una variable de clase es un valor por defecto compartido. Escribir self.attr = value siempre cae en la instancia y oculta la de clase para ese objeto. Guarda las variables de clase para constantes y valores por defecto; el estado por objeto va en self.
JunoVariables de clase vs. variables de instancia El bug silencioso es una variable de clase mutable: self.x = ... crea una copia privada, pero self.x.append(...) muta el objeto compartido, así que todas las instancias lo ven. Si quieres una lista por instancia, constrúyela en __init__ con self.history = [], nunca history = [] a nivel de clase.

__str__ y __repr__ ​

__str__ controla lo que print() y los f-strings muestran de tu objeto. __repr__ controla la vista para desarrolladores que se muestra en la consola y para depuración. Define siempre __repr__. Define __str__ cuando quieras una presentación limpia para el usuario, distinta de la vista de depuración.

__str__ es llamado por str(), print() y los f-strings del capítulo sobre salida, el texto orientado al usuario. __repr__ es llamado por repr() y se muestra cuando un objeto se imprime en la consola, la vista para desarrolladores. Si solo se define __repr__, Python lo usa para ambos casos. La convención es: __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 (abreviación de double-underscore: métodos nombrados con __ al inicio y al final que Python llama por ti en el momento adecuado). print(obj) y str(obj) llaman a __str__, y recurren a __repr__ cuando no hay __str__. repr(obj), y un objeto mostrado sin más en la consola, llaman a __repr__. Por eso __repr__ es el que siempre tiene una tarea que cumplir, y de ahí la regla: define siempre __repr__, agrega __str__ solo cuando el texto orientado al usuario deba diferir del texto de depuración. Haz que __repr__ se vea como la llamada al constructor que construyó el objeto (Player(name='Alice', score=87)) y usa !r en los campos, para que las cadenas conserven sus comillas y un espacio o salto de línea extraño se note en vez de esconderse. Un buen __repr__ es lo que hace que una línea de log o un stack trace se puedan leer a las 2 de la madrugada.

python
class Player:
    def __init__(self, name, score):
        self.name = name
        self.score = score

    def __str__(self):
        return f"{self.name} ({self.score} pts)"

    def __repr__(self):
        return f"Player(name={self.name!r}, score={self.score})"

alice = Player("Alice", 87)
print(alice)        # "Alice (87 pts)"   (usa __str__)
repr(alice)         # "Player(name='Alice', score=87)"  (usa __repr__)

Define siempre __repr__. Define __str__ cuando quieras una representación limpia para el usuario, distinta de la vista de depuración. Si solo se define __repr__, Python lo usa para ambos casos.

Juno__str__ y __repr____str__ es lo que muestran print() y los f-strings, la versión amigable. __repr__ es la vista para desarrolladores que ves en la consola. Escribe siempre __repr__; es el que cumple su función incluso cuando olvidas __str__. Agrega __str__ solo cuando el texto orientado al usuario deba leerse distinto.
Juno__str__ y __repr____str__ es la versión legible para print() y los f-strings; __repr__ es la versión de depuración, y Python recurre a ella cuando no hay __str__. Define siempre __repr__, hazlo parecerse a la llamada que construyó el objeto, y te ahorrarás problemas cuando un objeto aparezca en un log.
Juno__str__ y __repr____repr__ siempre tiene una tarea que cumplir, __str__ solo cuando la vista para el usuario deba diferir, así que define __repr__ primero y siempre. Dale la forma del constructor y pon !r en los campos, para que un espacio o salto de línea extraño se muestre en vez de esconderse. Ese solo hábito es lo que hace legible un stack trace a las 2 de la madrugada.

Convención de privacidad ​

Python no tiene variables realmente privadas, pero un solo guion bajo al inicio de un nombre (_balance) es una convención que señala "esto es interno, no lo uses directamente desde fuera de la clase". No lo impone el lenguaje; es una forma de comunicárselo a otros desarrolladores.

Un solo guion bajo (_attr) es una convención que señala uso interno. Python no lo impone, pero todos los linters, IDEs y desarrolladores la respetan. Un doble guion bajo (__attr) activa el name mangling: Python lo reescribe como _ClassName__attr, lo cual evita colisiones accidentales en subclases. No es privacidad real; es un mecanismo para evitar colisiones.

Un solo guion bajo es pura convención: nada lo impone, pero los linters (herramientas que revisan tu código buscando estilo y posibles errores), los editores y los revisores de código leen _balance como "interno, no tocar desde afuera". Un doble guion bajo activa el name mangling: Python reescribe __attr como _ClassName__attr para que una subclase que defina su propio __attr no choque accidentalmente con el de la clase padre. Es una protección contra colisiones, no privacidad: el nombre reescrito sigue siendo accesible desde afuera si alguien insiste. Úsalo solo en una clase base pensada para tener muchas subclases, donde necesitas un atributo con el que las subclases no puedan interferir; en código normal, un solo guion bajo es la opción correcta por defecto, y el doble guion bajo generalmente solo genera confusión. Si quieres impedir por completo que se asignen atributos arbitrarios (el self.scor mal escrito de antes), define __slots__ con la lista fija de nombres de atributos: eso le indica a Python que esos son los únicos atributos que puede tener una instancia, así que cualquier otra asignación lanza un error en vez de quedar pegada silenciosamente, y además reduce el consumo de memoria por instancia como beneficio extra.

python
class BankAccount:
    def __init__(self, balance):
        self._balance = balance    # _ significa "no tocar"

    def deposit(self, amount):
        if amount > 0:
            self._balance += amount

    def balance(self):
        return self._balance

Un doble guion bajo (__name) activa el name mangling; Python renombra el atributo como _ClassName__name para evitar conflictos en subclases. Rara vez se necesita. El guion bajo simple es la convención en la mayoría del código.

JunoConvención de privacidad Python no tiene variables realmente privadas, pero un guion bajo al inicio (_balance) es la señal acordada para "interno, no tocar desde afuera". Nada te impide acceder a ella, es un mensaje para otros desarrolladores, incluyendo tu propio yo futuro. El doble guion bajo es una herramienta más rara para evitar choques de nombres en subclases; el guion bajo simple es lo que usarás en el día a día.
JunoConvención de privacidad Un solo guion bajo significa "interno", y todas las herramientas y revisores lo respetan aunque Python no lo imponga. Un doble guion bajo activa el name mangling hacia _ClassName__attr, lo cual protege contra choques de nombres en subclases, no contra el acceso. Usa el guion bajo simple por defecto; el doble es raro.
JunoConvención de privacidad El guion bajo simple es convención, el doble guion bajo se reescribe como _ClassName__attr para evitar choques en subclases, ninguno de los dos es privacidad real. Reserva __attr para clases base con muchas subclases; en el resto de los casos solo suma confusión. Si quieres prohibir del todo los atributos sueltos y detectar el self.scor mal escrito, define __slots__ con la lista fija.

Herencia ​

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

La herencia modela una relación "es-un": un Dog es un Animal. La subclase obtiene todos los métodos y atributos del padre y puede sobrescribir cualquiera de ellos definiendo su propia versión. Cuando llamas a un método, Python busca primero en la subclase y luego sube hasta el padre, así que un método no sobrescrito se resuelve automáticamente por esa vía. Ese orden de búsqueda es el orden de resolución de métodos (MRO), la lista ordenada de clases que Python recorre.

Cuando llamas a obj.method, Python busca en las clases siguiendo un orden fijo, el MRO (method resolution order: la lista aplanada de una clase y sus ancestros, que puedes leer con Dog.__mro__), y usa la primera coincidencia. La herencia simple es directa, subclase y luego padre. La razón por la que existe el MRO es la herencia múltiple y el caso del diamante (dos padres que a su vez heredan de un abuelo común): Python ordena las clases para que el abuelo aparezca una sola vez, después de ambos padres, y resuelve las llamadas de forma determinística. La recomendación práctica: prefiere jerarquías poco profundas y herencia simple, porque los árboles profundos o de herencia múltiple hacen difícil saber "qué método realmente se ejecutó". Cuando de verdad combines varias clases base, haz que cada __init__ pase por super() (siguiente sección) para que cada uno se ejecute exactamente una vez siguiendo ese orden.

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

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

class Dog(Animal):
    def speak(self):
        return f"{self.name} says Woof!"

class Cat(Animal):
    def speak(self):
        return f"{self.name} says Meow!"

pets = [Dog("Rex"), Cat("Luna"), Dog("Max")]
for pet in pets:
    print(pet.speak())

Dog y Cat heredan __init__ de Animal, así que no necesitan el suyo propio. Sobrescriben speak() con su comportamiento específico.

JunoHerencia Una subclase hereda todo lo que tiene el padre, y luego sobrescribe solo los métodos que quiere cambiar. Todo lo que no sobrescribas se resuelve gratis a través del padre, que es justamente la idea: escribir el comportamiento compartido una sola vez y especializarte donde sea distinto. Dog y Cat reutilizan el __init__ de Animal y solo redefinen speak().
JunoHerencia La herencia es "es-un": la subclase obtiene los métodos del padre y sobrescribe lo que necesita, y las llamadas no sobrescritas se resuelven a través del padre. Python encuentra un método recorriendo el MRO, primero la subclase, luego hacia arriba hasta el padre. Reutiliza la base compartida, redefine solo lo que difiere.
JunoHerencia Python resuelve un método recorriendo el MRO y tomando la primera coincidencia, lo cual es justo lo que hace determinístico el caso del diamante. Mantén las jerarquías poco profundas y apóyate en la herencia simple; los árboles profundos o de herencia múltiple convierten "qué método se ejecutó" en un juego de adivinanzas. Cuando debas combinar varias 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 vez de reemplazarlo por completo: llama al __init__ del padre para que ejecute su configuración, y luego agrega lo que tu subclase necesite además de eso.

super() devuelve un objeto proxy que delega las llamadas a métodos hacia la siguiente clase en el MRO. Llama siempre a super().__init__() desde el __init__ de una subclase cuando el padre tiene uno. Si lo omites, el código de configuración del padre no se ejecuta, lo cual puede dejar el objeto en un estado incompleto.

super() no significa "mi padre directo", significa "la siguiente clase en el MRO" (el orden de búsqueda de la sección anterior). En una subclase simple, esa siguiente clase es el padre, así que la distinción parece académica, pero es exactamente lo que hace funcionar la herencia múltiple: si todas las clases del árbol llaman a super().__init__(...), cada __init__ se ejecuta una sola vez, en el orden del MRO, sin que ninguna clase tenga el nombre del padre escrito a mano. Por eso escribes super().__init__() y no Animal.__init__(self): nombrar al padre directamente rompe la cadena en el momento en que se combina otra clase, y puede llegar a ejecutar dos veces una base compartida. El super() sin argumentos es la forma moderna; el super(Dog, self) más antiguo es lo mismo escrito de forma explícita, y aún lo verás en código pensado para versiones anteriores.

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

class Dog(Animal):
    def __init__(self, name):
        super().__init__(name, "Woof")   # llama a Animal.__init__
        self.tricks = []                  # agrega algo extra

    def learn(self, trick):
        self.tricks.append(trick)

rex = Dog("Rex")
rex.learn("sit")
print(rex.tricks)   # ["sit"]

Llama siempre a super().__init__() cuando tu subclase tiene su propio __init__ y el padre también tiene uno.

Junosuper()super() te lleva a la clase padre, así que super().__init__() ejecuta la configuración del padre antes de que agregues la tuya. Úsalo cuando tu subclase escriba su propio __init__ y el padre también tenga uno. Si lo omites, la configuración del padre nunca se ejecuta y el objeto queda a medio construir.
Junosuper()super() delega a la siguiente clase hacia arriba, así que llama a super().__init__() desde el __init__ de una subclase siempre que el padre tenga uno. Si lo olvidas, la configuración del padre se salta, lo cual puede dejar el objeto en un estado incompleto. Es la línea que conecta tu subclase con el padre que extiende.
Junosuper()super() significa "la siguiente clase en el MRO", no "mi padre", por eso escribir a mano Animal.__init__(self) rompe la cadena en cuanto se combina otra clase. Si cada __init__ llama a super(), cada uno se ejecuta una sola vez en orden. El super() sin argumentos es la forma moderna; super(Dog, self) es lo mismo escrito de forma explícita.

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

@classmethod crea un método que recibe la clase en sí en vez de una instancia. Es útil para constructores alternativos: crear una instancia a partir de una cadena, un archivo u otro formato. @staticmethod es una función normal que vive dentro de la clase por razones de organización; no recibe ni la instancia ni la clase.

@classmethod recibe cls (la clase) como su primer argumento, en vez de una instancia. El uso principal son los constructores alternativos que crean instancias a partir de distintos formatos de entrada. @staticmethod es una función normal agrupada dentro de la clase; no tiene acceso ni a la clase ni a la instancia. Usa @classmethod para constructores y @staticmethod para funciones utilitarias lógicamente ligadas a la clase.

@classmethod recibe la clase como su primer argumento (cls) en vez de una instancia, y el detalle que vale la pena tener presente es que cls es la clase sobre la que se llamó al método, no la clase donde se definió. Así que si Player.from_string construye con cls(...) y una subclase ProPlayer llama a ProPlayer.from_string(...), se construye un ProPlayer, no un Player. Por eso los constructores alternativos usan @classmethod en vez de escribir el nombre de la clase a mano: siguen funcionando bajo herencia. @staticmethod no recibe ni la instancia ni la clase; es una función normal alojada dentro de la clase por organización, cuando la lógica pertenece a la clase pero no necesita nada de su información. Recurre a un classmethod cuando estés creando una instancia a partir de otro formato, a un staticmethod para un ayudante relacionado, y a un método normal para todo lo que toque la información de un objeto en particular.

python
class Player:
    def __init__(self, name, score):
        self.name = name
        self.score = score

    @classmethod
    def from_string(cls, data):
        name, score = data.split(",")
        return cls(name, int(score))

alice = Player.from_string("Alice,87")
python
class Player:
    @staticmethod
    def is_valid_name(name):
        return name.isalpha() and len(name) >= 2

Player.is_valid_name("Alice")   # True
Player.is_valid_name("A1")      # False

Usa @classmethod para constructores alternativos. Usa @staticmethod para funciones utilitarias que lógicamente pertenecen a la clase pero no necesitan información de la instancia ni de la clase.

JunoMétodos de clase y métodos estáticos@classmethod te entrega la clase en vez de una instancia, lo cual lo convierte en la opción para constructores alternativos: construir un Player a partir de una cadena, un archivo, cualquier formato que tengas. @staticmethod es una función común guardada dentro de la clase por prolijidad; no recibe ni la clase ni la instancia. Los métodos normales tocan la información de un objeto, estos dos no.
JunoMétodos de clase y métodos estáticos@classmethod recibe cls, la clase misma, así que úsalo para constructores alternativos que devuelvan cls(...). @staticmethod no recibe nada implícito, es un ayudante agrupado dentro de la clase. Método normal para información por objeto, classmethod para construir instancias, staticmethod para un utilitario relacionado.
JunoMétodos de clase y métodos estáticos El punto de un constructor con classmethod es que cls es quien sea que lo llamó, así que ProPlayer.from_string(...) construye un ProPlayer, no un Player. Escribe el nombre de la clase a mano y pierdes eso. Classmethod para construir, staticmethod para un ayudante relacionado que no necesita datos, método normal para todo lo que toque la instancia.

@property ​

@property te permite acceder a un método como si fuera un atributo, sin necesidad de paréntesis. Úsalo para valores que se calculan a partir de otros atributos y se leen naturalmente como un simple acceso a un atributo.

@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 a partir de información almacenada, y para agregar validación al acceso a atributos sin cambiar la interfaz pública. Un @name.setter emparejado hace que el atributo sea escribible.

@property convierte un método en algo que lees como un atributo: c.area, sin paréntesis, ejecuta el método cada vez. Agrega @area.setter y también se vuelve escribible, con tu código ejecutándose al momento de asignar, que es donde va la validación (rechazar un radio negativo antes de que se guarde). El verdadero valor está en que esto no cambia nada para quien usa la clase: puedes empezar con un atributo simple self.radius y más adelante convertirlo en una property con validación sin tocar una sola línea que lo lea o lo escriba. Por eso Python no tiene la ceremonia de getters/setters, expones atributos simples y recurres a una property solo cuando uno necesita cálculo o protección. Dos advertencias para producción: el acceso parece gratis pero ejecuta código, así que mantenlo liviano (guarda en caché un resultado costoso en vez de recalcularlo en cada lectura), y una property que en silencio hace trabajo pesado o lanza un error sorprende a quien asumió que era un simple campo.

python
class Circle:
    def __init__(self, radius):
        self.radius = radius

    @property
    def area(self):
        return 3.14159 * self.radius ** 2

    @property
    def diameter(self):
        return self.radius * 2

c = Circle(5)
print(c.area)      # 78.53975 (se ve como un atributo, funciona como un método)
print(c.diameter)  # 10

Las properties son útiles para valores calculados: cosas derivadas de otros atributos y que se leen naturalmente sin ().

Juno@property@property te permite leer un método como si fuera un atributo, sin paréntesis: c.area en vez de c.area(). Le queda bien a valores que se calculan a partir de otros atributos y se leen naturalmente como datos simples. Detrás de escena, sigue ejecutando tu método cada vez que accedes.
Juno@property@property lee un método como si fuera un atributo, y un @name.setter emparejado lo hace escribible para que puedas validar al momento de asignar. La ganancia: puedes convertir un atributo simple en una property más adelante sin cambiar ningún código que lo use. Úsalo para valores calculados o protegidos, no como un hábito en cada campo.
Juno@property La razón para preferir @property: un atributo simple puede convertirse más adelante en uno calculado o validado sin ningún cambio para quien lo usa, así que te saltas los getters y setters hasta que realmente necesites uno. El acceso parece gratis pero ejecuta código, así que mantenlo liviano, guarda en caché los costosos, y no escondas trabajo pesado o un error detrás de lo que se lee como un simple campo.

En la práctica ​

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

python
class Player:
    max_lives = 3

    def __init__(self, name: str):
        self.name = name
        self.score = 0
        self.lives = Player.max_lives

    def earn_points(self, amount: int) -> None:
        self.score += amount

    def take_hit(self) -> bool:
        self.lives -= 1
        return self.lives > 0

    @property
    def is_alive(self) -> bool:
        return self.lives > 0

    def __str__(self) -> str:
        return f"{self.name} | Score: {self.score} | Lives: {self.lives}"

alice = Player("Alice")
alice.earn_points(50)
alice.take_hit()
print(alice)            # "Alice | Score: 50 | Lives: 2"
print(alice.is_alive)   # True

Una clase User que usa un atributo privado con un getter @property, un método deactivate y un serializador to_dict:

python
class User:
    def __init__(self, user_id: int, username: str, email: str):
        self.id = user_id
        self.username = username
        self.email = email
        self._active = True

    @property
    def active(self) -> bool:
        return self._active

    def deactivate(self) -> None:
        self._active = False

    def to_dict(self) -> dict:
        return {
            "id": self.id,
            "username": self.username,
            "email": self.email,
            "active": self._active,
        }

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

alice = User(1, "alice", "[email protected]")
print(alice.to_dict())
alice.deactivate()
print(alice.active)   # False

Una clase DataSplit que encapsula la división entre entrenamiento y validación detrás de properties, con __repr__ para una salida de depuración limpia:

python
class DataSplit:
    def __init__(self, data: list, train_ratio: float = 0.8):
        split = int(len(data) * train_ratio)
        self._train = data[:split]
        self._val = data[split:]

    @property
    def train(self) -> list:
        return self._train

    @property
    def val(self) -> list:
        return self._val

    @property
    def sizes(self) -> tuple[int, int]:
        return len(self._train), len(self._val)

    def __repr__(self) -> str:
        return f"DataSplit(train={len(self._train)}, val={len(self._val)})"

data = list(range(100))
split = DataSplit(data, train_ratio=0.8)
print(split)         # DataSplit(train=80, val=20)
print(split.sizes)   # (80, 20)

El prefijo de guion bajo en _train y _val indica que quien use la clase debería pasar por las properties en vez de mutar directamente las listas en crudo. Python no impone esto, pero establece un contrato claro.