06 — Clases y objetos: el molde y el Pokémon concreto¶
Por qué importa¶
Hasta ahora, cuando querías representar un Pokémon, probablemente habrías usado un diccionario suelto: {"name": "Pikachu", "hp": 35, "attack": 55}. Funciona para uno. Pero en una batalla tienes dos Pokémon, cada uno con cuatro movimientos, y a cada uno le baja la vida en cada turno. Con diccionarios sueltos, la lógica de "bajar vida" vive desperdigada por todo el código: una resta aquí, una comprobación de "¿se debilitó?" allá. Cada vez que tocas un Pokémon tienes que recordar las reglas tú mismo.
Una clase resuelve esto juntando dos cosas que siempre van de la mano: los datos de un Pokémon (su nombre, su vida, su ataque) y las operaciones que se le pueden hacer (recibir daño, comprobar si está debilitado). En vez de "un diccionario más una pila de funciones que esperan que el diccionario tenga las claves correctas", tienes un objeto que sabe qué es y qué se le puede pedir.
Este es el doc base de todo el motor de batalla. Move, Pokemon y más adelante Battle son clases. Si este modelo mental queda firme, el resto es repetición.
Schema / Modelo mental¶
La distinción central, antes que cualquier sintaxis:
CLASE (el molde) OBJETOS / INSTANCIAS (lo fabricado)
┌───────────────────────┐
│ class Pokemon │ ──fabrica──► ┌──────────────────────────┐
│ │ │ pikachu │
│ "todo Pokémon TIENE:" │ │ name = "Pikachu" │
│ - name │ │ type = "Electric" │
│ - type │ │ max_hp = 35 │
│ - max_hp │ │ hp = 35 │
│ - hp │ │ attack = 55 │
│ - attack │ └──────────────────────────┘
│ - defense │
│ - level │ ──fabrica──► ┌──────────────────────────┐
│ │ │ charmander │
│ "todo Pokémon SABE:" │ │ name = "Charmander" │
│ - take_damage(n) │ │ type = "Fire" │
│ - is_fainted() │ │ max_hp = 39 │
│ │ │ hp = 39 │
└───────────────────────┘ │ attack = 52 │
└──────────────────────────┘
Una sola clase Pokemon. Muchos objetos Pokemon: pikachu, charmander, squirtle... Cada objeto es independiente: si a pikachu le bajas la vida, charmander ni se entera. Comparten el molde (las mismas reglas, los mismos métodos), pero no los datos.
Frases para fijar el modelo:
- La clase es el plano de la casa. El objeto es una casa construida.
- La clase es la receta. El objeto es el plato que te comes.
class Pokemonse escribe una vez. ObjetosPokemonse crean diez veces (uno por cada especie endata.py).
Atributos: qué tiene un Pokémon¶
Un atributo es un dato que pertenece a un objeto. pikachu.name es el atributo name del objeto pikachu. Se accede con un punto: objeto.atributo.
pikachu.name # "Pikachu" ── leer
pikachu.hp # 35 ── leer
pikachu.hp = 20 # ── escribir (modificar el objeto)
Antes de saber crearlos, ten claro qué atributos tendrá cada Pokemon según el CANON del proyecto:
| Atributo | Tipo | Significado |
|---|---|---|
name |
str |
"Pikachu" |
type |
str |
uno de los 6 tipos (ver más abajo) |
max_hp |
int |
vida máxima, no cambia |
hp |
int |
vida actual, baja durante el combate |
attack |
int |
potencia ofensiva |
defense |
int |
reduce el daño recibido |
level |
int |
nivel del Pokémon |
moves |
list[Move] |
sus movimientos (objetos Move) |
Fíjate en algo clave: max_hp y hp empiezan iguales (un Pikachu recién creado tiene 35 de 35), pero hp está pensado para cambiar y max_hp para quedarse fijo. Esa diferencia es el corazón del estado mutable, lo vemos al final.
__init__: cómo nace un objeto¶
Cuando escribes Pokemon("Pikachu", "Electric", 35, 55, 40, 5, [...]), Python crea un objeto vacío y acto seguido llama a un método especial llamado __init__ para rellenarlo. __init__ es el constructor: su único trabajo es poner los atributos iniciales en el objeto recién nacido.
No te enseño todavía cómo se escribe el de Pokemon (eso es tu práctica). Te lo enseño con Move, que es más pequeño y sirve de plantilla mental. Aquí está la clase Move completa:
class Move:
def __init__(self, name, type, power, accuracy, pp, max_pp):
self.name = name
self.type = type
self.power = power
self.accuracy = accuracy
self.pp = pp
self.max_pp = max_pp
def use(self):
self.pp -= 1
def has_pp(self):
return self.pp > 0
Léelo despacio. __init__ recibe seis datos y los guarda en el objeto. self.name = name significa: "el atributo name de este objeto vale lo que me pasaron en el parámetro name". A la izquierda del = está el objeto; a la derecha, el dato de entrada. Es normal que el parámetro y el atributo se llamen igual; self. desambigua.
self: la pieza que cuesta (vamos muy despacio)¶
self es el concepto que hace tropezar a todo el mundo. Tómatelo con calma.
Mira esta llamada:
Lees: "al objeto pikachu, aplícale take_damage con 10". Pero el método está definido así (firma de ejemplo, no te doy el cuerpo: es tu práctica):
take_damage tiene dos parámetros (self, amount) pero tú lo llamaste con uno (10). ¿De dónde sale self? Python lo rellena automáticamente con el objeto que está a la izquierda del punto. Mentalmente, traduce siempre así:
pikachu.take_damage(10)
└───── es lo mismo que ─────┐
▼
Pokemon.take_damage(pikachu, 10)
▲ ▲
│ └── amount = 10
└── self = pikachu
self es el propio objeto sobre el que llamaste el método. Dentro de take_damage, self es pikachu. Por eso self.hp dentro del método significa "el hp de pikachu". Si en otro turno llamas charmander.take_damage(10), en esa llamada self es charmander, y self.hp es el de Charmander. El mismo código de método, distinto objeto cada vez, gracias a self.
Regla práctica que no falla: el primer parámetro de todo método es self, siempre, y nunca lo pasas tú al llamar — Python lo pone solo desde lo que hay antes del punto.
Métodos: qué sabe hacer un Pokémon¶
Un método es una función que vive dentro de una clase y opera sobre el objeto vía self. Ya viste tres en Move:
use(self): gasta un punto de poder.self.pp -= 1resta 1 alppde ese movimiento.has_pp(self): devuelveTruesi todavía quedan usos.return self.pp > 0evalúa una comparación y devuelve el booleano resultante.
Para Pokemon, el CANON pide dos métodos. Te muestro uno entero como ejemplo guía y dejo el otro descrito (para que no se resuelva tu práctica):
class Pokemon:
def __init__(self, name, type, max_hp, attack, defense, level, moves):
... # tu práctica
def is_fainted(self):
return self.hp <= 0
is_fainted no recibe nada aparte de self: solo mira el estado interno (self.hp) y responde una pregunta sí/no. "¿Este Pokémon está debilitado?" → True si su vida llegó a 0 o menos.
El otro método, take_damage(self, amount), debe bajar la vida del Pokémon en función del daño recibido. Modifica self.hp. Cómo exactamente (y qué papel juega defense) lo decides tú en la práctica; el modelo mental que necesitas ya lo tienes: un método que recibe un número y muta un atributo del objeto.
Estado mutable: el hp que baja¶
Aquí se junta todo. Un objeto no es una foto fija: sus atributos cambian con el tiempo. Eso es estado mutable, y es exactamente lo que hace que una batalla sea una batalla.
pikachu = Pokemon("Pikachu", "Electric", 35, 55, 40, 5, [...])
pikachu.hp ──► 35 (recién creado: hp == max_hp)
pikachu.take_damage(10)
pikachu.hp ──► 25 (el objeto CAMBIÓ)
pikachu.take_damage(30)
pikachu.hp ──► -5 (o lo que decida tu lógica)
pikachu.is_fainted()──► True (hp <= 0)
max_hp sigue valiendo 35 todo el rato: es el techo. hp es lo que va cambiando. Dos atributos parecidos con responsabilidades distintas. Y crucial: si tuvieras charmander al lado, charmander.hp seguiría a 39 — golpear a uno no toca al otro, porque cada objeto tiene su propia copia de los atributos.
Move.use() es el mismo patrón: cada vez que un movimiento se usa, su pp baja un punto, hasta que has_pp() devuelve False y ya no se puede usar más. Mismo concepto (mutar un atributo del objeto), aplicado a otra clase.
Sintaxis Python (al final)¶
Recopilada, ahora que el modelo está claro:
class Move: # definir una clase: class + Nombre + :
def __init__(self, name, power): # constructor: primer parámetro self
self.name = name # crear/asignar un atributo del objeto
self.power = power
def use(self): # método: función con self primero
self.pp -= 1 # mutar un atributo
def has_pp(self):
return self.pp > 0 # un método puede devolver un valor
fire_punch = Move("Fire Punch", 75) # instanciar: ClaseNombre(args) → objeto
fire_punch.name # leer atributo → "Fire Punch"
fire_punch.use() # llamar método (no pasas self)
fire_punch.has_pp() # → True / False
Detalles que conviene fijar:
- El nombre de la clase va en
CapWords(Pokemon,Move), por convención. __init__lleva doble guion bajo a cada lado (un dunder). No lo llamas tú: se dispara solo al instanciar.- Dentro de un método, siempre usas
self.atributopara tocar datos del objeto. Sinself.,namesería una variable local que muere al acabar el método y no quedaría guardada en el objeto.
__str__: cómo se imprime un Pokémon¶
Si haces print(pikachu) sin más, ves algo feo tipo <__main__.Pokemon object at 0x7f...>. El método especial __str__ te deja decidir qué texto representa al objeto:
class Move:
def __init__(self, name, pp):
self.name = name
self.pp = pp
def __str__(self):
return f"{self.name} ({self.pp} PP)"
print(Move("Thunderbolt", 15)) # → Thunderbolt (15 PP)
__str__ recibe solo self, construye un string con los atributos del objeto y lo devuelve (return, no print). Quien haga print(...) o str(...) sobre el objeto verá ese texto. Para Pokemon, el CANON pide un __str__; un formato razonable mostraría el nombre y la vida actual frente a la máxima (por ejemplo Pikachu HP: 25/35), pero el contenido exacto lo decides tú en la práctica.
Gotchas / Anti-patterns¶
| Error | Síntoma | Causa / Fix |
|---|---|---|
Olvidar self en la firma |
TypeError: take_damage() takes 1 positional argument but 2 were given |
Todo método empieza por self: def take_damage(self, amount): |
Olvidar self. al asignar |
El atributo no se guarda; luego AttributeError al leerlo |
Dentro de __init__ usa self.hp = max_hp, no hp = max_hp |
Pasar self tú al llamar |
TypeError: ... got multiple values for argument |
Llama pikachu.take_damage(10), NUNCA take_damage(pikachu, 10) |
| Confundir clase con instancia | Tocas Pokemon.hp esperando afectar a un Pokémon concreto |
Operas sobre objetos (pikachu.hp), no sobre la clase Pokemon |
| Creer que los objetos comparten datos | Golpeas a uno y "baja la vida del otro" | Cada objeto tiene atributos propios; comparten métodos, no estado |
__str__ con print en vez de return |
print(pikachu) muestra None |
__str__ debe devolver un string con return |
Mutar max_hp en vez de hp |
El techo de vida cambia; curaciones rotas | take_damage toca self.hp; max_hp no se modifica |
Tu turno¶
Implementa las dos clases base del motor:
pokemon/move.py— la claseMovecon__init__(self, name, type, power, accuracy, pp, max_pp), el métodouse(self)(bajappen 1) yhas_pp(self)(devuelvebool: ¿quedapp?). La tienes entera más arriba como ejemplo: aquí solo la transcribes a su fichero y la entiendes a fondo.pokemon/pokemon.py— la clasePokemoncon__init__(self, name, type, max_hp, attack, defense, level, moves)(recuerda:self.hp = max_hpal nacer), el métodotake_damage(self, amount)(bajaself.hp),is_fainted(self)(devuelveTruesihp <= 0) y__str__(self).
Verifica con el comando exacto:
Si no tienes pytest disponible, usa el fallback del proyecto: python run_tests.py.
Conexiones¶
- 07-imports-y-paquetes —
MoveyPokemonviven en módulos separados que luego hay que importar entre sí. - MOC_Programacion — punto de entrada del área.
- Ruta futura:
pokemon/move.py(claseMove),pokemon/pokemon.py(clasePokemon). - Tests spec:
tests/test_move.py,tests/test_pokemon.py(define el contrato exacto que debes cumplir).
Resumen mental¶
Una clase es un molde (
class Pokemon, escrito una vez); un objeto es algo fabricado con ese molde (pikachu, con 35 de 35 PS, independiente decharmander). Los atributos son los datos del objeto (pikachu.hp), se leen y escriben con punto.__init__es el constructor: se dispara solo al instanciar y rellena los atributos víaself.atributo = valor.selfes el propio objeto sobre el que llamaste el método:pikachu.take_damage(10)equivale aPokemon.take_damage(pikachu, 10), Python poneself=pikachusolo. Un método es una función dentro de la clase conselfde primer parámetro; opera sobre el objeto. El estado mutable es quehpbaja contake_damagemientrasmax_hpqueda fijo, y cada objeto muta el suyo.__str__devuelve el texto con que se imprime el objeto. Errores típicos: olvidarself, confundir clase con instancia, mutar el atributo equivocado.