6.3 Métodos de clase, estáticos y especiales
Hasta ahora se han utilizado sobre todo m étodos de instancia, es decir, funciones que trabajan con un objeto concreto y reciben self como primer parámetro.
Sin embargo, en Python también se puede trabajar:
- A nivel de clase completa (no de un solo objeto), usando:
- atributos de clase
- métodos de clase (
@classmethod)
- Con funciones de apoyo relacionadas con la clase, pero que no necesitan ni objeto ni clase, usando:
- métodos estáticos (
@staticmethod)
- métodos estáticos (
- Y con métodos especiales (o dunder methods) que permiten integrar nuestros objetos con operadores y funciones integradas del lenguaje.
Comprender esta diferencia permite diseñar clases más ordenadas.
1️⃣ Visión general de los tipos de métodos
Podemos pensar en tres “niveles”:
- Instancia → acciones que afectan a un objeto concreto (un usuario, una factura, un producto…).
- Clase → acciones que afectan a todas las instancias o a la idea general de la clase (total de instancias creadas, configuración global del programa…).
- Estáticos → funciones auxiliares que solo necesitan recibir datos normales (calcular daño, validar nombres, etc.).
| Tipo de método | Primer parámetro | Puede usar atributos de instancia (self.algo) | Puede usar atributos de clase (Clase.algo o cls.algo) | Se usa para… |
|---|---|---|---|---|
| Instancia | self | ✅ | ✅ (a través de la clase) | Acciones sobre un objeto concreto |
| Clase | cls | ❌ (no directamente) | ✅ | Lógica global de la clase o constructores extra |
| Estático | ninguno especial | ❌ | ❌ (no directamente) | Funciones de apoyo relacionadas con la clase |
A continuación se detalla cada tipo por separado: definición, usos tipicos y ejemplos.
2️⃣ Métodos de instancia
Los métodos de instancia son los más habituales. Se definen dentro de la clase y reciben self como primer parámetro. self representa al objeto concreto sobre el que se esta trabajando.
def nombre_metodo(self, ...):
...
Se usan cuando el método necesita:
- Leer o modificar atributos del objeto (
self.vida,self.nivel,self.inventario, etc.). - Representar una acción que realiza esa instancia (atacar, curarse, subir de nivel…).
🟩 Cuando se usan
Usa un método de instancia cuando:
- La lógica depende del estado del objeto (sus puntos de vida, su nivel, su posición…).
- Se necesita acceder o cambiar atributos de instancia.
- La acción describe algo que “hace” el objeto (un personaje que ataca, un enemigo que se mueve, un cofre que se abre…).
🟦 Ejemplo: jugador que sube de nivel
class Jugador:
def __init__(self, nombre, nivel=1):
self.nombre = nombre
self.nivel = nivel
self.puntos_experiencia = 0
def ganar_experiencia(self, cantidad):
"""Suma puntos de experiencia al jugador."""
self.puntos_experiencia += cantidad
def subir_nivel(self):
"""Sube el nivel del jugador si tiene suficientes puntos de experiencia."""
if self.puntos_experiencia >= 100:
self.nivel += 1
self.puntos_experiencia -= 100
print(f"{self.nombre} ha subido al nivel {self.nivel}")
else:
print("Todavia no tienes suficiente experiencia.")
def mostrar_estado(self):
"""Muestra la informacion del jugador."""
print(
f"{self.nombre} - Nivel: {self.nivel} - PX: {self.puntos_experiencia}"
)
j = Jugador("Ana")
j.mostrar_estado()
j.ganar_experiencia(120)
j.subir_nivel()
j.mostrar_estado()
- Todos los métodos usan
selfpara leer y modificar el estado del jugador. - Las acciones (
ganar_experiencia,subir_nivel,mostrar_estado) tienen sentido para cada jugador concreto.
3️⃣ Métodos de clase (@classmethod)
Un método de clase es un método que no trabaja con una instancia concreta, sino con la clase en general.
Se define con el decorador @classmethod y recibe cls como primer parámetro en lugar de self.
@classmethod
def nombre_metodo(cls, ...):
...
cls representa a la propia clase (por ejemplo, Personaje, Enemigo, Partida…), y se usa para:
- Acceder a atributos de clase (compartidos por todos los objetos).
- Crear constructores alternativos (formas especiales de crear personajes, enemigos, etc.).
- Realizar operaciones que afectan al “conjunto” de objetos, no a uno solo.
🟩 Cuando se usan
Usa un método de clase cuando:
- La lógica que quieres implementar no depende de un objeto concreto, sino de la clase completa.
- Necesitas trabajar con atributos de clase (contadores, configuración global…).
- Quieres proporcionar una forma alternativa de crear instancias (por ejemplo, personajes iniciales, enemigos de un nivel, etc.).
🟦 Ejemplo: contador de personajes creados
class PersonajeJuego:
total_personajes = 0 # atributo de clase (compartido)
def __init__(self, nombre, nivel=1):
self.nombre = nombre # atributo de instancia
self.nivel = nivel
PersonajeJuego.total_personajes += 1
@classmethod
def obtener_total_personajes(cls):
"""Devuelve cuantos personajes se han creado en total."""
return cls.total_personajes
p1 = PersonajeJuego("Guerrero")
p2 = PersonajeJuego("Mago", nivel=3)
p3 = PersonajeJuego("Arquera", nivel=2)
print(PersonajeJuego.obtener_total_personajes()) # 3
total_personajesno pertenece a un personaje concreto, sino a la clase en general.- Cada vez que se crea un personaje, se incrementa el contador.
obtener_total_personajeses un método de clase porque no tiene sentido asociarlo a un solo personaje.
🟨 Ejemplo: constructor alternativo para personajes iniciales
class PersonajeJuego:
total_personajes = 0
def __init__(self, nombre, nivel, vida, mana):
self.nombre = nombre
self.nivel = nivel
self.vida = vida
self.mana = mana
PersonajeJuego.total_personajes += 1
@classmethod
def crear_personaje_inicial(cls, nombre):
"""
Crea un personaje inicial con valores por defecto.
"""
return cls(nombre, nivel=1, vida=100, mana=50)
@classmethod
def obtener_total_personajes(cls):
return cls.total_personajes
p_inicial = PersonajeJuego.crear_personaje_inicial("Luchador")
print(p_inicial.nivel, p_inicial.vida, p_inicial.mana) # 1 100 50
print(PersonajeJuego.obtener_total_personajes()) # 1
crear_personaje_inicial actúa como forma alternativa de crear personajes, reutilizando el constructor principal (cls(...)).
4️⃣ Métodos estáticos (@staticmethod)
Un método estático es una función definida dentro de una clase que no recibe ni self ni cls de forma automatica.
Se define con el decorador @staticmethod.
@staticmethod
def nombre_metodo(...):
...
Se comporta como una función normal, pero “pegada” a la clase por organización:
- No puede acceder directamente a atributos de instancia ni de clase (salvo que se le pasen como parámetros).
- Se usa para agrupar funciones de apoyo relacionadas con la clase (por ejemplo, calcular daño, validar nombres, comprobar rangos…).
🟩 Cuando se usan
Usa un método estático cuando:
- La función no necesita acceder ni al objeto (
self) ni a la clase (cls). - La lógica solo usa los parámetros que se le pasan.
- Quieres mantener esa función cerca de la clase porque tiene relacion con ella (validaciones, conversiones, calculos auxiliares…).
🟦 Ejemplo: utilidades de combate
class CalculadoraCombate:
@staticmethod
def calcular_danio(base, multiplicador_critico=1.0):
"""Calcula el daño final aplicando un multiplicador critico."""
return int(base * multiplicador_critico)
@staticmethod
def es_critico(probabilidad):
"""
Decide si un ataque es critico.
(En un juego real se usaria random, aqui simplificamos).
"""
return probabilidad >= 1.0 # ejemplo trivial
print(CalculadoraCombate.calcular_danio(50)) # 50
print(CalculadoraCombate.calcular_danio(50, 1.5)) # 75
print(CalculadoraCombate.es_critico(1.0)) # True
-
Ninguna de estas funciones necesita información de un objeto ni de la clase.
-
Podrian estar fuera de la clase, pero agruparlas en
CalculadoraCombateayuda a:- entender que estan relacionadas con el combate,
- no llenar el modulo con funciones sueltas.
🟨 Ejemplo integrado: instancia + clase + estático en un personaje
class Personaje:
# Atributo de clase: compartido por todos los personajes
total_personajes = 0
def __init__(self, nombre, rol, nivel=1, vida=100):
# Atributos de instancia
self.nombre = nombre
self.rol = rol # por ejemplo: "guerrero", "mago"
self.nivel = nivel
self.vida = vida
Personaje.total_personajes += 1
# Metodo de instancia: trabaja con self
def recibir_danio(self, cantidad):
"""Resta puntos de vida al personaje."""
if cantidad < 0:
return
self.vida = max(0, self.vida - cantidad)
print(f"{self.nombre} recibe {cantidad} puntos de daño. Vida: {self.vida}")
def curar(self, cantidad):
"""Cura al personaje."""
if cantidad > 0:
self.vida += cantidad
print(f"{self.nombre} se cura {cantidad} puntos. Vida: {self.vida}")
# Metodos de clase: trabajan con cls y la clase en general
@classmethod
def crear_guerrero_basico(cls, nombre):
"""Crea un guerrero con valores por defecto."""
return cls(nombre, rol="guerrero", nivel=1, vida=120)
@classmethod
def obtener_total_personajes(cls):
"""Devuelve cuantas instancias de Personaje se han creado."""
return cls.total_personajes
# Metodo estatico: utilidad que no necesita ni self ni cls
@staticmethod
def es_nombre_valido(nombre):
"""Comprueba si un nombre es valido segun reglas sencillas."""
return len(nombre.strip()) >= 3
# -------- Uso de la clase --------
# Comprobar si un nombre es valido (metodo estatico)
print(Personaje.es_nombre_valido("Li")) # False
print(Personaje.es_nombre_valido("Arthas")) # True
# Crear personajes
p1 = Personaje.crear_guerrero_basico("Arthas") # metodo de clase
p2 = Personaje("Jaina", rol="mago", nivel=2, vida=80)
# Usar metodos de instancia
p1.recibir_danio(30)
p2.curar(20)
# Consultar el total de personajes creados (metodo de clase)
print(Personaje.obtener_total_personajes()) # 2
- Método de instancia (
recibir_danio,curar) → usaselfy modificaself.vida. - Métodos de clase (
crear_guerrero_basico,obtener_total_personajes) → usanclsy acceden acls.total_personajes. - Método estático (
es_nombre_valido) → no usa niselfnicls; solo trabaja con el parámetronombre.
5️⃣ Métodos especiales
Además de los métodos “normales” (instancia, clase y estáticos), Python proporciona una serie de métodos especiales, llamados también métodos magicos o dunder methods (de double underscore), porque su nombre empieza y termina con doble guion bajo:
__init__,__str__,__len__,__add__,__eq__,__lt__,__contains__, etc.
Estos métodos permiten que los objetos:
- Se integren mejor con el propio lenguaje.
- Definan su comportamiento con operadores (
+,==,<,in, …). - Funcionen con funciones integradas (
len(),str(),repr(), etc.).
Normalmente no se llaman directamente, sino que es Python quien los invoca internamente cuando se usan ciertos operadores o funciones.
🟩 Principales métodos especiales
| Método | Se usa cuando... | Ejemplo de código de usuario |
|---|---|---|
__init__ | Se crea una instancia | obj = Clase(...) |
__str__ | Se convierte a texto legible | print(obj) |
__repr__ | Se muestra en consola / depuración | repr(obj) |
__len__ | Se pide la longitud | len(obj) |
__add__ | Se aplica el operador + | obj1 + obj2 |
__eq__ | Se compara con == | obj1 == obj2 |
__lt__ | Se compara con < | obj1 < obj2 |
__contains__ | Se usa el operador in | elem in obj |
__getitem__ | Se accede por índice o clave | obj[i] |
🟦 Representación de texto: __str__ y __repr__
Estos dos métodos especiales sirven para controlar como se ve un objeto convertido a texto.
-
__str__(self)
Devuelve una cadena amigable para la persona jugadora, pensada paraprint(objeto)ostr(objeto).
Suele ser una descripción corta y legible (por ejemplo, como se veria un objeto en el inventario). -
__repr__(self)
Devuelve una cadena pensada para desarrolladores y depuración: se intenta que sea lo más precisa posible, idealmente algo que se parezca a un “constructor” del objeto.
Se usa enrepr(objeto)y en la consola interactiva.
class ObjetoInventario:
def __init__(self, nombre, tipo, valor):
self.nombre = nombre
self.tipo = tipo # por ejemplo: "arma", "pocion", "armadura"
self.valor = valor # valor en monedas
def __str__(self):
# Representacion pensada para la interfaz del juego
return f"{self.nombre} ({self.tipo}) - Valor: {self.valor} monedas"
def __repr__(self):
# Representacion tecnica para depuracion
return (
f"ObjetoInventario(nombre={self.nombre!r}, "
f"tipo={self.tipo!r}, valor={self.valor!r})"
)
espada = ObjetoInventario("Espada de hierro", "arma", 150)
print(espada) # Espada de hierro (arma) - Valor: 150 monedas
print(repr(espada)) # ObjetoInventario(nombre='Espada de hierro', tipo='arma', valor=150)
__str__→ “como quiero que lo vea la persona jugadora en pantalla”.__repr__→ “como quiero verlo yo cuando estoy depurando el juego”.
🟨 Longitud: __len__
El método __len__(self) permite que len(objeto) funcione con objetos de nuestra propia clase.
En un programa se usa, por ejemplo, cuando una clase representa una colección: un inventario, un equipo de personajes, una lista de misiones activas, etc.
class Inventario:
def __init__(self):
self.objetos = []
def anadir_objeto(self, objeto):
self.objetos.append(objeto)
def __len__(self):
# Delegamos en len() de la lista interna
return len(self.objetos)
inv = Inventario()
inv.anadir_objeto(ObjetoInventario("Pocion pequena", "pocion", 25))
inv.anadir_objeto(ObjetoInventario("Escudo de madera", "armadura", 80))
print(len(inv)) # 2
🟧 Operador suma: __add__
El método __add__(self, otro) define que ocurre cuando se usa el operador + entre dos objetos de nuestra clase.
En programa, se puede usar para:
- Sumar posiciones en el mapa (vectores de movimiento).
- Combinar atributos (por ejemplo, daño base + bonificacion).
- Unir inventarios o listas de misiones, etc.
class Posicion:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, otra):
# Suma componente a componente: avanzar en el mapa
return Posicion(self.x + otra.x, self.y + otra.y)
def __repr__(self):
return f"Posicion(x={self.x}, y={self.y})"
pos_jugador = Posicion(10, 5)
desplazamiento = Posicion(2, -1)
nueva_posicion = pos_jugador + desplazamiento
print(nueva_posicion) # Posicion(x=12, y=4)
Al escribir pos_jugador + desplazamiento, Python traduce internamente a:
Posicion.__add__(pos_jugador, desplazamiento)
🟥 Comparaciones: __eq__ y __lt__ (y ordenar objetos)
Estos métodos definen como se comparan los objetos de una clase.
__eq__(self, otro)→ se llama cuando se usa el operador==.__lt__(self, otro)→ se llama cuando se usa el operador<(menor que).
En programas son muy útiles para:
- Comparar personajes por nivel o puntos de vida.
- Ordenar una lista de jugadores por puntuación.
- Ordenar misiones por dificultad.
Cuando Python necesita ordenar una lista de objetos (por ejemplo, con sorted(lista) o lista.sort()), intenta usar los métodos de comparación, en especial __lt__.
Si nuestra clase implementa __lt__, se podrán ordenar listas de esos objetos sin necesidad de indicar una clave.
class Personaje:
def __init__(self, nombre, nivel, experiencia):
self.nombre = nombre
self.nivel = nivel
self.experiencia = experiencia
def __eq__(self, otro):
# Consideramos que dos personajes son "equivalentes" si tienen el mismo nivel
return self.nivel == otro.nivel
def __lt__(self, otro):
# Ordenamos por nivel y, en caso de empate, por experiencia
if self.nivel == otro.nivel:
return self.experiencia < otro.experiencia
return self.nivel < otro.nivel
def __repr__(self):
return (
f"Personaje({self.nombre!r}, nivel={self.nivel}, "
f"experiencia={self.experiencia})"
)
p1 = Personaje("Guerrero", 5, 120)
p2 = Personaje("Mago", 3, 250)
p3 = Personaje("Arquera", 5, 90)
print(p1 == p3) # True (mismo nivel)
print(p2 < p1) # True (nivel 3 < nivel 5)
# Gracias a __lt__, se puede ordenar directamente una lista de personajes:
grupo = [p1, p2, p3]
grupo_ordenado = sorted(grupo) # usa Personaje.__lt__ por debajo
print(grupo_ordenado)
# [Personaje('Mago', nivel=3, experiencia=250),
# Personaje('Arquera', nivel=5, experiencia=90),
# Personaje('Guerrero', nivel=5, experiencia=120)]
También existen otros métodos de comparación:
__le__(<=),__gt__(>),__ge__(>=),__ne__(!=)
Pero muchas veces basta con definir __eq__ y __lt__ y, si hace falta, apoyarse en funciones como:
sorted(grupo, key=lambda p: p.nivel)
Si quieres poder ordenar objetos de tu juego sin usar key=, lo normal es implementar al menos __lt__ (y, si tiene sentido, __eq__).
🟪 Pertenencia: __contains__
El método __contains__(self, item) permite usar el operador in con objetos personalizados, del mismo modo que se usa con listas, cadenas, diccionarios, etc.
En el contexto del juego es muy natural para:
- Comprobar si un objeto esta en el inventario:
if espada in inventario: - Ver si una misión esta activa.
- Comprobar si un jugador esta en un equipo o sala.
Cuando escribes:
objeto in inventario
Python llama internamente a:
inventario.__contains__(objeto)
class Inventario:
def __init__(self):
self.objetos = []
def anadir_objeto(self, objeto):
self.objetos.append(objeto)
def __contains__(self, objeto):
# Se considera que un objeto "esta" si coincide por nombre
return any(o.nombre == objeto.nombre for o in self.objetos)
def __repr__(self):
return f"Inventario({self.objetos!r})"
inv = Inventario()
espada = ObjetoInventario("Espada de hierro", "arma", 150)
pocion = ObjetoInventario("Pocion pequena", "pocion", 25)
inv.anadir_objeto(espada)
print(espada in inv) # True
print(pocion in inv) # False
Este método es útil cuando:
- La clase representa una colección y queremos una sintaxis natural del estilo:
if objeto in inventario: ... - Se quiere aplicar una lógica de pertenencia especial (por ejemplo, comparar por nombre, id, tipo, etc., en lugar de usar igualdad directa de objetos.
🟫 Acceso a elementos: __getitem__
El método __getitem__(self, indice) permite que los objetos de una clase se comporten como secuencias (listas, tuplas) o diccionarios, pudiendo acceder a sus elementos mediante corchetes: objeto[indice].
En un juego, este método es fundamental para permitir:
- Acceder a un objeto del inventario por su posición:
inventario[0]. - Obtener un objeto por su nombre (si implementamos lógica de diccionario):
equipo["Mago"]. - Acceder a casillas de un tablero de juego:
tablero[x, y].
class Inventario:
def __init__(self):
self.objetos = []
def anadir_objeto(self, objeto):
self.objetos.append(objeto)
def __getitem__(self, indice):
# Delegamos el acceso a la lista interna de objetos
# Esto permite usar indices positivos y negativos (como -1)
return self.objetos[indice]
def __len__(self):
return len(self.objetos)
inv = Inventario()
inv.anadir_objeto("Espada de hierro")
inv.anadir_objeto("Pocion de vida")
print(inv[0]) # Espada de hierro
print(inv[-1]) # Pocion de vida
Al definir __getitem__, las instancias de tu clase se vuelven automáticamente iterables.
Python intentará acceder al índice 0, luego al 1, etc., lo que permite usar un bucle for directamente sobre el objeto: for objeto in inv: ...
⚔️ Actividad Práctica: El Gran Almacén de los Hechizos
En esta actividad practicaremos los métodos especiales (dunder), así como los métodos de clase y estáticos, creando un sistema para gestionar una biblioteca de pergaminos mágicos. Esta actividad integra todos los conocimientos del punto 6.3.
📜 1. Pergaminos de Poder (__init__, __str__, __repr__)
Cada hechizo es una entidad única con un nombre, un elemento (fuego, agua, aire, tierra) y un nivel de potencia.
Archivo: hechizo.py
📋 Tarea a realizar
Crea la clase Hechizo con:
- Método
__init__: Inicializanombre,elemento,potenciayid_hechizo. - Método
__str__: Devuelve una descripción amigable (ej: "Llamarada [Fuego] - Potencia 50"). - Método
__repr__: Devuelve una representación técnica clara.
h = Hechizo("Bola de Fuego", "Fuego", 50)
print(h)
# Bola de Fuego [Fuego] - Potencia 50
repr(h)
# Hechizo(nombre='Bola de Fuego', elemento='Fuego', potencia=50)
📚 2. La Biblioteca Arcana (__len__, __contains__)
Los hechizos se guardan en una biblioteca central. Necesitamos saber cuántos hay y si un hechizo concreto ya está registrado por su nombre utilizando el operador in.
📋 Tarea a realizar
Crea la clase Biblioteca que guarde una lista interna de hechizos:
- Método
__len__: Permite usarlen(biblioteca)para saber cuántos hechizos contiene. - Método
__contains__: Permite usarif "Nombre" in biblioteca:para verificar si un hechizo existe.
b = Biblioteca()
b.añadir(h)
print(len(b))
# 1
print("Bola de Fuego" in b)
# True
🏛️ 3. Gestión del Gremio (@classmethod, @staticmethod)
El Gremio de Magos supervisa todas las bibliotecas y hechizos globalmente.
📋 Tarea a realizar
- Atributo de clase:
total_hechizos_creadosen la claseHechizo. - Método de clase (
@classmethod):obtener_total()para consultar el contador global de pergaminos creados. - Método estático (
@staticmethod):es_elemento_valido(elemento)que verifique si el elemento pertenece a la lista: fuego, agua, aire, tierra.
💥 4. Fusión y Duelos (__add__, __lt__, __eq__)
Los magos pueden combinar hechizos y compararlos para saber cuál es más poderoso en un duelo.
📋 Tarea a realizar
- Método
__add__: Permite sumar dos hechizos. El resultado es un nuevo hechizo llamado "Fusión de [Nombre1] y [Nombre2]" con la suma de sus potencias y el elemento del primero. - Método
__lt__: Permite comparar si un hechizo es menos potente que otro para poder ordenar listas de hechizos automáticamente. - Método
__eq__: Dos hechizos son iguales si tienen la misma potencia y elemento.
h1 = Hechizo("Rayo", "Aire", 30)
h2 = Hechizo("Vendaval", "Aire", 40)
fusion = h1 + h2
print(fusion.potencia)
# 70
print(h1 < h2)
# True