Herencia y super(), overriding, polimorfismo (__str__/__repr__, operadores, duck typing), multiherencia y MRO, clases abstractas (abc), @property y diagrama de clases (UML). Cubre la Pregunta 2 de la I1.
En Python no existe el overloading de funciones (definir dos veces def metodo(...) con distintos parametros): la ultima definicion sobrescribe a la anterior. Lo que Python si permite es sobrecargar operadores mediante metodos magicos: __add__ (+), __eq__ (==), __lt__ (<), __le__, __gt__, __ge__, etc.
class Fraccion: def __init__(self, num, den): self.num, self.den = num, den def __add__(self, otra): # sobrecarga el operador + return Fraccion(self.num * otra.den + otra.num * self.den, self.den * otra.den) def __repr__(self): return f"Fraccion({self.num}, {self.den})" # sin ambiguedad (debug) def __str__(self): return f"{self.num}/{self.den}" # legible para humanos print(Fraccion(1, 2) + Fraccion(1, 3)) # 5/6 -> usa __str__print([Fraccion(1, 2)]) # [Fraccion(1, 2)] -> usa __repr____str__ busca una representacion legible para humanos (la usa print). __repr__ busca una representacion sin ambiguedad para el desarrollador (la usan la consola y print de una lista de objetos). Si solo defines __repr__, print lo usa como respaldo.
Duck typing: "si camina como pato y grita como pato, es un pato". Python no exige herencia ni tipo declarado para llamar a un metodo; basta con que el objeto lo implemente. Por eso el for de arriba funciona con cualquier objeto que tenga describir(), aunque no hereden de una base comun.
Cuando el mismo metodo existe en varias ramas, Python decide cual usar con el MRO (Method Resolution Order), calculado por el algoritmo C3 Linearization: busca primero en la clase derivada y luego en las superclases en el orden en que se declararon. Puedes inspeccionarlo con Academico.__mro__.
Cuando dos superclases heredan de una misma base (forma de diamante), llamar explicitamente a cada __init__ ejecutaria la base dos veces. La solucion correcta es que cada clase use super().__init__(kwargs) y pase los argumentos con kwargs: el MRO se encarga de llamar a cada init una sola vez y en el orden correcto.
Instanciar una clase abstracta o una subclase que no implemente todos los metodos abstractos lanza TypeError. Las clases abstractas tambien pueden tener metodos concretos (no abstractos) que las subclases heredan normalmente.
@property deja que objeto.radio se lea y escriba como atributo, pero por debajo corre codigo que valida. Es el patron esperado en el curso para atributos con reglas (ej. nivel minimo 1, radio ≥ 0). @staticmethod, en cambio, define un metodo que no recibe self y se llama sobre la clase.
El diagrama de clases modela el sistema antes de programarlo. Cada clase es un rectangulo con tres niveles: nombre, atributos (incluyendo properties) y metodos (con sus parametros y tipo de retorno). Las relaciones principales son:
Partida contiene Personajes). El curso no distingue formalmente composicion de agregacion.El diagrama se traduce directo a un esqueleto de codigo: cada clase, sus atributos en __init__ y un def por metodo. Esto es exactamente la Pregunta 2 de la I1 (modelar Personaje abstracto, Heroe, Enemigo, Partida con herencia, properties con validacion y polimorfismo en atacar()).