Saltar al contenido principal

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)
  • 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”:

  1. Instancia → acciones que afectan a un objeto concreto (un usuario, una factura, un producto…).
  2. 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…).
  3. Estáticos → funciones auxiliares que solo necesitan recibir datos normales (calcular daño, validar nombres, etc.).
Tipo de métodoPrimer parámetroPuede usar atributos de instancia (self.algo)Puede usar atributos de clase (Clase.algo o cls.algo)Se usa para…
Instanciaself✅✅ (a través de la clase)Acciones sobre un objeto concreto
Clasecls❌ (no directamente)✅Lógica global de la clase o constructores extra
Estáticoninguno 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​

Método de instancia en un jugador
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()
en este ejemplo
  • Todos los métodos usan self para 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​

Contador de instancias con atributo y método de clase
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
en este ejemplo
  • total_personajes no pertenece a un personaje concreto, sino a la clase en general.
  • Cada vez que se crea un personaje, se incrementa el contador.
  • obtener_total_personajes es un método de clase porque no tiene sentido asociarlo a un solo personaje.

🟨 Ejemplo: constructor alternativo para personajes iniciales​

Constructor alternativo con @classmethod
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
en este ejemplo

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​

Métodos estáticos como funciones de apoyo en un juego
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
en este ejemplo
  • Ninguna de estas funciones necesita información de un objeto ni de la clase.

  • Podrian estar fuera de la clase, pero agruparlas en CalculadoraCombate ayuda a:

    • entender que estan relacionadas con el combate,
    • no llenar el modulo con funciones sueltas.

🟨 Ejemplo integrado: instancia + clase + estático en un personaje​

Ejemplo con método de instancia, de clase y estático
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
en este ejemplo
  • Método de instancia (recibir_danio, curar) → usa self y modifica self.vida.
  • Métodos de clase (crear_guerrero_basico, obtener_total_personajes) → usan cls y acceden a cls.total_personajes.
  • Método estático (es_nombre_valido) → no usa ni self ni cls; solo trabaja con el parámetro nombre.

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étodoSe usa cuando...Ejemplo de código de usuario
__init__Se crea una instanciaobj = Clase(...)
__str__Se convierte a texto legibleprint(obj)
__repr__Se muestra en consola / depuraciónrepr(obj)
__len__Se pide la longitudlen(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 inelem in obj
__getitem__Se accede por índice o claveobj[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 para print(objeto) o str(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 en repr(objeto) y en la consola interactiva.

Ejemplo de __str__ y __repr__ con objetos del juego
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)
en este ejemplo
  • __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.

Uso de __len__ en un inventario
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.
Uso de __add__ con posiciones en el mapa
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)
en este ejemplo

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.

Uso de __eq__ y __lt__ para comparar y ordenar personajes
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)
info

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)
Uso de __contains__ en un inventario de juego
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].
Uso de __getitem__ en un inventario
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
en este ejemplo

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__: Inicializa nombre, elemento, potencia y id_hechizo.
  • Método __str__: Devuelve una descripción amigable (ej: "Llamarada [Fuego] - Potencia 50").
  • Método __repr__: Devuelve una representación técnica clara.
Ejemplo de salida Fase 1
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 usar len(biblioteca) para saber cuántos hechizos contiene.
  • Método __contains__: Permite usar if "Nombre" in biblioteca: para verificar si un hechizo existe.
Ejemplo de salida Fase 2
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​

  1. Atributo de clase: total_hechizos_creados en la clase Hechizo.
  2. Método de clase (@classmethod): obtener_total() para consultar el contador global de pergaminos creados.
  3. 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​

  1. 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.
  2. Método __lt__: Permite comparar si un hechizo es menos potente que otro para poder ordenar listas de hechizos automáticamente.
  3. Método __eq__: Dos hechizos son iguales si tienen la misma potencia y elemento.
Ejemplo de salida Fase 4
h1 = Hechizo("Rayo", "Aire", 30)
h2 = Hechizo("Vendaval", "Aire", 40)
fusion = h1 + h2
print(fusion.potencia)
# 70
print(h1 < h2)
# True