La clase pasada aprendimos a pensar en objetos.
Hoy vamos a hacer que trabajen juntos.
La frase de la clase de hoy
Ya sabemos pensar en objetos.
Ahora vamos a aprender a relacionarlos.
Un objeto solo no hace un programa.
Un programa es un montón de objetos hablando entre ellos.
¿Qué objetos hay acá?
¿Qué información tiene?
¿Qué puede hacer?
Datos → atributos · Acciones → métodos.
Las mismas dos preguntas de la clase pasada.
Perro es una clase. firulais ¿qué es?
Un objeto. La clase es el molde; el objeto es lo fabricado.
¿Un Gato puede heredar de Animal?
Sí: un gato ES UN animal. Hereda todo lo que Animal ya tenía.
¿Y una Rueda puede heredar de Auto?
No. Una rueda no es un auto… está adentro del auto. 👀
✓ Ya podemos
Crear una clase y objetos.
Darles atributos y métodos.
Hacer que una clase herede de otra.
✗ Todavía no
Que el hijo agregue sus propios atributos sin repetir los del padre.
Que un método nos devuelva un resultado para seguir usándolo.
Que un objeto tenga adentro a otro objeto.
🧬
super()
El hijo aprovecha lo que el padre ya hace.
🪜
Varios niveles
Heredar de quien ya heredó.
📦
return
Que el método me entregue el resultado.
🎁
Composición
Un objeto adentro de otro objeto.
Y al final las usamos las cuatro juntas en un mismo programa.
Tenemos una clase Persona.
Ahora queremos una clase Jugador.
Un jugador… ¿es una persona?
Si Jugador hereda de Persona… ¿tenemos que volver a escribir nombre y edad?
class Persona: def __init__(self, nombre, edad): self.nombre = nombre self.edad = edad
✗ Primer intento
class Jugador(Persona): def __init__(self, nombre, edad, nivel): self.nombre = nombre self.edad = edad self.nivel = nivel
Funciona… pero escribimos lo mismo dos veces.
…queremos que toda Persona tenga también dni.
¿En cuántos lugares hay que tocar el código?
2
Con 1 hijo
6
Con 5 hijos
😱
Y alguno te lo vas a olvidar
¿Y si le pedimos al padre que haga la parte que él ya sabe hacer?
El padre hace
nombre
edad
El hijo agrega
nivel
Un jugador completo
sin repetir
nada
class Persona: def __init__(self, nombre, edad): self.nombre = nombre self.edad = edad class Jugador(Persona): def __init__(self, nombre, edad, nivel): super().__init__(nombre, edad) self.nivel = nivel
super() quiere decir
“la clase de arriba”.
Cómo se lee esa línea
“Padre, encargate vos de nombre y edad. Yo me ocupo de nivel.”
jugador = Jugador("Pedro", 28, 5)
🧬
El padre hace lo que ya sabe hacer.
El hijo agrega lo que necesita.
class Jugador(Persona): def __init__(self, nombre, edad, nivel): # me olvidé del super() self.nivel = nivel jugador = Jugador("Pedro", 28, 5) print(jugador.nombre)
¿Qué va a pasar en la última línea?
Por qué
Nadie ejecutó el __init__ de Persona.
El padre nunca hizo su parte.
class Vehiculo: def __init__(self, marca, ruedas): self.marca = marca self.ruedas = ruedas
Escribí Moto, que hereda de Vehiculo y agrega cilindrada.
Una solución
class Moto(Vehiculo): def __init__(self, marca, ruedas, cilindrada): super().__init__(marca, ruedas) self.cilindrada = cilindrada
Bonus: ¿y si todas las motos tienen 2 ruedas?
super().__init__(marca, 2) ← el hijo puede decidir qué le pasa al padre.
¿Un perro es un mamífero?
¿Un mamífero es un animal?
Entonces… ¿qué cosas recibe el perro?
Animal aporta
Mamífero agrega
Perro agrega
Entonces un Perro tiene
class Animal: def __init__(self, nombre): self.nombre = nombre class Mamifero(Animal): def __init__(self, nombre, pelaje): super().__init__(nombre) self.pelaje = pelaje class Perro(Mamifero): def __init__(self, nombre, pelaje, raza): super().__init__(nombre, pelaje) self.raza = raza
¿Qué hay de nuevo acá?
Nada
Es exactamente lo mismo del bloque anterior, dos veces.
Perro le pide a Mamifero, y Mamifero le pide a Animal.
Cada clase solo se ocupa de lo suyo. Nadie repite el trabajo de otro.
firu = Perro("Firulais", "corto", "beagle") print(firu.nombre) print(firu.pelaje) print(firu.raza)
Lo importante
Los tres atributos viven en un solo objeto.
Al usarlo, no se nota quién puso cada cosa.
✗ Nadie necesita esto
SerVivo
→ Animal
→ Vertebrado
→ Mamifero
→ Canido
→ Perro
→ Beagle
→ BeagleChico
¿Cuál es el problema de una cadena tan larga?
Para entender una clase hay que leer ocho.
Y para encontrar un dato, subir ocho escalones.
Regla práctica: dos o tres niveles alcanzan casi siempre. Si necesitás más, casi seguro no era herencia lo que buscabas.
Instrumento
→ Cuerda
→ Guitarra
Empleado
→ Gerente
→ Oficina
Personaje
→ Enemigo
→ Jefe Final
El test no cambió nunca: “¿ES UN?” en cada escalón.
def sumar(a, b): print(a + b) sumar(5, 3)
¿Funciona?
Sí. Aparece el 8 en pantalla y todos contentos.
def sumar(a, b): print(a + b) resultado = sumar(5, 3) print(resultado)
¿Qué imprime la última línea?
El 8 se vio… pero resultado quedó vacío.
¿Por qué resultado quedó vacío, si el 8 apareció en la pantalla?
Porque
La función mostró el 8, pero nunca lo entregó.
Mostrar y entregar
no son lo mismo.
✗ Antes
def sumar(a, b): print(a + b)
✓ Ahora
def sumar(a, b): return a + b
resultado = sumar(5, 3) # resultado vale 8
Lo veo… y se terminó ahí.
print() me muestra algo. return me entrega algo.
🧑🍳
print()
El mozo te dice: “el café sale 2000”.
Lo escuchaste. Pero no tenés nada en la mano.
return
El mozo te trae el café.
Ahora sí: podés tomarlo, compartirlo, guardarlo.
class Jugador: def __init__(self, nombre, vida): self.nombre = nombre self.vida = vida def esta_vivo(self): return self.vida > 0
if jugador.esta_vivo(): print("Seguí jugando") else: print("Game Over")
Acá está la gracia
Con return, el objeto puede contestarle al programa.
Y con esa respuesta, el programa decide.
return — después quiero sumarlo al total.
print() — solo hay que verlo, nadie lo reutiliza.
return — la respuesta va adentro de un if.
print() — es información para la persona, no para el programa.
¿Un Jugador ES un Arma?
¿Un Jugador TIENE un Arma?
Entonces acá no va herencia.
Va otra cosa.
HERENCIA
El perro es un animal.
Recibe lo que el animal ya tenía.
COMPOSICIÓN
El jugador tiene un arma.
Son dos objetos distintos, conectados.
ES UN → herencia
TIENE UN → composición
TIENE UNA → composición
ES UNA → herencia
TIENE UNA → composición
class Arma: def __init__(self, nombre, daño): self.nombre = nombre self.daño = daño def atacar(self): return self.daño
¿Por qué return y no print?
Porque el daño no es para mirarlo:
es para restárselo a la vida del enemigo.
Un arma es un objeto completo: tiene sus datos y sabe hacer lo suyo. Todavía no sabe nada de ningún jugador.
class Jugador: def __init__(self, nombre, arma): self.nombre = nombre self.arma = arma
Esa línea resaltada… ¿qué guarda?
Guarda
Un objeto entero, no un texto ni un número.
Un atributo también puede ser otro objeto.
Es la misma línea de siempre: self.algo = algo. Lo único distinto es qué estamos guardando.
espada = Arma("Espada", 20) jugador = Jugador("Pedro", espada)
¿Qué pasa en la segunda línea?
El objeto espada entra adentro del objeto jugador.
Primero fabricamos el arma. Después fabricamos el jugador con el arma.
Un objeto puede tener otro objeto adentro.
print(jugador.nombre) # Pedro print(jugador.arma.nombre) # Espada print(jugador.arma.atacar()) # 20
¿Cómo se lee jugador.arma.nombre?
“El nombre del arma del jugador.”
Se lee de derecha a izquierda, poniendo “de” en cada punto.
El jugador no sabe cuánto daño hace. Le pregunta al arma.
hacha = Arma("Hacha", 35) jugador.arma = hacha print(jugador.arma.atacar()) # 35
¿Qué línea de la clase Jugador tuvimos que cambiar?
Ninguna.
El jugador siempre supo lo mismo: “tengo un arma y le pido que ataque”. Qué arma sea, no es su problema.
Con 50 armas distintas, la clase Jugador sigue igual de corta.
🎮
Ahora las usamos las cuatro juntas.
Un mismo sistema con herencia, super(), return y composición.
Primero lo pensamos. Después lo dibujamos. Recién después lo escribimos.
¿Qué tiene todo personaje?
¿Qué agrega el jugador?
¿Y el arma?
En Personaje va solo lo que comparten todos. El arma no va ahí: no todos los personajes tienen.
class Personaje: def __init__(self, nombre, vida): self.nombre = nombre self.vida = vida def esta_vivo(self): return self.vida > 0 def recibir_daño(self, cantidad): self.vida = self.vida - cantidad
Todo lo que comparten todos los personajes: jugadores, enemigos, jefes.
¿Por qué esta_vivo() usa return y recibir_daño() no?
esta_vivo() contesta una pregunta → hay que devolver la respuesta.
recibir_daño() hace un cambio en el objeto → no tiene nada que entregar.
class Jugador(Personaje): def __init__(self, nombre, vida, arma): super().__init__(nombre, vida) self.arma = arma def atacar_a(self, otro): daño = self.arma.atacar() otro.recibir_daño(daño) return daño
Línea 3: el padre pone nombre y vida.
Línea 4: el hijo agrega su arma.
En atacar_a(), ¿cuántos objetos participan?
Tres. El jugador le pide el daño a su arma, y se lo aplica al otro personaje.
espada = Arma("Espada", 20) pedro = Jugador("Pedro", 100, espada) orco = Personaje("Orco", 50) pedro.atacar_a(orco) print(orco.vida) pedro.atacar_a(orco) pedro.atacar_a(orco) print(orco.esta_vivo())
¿Qué imprime cada print?
Las cuentas
50 − 20 = 30 · después 10 · después −10.
Con vida negativa, esta_vivo() devuelve False. 💀
Ninguna es difícil sola.
Juntas, ya podés modelar casi cualquier cosa.
El orco de recién no ataca: solo recibe golpes. Queremos que se defienda.
La consigna
Escribí la clase Enemigo. Tiene que poder atacar igual que el jugador, pero con un Hechizo en vez de un arma… y además tener una dificultad.
class Hechizo: def __init__(self, nombre, daño): self.nombre = nombre self.daño = daño def atacar(self): return self.daño class Enemigo(Personaje): def __init__(self, nombre, vida, hechizo, dificultad): super().__init__(nombre, vida) self.hechizo = hechizo self.dificultad = dificultad def atacar_a(self, otro): daño = self.hechizo.atacar() otro.recibir_daño(daño) return daño
🧬 Hereda nombre y vida.
🧬 super() para no repetirlos.
🎁 Tiene un hechizo adentro.
📦 atacar() devuelve el daño.
Última pregunta difícil:
¿no convendría mover el arma a Personaje?
ES UN
↓
🧬 HERENCIA
TIENE UN
↓
🎁 COMPOSICIÓN
HACE ALGO Y ME LO MUESTRA
↓
print()
HACE ALGO Y ME ENTREGA EL RESULTADO
↓
return
Olvidarse el super()
Síntoma: AttributeError: has no attribute… El padre nunca hizo su parte.
Usar print donde iba return
Síntoma: aparece un None donde esperabas un número.
Heredar cuando había que componer
Síntoma: la frase “es un” suena rara. un jugador es un arma
Encontrá 3 relaciones ES UN y 3 relaciones TIENE UN en cosas de tu vida real.
Ejemplos para arrancar
🧬 Un colectivo es un vehículo.
🎁 Un colectivo tiene un motor.
La prueba
Leelo en voz alta con las dos frases. Si una suena rara, esa no es.
Diseñala sin escribir código, usando las 7 preguntas.
Tiene que aparecer
🧬 Una herencia (algo que ES UN algo)
🎁 Una composición (algo que TIENE UN algo)
📦 Un método que devuelva un valor
Entregá
Un dibujo con las cajas y las flechas, como los de hoy. Nada de Python todavía.
1 · Persona con nombre y edad.
2 · Estudiante hereda de Persona y agrega curso, usando super().
3 · Una clase Mochila con un método que devuelva el peso total.
4 · Que el Estudiante tenga una Mochila.
Para probar que anda
mochila = Mochila(3) ana = Estudiante("Ana", 16, "5°B", mochila) print(ana.nombre) print(ana.mochila.peso_total())
★ Bonus · que el enemigo devuelva el golpe
No aprendimos comandos nuevos porque sí.
Aprendimos herramientas para representar mejor nuestras ideas.
La clase pasada aprendimos a ver objetos.
Hoy aprendimos a conectarlos.
Y para terminar
Si mañana te doy un problema nuevo… ¿qué es lo primero que deberías preguntarte?
“¿Qué objetos hay acá?”
Nos vemos la próxima 👋