32. Ampliar clases con herencia y polimorfismo
En el Capítulo 30, aprendimos a crear nuestras propias clases para modelar conceptos del mundo real. Construimos clases como BankAccount y Student que agrupaban datos y comportamiento. Pero, ¿qué pasa cuando necesitas crear una nueva clase que sea similar a una existente, pero con algunas diferencias o añadidos?
La herencia(inheritance) es el mecanismo de Python para crear nuevas clases basadas en otras existentes. En lugar de copiar y pegar código, puedes crear una subclase(subclass) que automáticamente obtiene todos los atributos y métodos de una clase padre(parent class) (también llamada clase base(base class) o superclase(superclass)), y luego añadir o modificar lo que necesites.
Este capítulo explora cómo la herencia te permite construir jerarquías de clases relacionadas, cómo personalizar el comportamiento heredado y cómo el polimorfismo(polymorphism) permite que distintas clases se usen de forma intercambiable cuando comparten interfaces comunes.
32.1) Crear subclases a partir de clases existentes
32.1.1) La sintaxis básica de la herencia
La herencia te permite crear una nueva clase (llamada subclase(subclass) o clase hija(child class)) basada en una clase existente (llamada clase padre(parent class) o clase base(base class)). La subclase hereda automáticamente todos los métodos y atributos de la clase padre.
Cuando creas una subclase, especificas la clase padre entre paréntesis después del nombre de la clase.
# Clase padre
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} makes a sound"
# Subclase que hereda de Animal
class Dog(Animal):
pass # Todavía no se necesita código adicional
# Crear una instancia de Dog
buddy = Dog("Buddy")
print(buddy.speak()) # Output: Buddy makes a sound
print(buddy.name) # Output: BuddyAunque Dog no tiene código propio (solo pass), hereda todo de Animal. La clase Dog automáticamente tiene el método __init__ y el método speak de su padre.
32.1.2) Por qué importa la herencia
La herencia resuelve un problema común de programación: la duplicación de código. Imagina que estás construyendo un sistema para gestionar diferentes tipos de empleados:
# Sin herencia: mucha duplicación
class FullTimeEmployee:
def __init__(self, name, employee_id, salary):
self.name = name
self.employee_id = employee_id
self.salary = salary
def get_info(self):
return f"{self.name} (ID: {self.employee_id})"
class PartTimeEmployee:
def __init__(self, name, employee_id, hourly_rate):
self.name = name
self.employee_id = employee_id
self.hourly_rate = hourly_rate
def get_info(self):
return f"{self.name} (ID: {self.employee_id})"Fíjate en que name, employee_id y get_info() están duplicados. Con la herencia, podemos eliminar esta duplicación:
# Con herencia: código compartido en la clase padre
class Employee:
def __init__(self, name, employee_id):
self.name = name
self.employee_id = employee_id
def get_info(self):
return f"{self.name} (ID: {self.employee_id})"
class FullTimeEmployee(Employee):
def __init__(self, name, employee_id, salary):
Employee.__init__(self, name, employee_id) # Llama al __init__ del padre
# Nota: Aprenderemos una mejor forma de hacer esto con super() en la Sección 32.3
self.salary = salary
class PartTimeEmployee(Employee):
def __init__(self, name, employee_id, hourly_rate):
Employee.__init__(self, name, employee_id) # Llama al __init__ del padre
self.hourly_rate = hourly_rate
# Ambas subclases heredan get_info()
alice = FullTimeEmployee("Alice", "E001", 75000)
bob = PartTimeEmployee("Bob", "E002", 25)
print(alice.get_info()) # Output: Alice (ID: E001)
print(bob.get_info()) # Output: Bob (ID: E002)Ahora, los atributos y métodos comunes viven en Employee, y cada subclase solo define lo que la hace única.
32.1.3) Añadir nuevos métodos a las subclases
Las subclases pueden añadir sus propios métodos que el padre no tiene:
class Vehicle:
def __init__(self, brand, model):
self.brand = brand
self.model = model
def get_description(self):
return f"{self.brand} {self.model}"
class Car(Vehicle):
def __init__(self, brand, model, num_doors):
Vehicle.__init__(self, brand, model)
self.num_doors = num_doors
def honk(self): # Nuevo método específico de Car
return "Beep beep!"
class Motorcycle(Vehicle):
def __init__(self, brand, model, has_sidecar):
Vehicle.__init__(self, brand, model)
self.has_sidecar = has_sidecar
def rev_engine(self): # Nuevo método específico de Motorcycle
return "Vroom vroom!"
# Cada subclase tiene los métodos de su padre más los suyos
my_car = Car("Toyota", "Camry", 4)
print(my_car.get_description()) # Output: Toyota Camry
print(my_car.honk()) # Output: Beep beep!
my_bike = Motorcycle("Harley", "Sportster", False)
print(my_bike.get_description()) # Output: Harley Sportster
print(my_bike.rev_engine()) # Output: Vroom vroom!La clase Car tiene tanto get_description() (heredado) como honk() (propio). La clase Motorcycle tiene get_description() (heredado) y rev_engine() (propio).
32.1.4) Añadir nuevos atributos a las subclases
Las subclases también pueden añadir sus propios atributos de instancia. Normalmente haces esto en el método __init__ de la subclase:
class BankAccount:
def __init__(self, account_number, balance):
self.account_number = account_number
self.balance = balance
def deposit(self, amount):
self.balance += amount
return self.balance
class SavingsAccount(BankAccount):
def __init__(self, account_number, balance, interest_rate):
BankAccount.__init__(self, account_number, balance)
self.interest_rate = interest_rate # Nuevo atributo
def apply_interest(self): # Nuevo método que usa el nuevo atributo
interest = self.balance * self.interest_rate
self.balance += interest
return interest
# SavingsAccount tiene todos los atributos de BankAccount más interest_rate
savings = SavingsAccount("SA001", 1000, 0.03)
savings.deposit(500) # Método heredado
print(f"Balance: ${savings.balance}") # Output: Balance: $1500
interest_earned = savings.apply_interest()
print(f"Interest earned: ${interest_earned:.2f}") # Output: Interest earned: $45.00
print(f"New balance: ${savings.balance:.2f}") # Output: New balance: $1545.00SavingsAccount tiene account_number y balance de BankAccount, además de su propio atributo interest_rate.
32.1.5) Múltiples niveles de herencia
Las clases pueden heredar de clases que a su vez heredan de otras, creando una jerarquía de herencia:
class LivingThing:
def __init__(self, name):
self.name = name
def is_alive(self):
return True
class Animal(LivingThing):
def __init__(self, name, species):
LivingThing.__init__(self, name)
self.species = species
def move(self):
return f"{self.name} is moving"
class Dog(Animal):
def __init__(self, name, breed):
Animal.__init__(self, name, "Dog")
self.breed = breed
def bark(self):
return f"{self.name} says: Woof!"
# Dog hereda de Animal, que hereda de LivingThing
max_dog = Dog("Max", "Golden Retriever")
# Los métodos de los tres niveles funcionan
print(max_dog.is_alive()) # Output: True (from LivingThing)
print(max_dog.move()) # Output: Max is moving (from Animal)
print(max_dog.bark()) # Output: Max says: Woof! (from Dog)
# Existen atributos de los tres niveles
print(max_dog.name) # Output: Max (from LivingThing)
print(max_dog.species) # Output: Dog (from Animal)
print(max_dog.breed) # Output: Golden Retriever (from Dog)Dog hereda de Animal, que hereda de LivingThing. Esto significa que Dog tiene acceso a métodos y atributos de ambas clases padre.
32.2) Sobrescribir métodos en subclases
32.2.1) Qué significa sobrescribir métodos
A veces, una subclase necesita cambiar cómo funciona un método heredado. La sobrescritura de métodos(method overriding) significa definir un método en la subclase con el mismo nombre que un método en la clase padre. Cuando llamas a ese método en una instancia de la subclase, Python usa la versión de la subclase en lugar de la del padre.
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} makes a sound"
class Dog(Animal):
def speak(self): # Sobrescribe el método speak del padre
return f"{self.name} says: Woof!"
class Cat(Animal):
def speak(self): # Sobrescribe con un comportamiento diferente
return f"{self.name} says: Meow!"
# Cada clase tiene su propia versión de speak()
generic_animal = Animal("Generic")
print(generic_animal.speak()) # Output: Generic makes a sound
buddy = Dog("Buddy")
print(buddy.speak()) # Output: Buddy says: Woof!
whiskers = Cat("Whiskers")
print(whiskers.speak()) # Output: Whiskers says: Meow!Cuando llamas a buddy.speak(), Python busca primero el método speak en la clase Dog, ya que buddy es una instancia de Dog. Como Dog define su propio método speak, Python usa esa versión. Si Dog no tuviera un método speak, Python entonces buscaría en la clase padre Animal y usaría esa versión en su lugar.
Este orden de búsqueda—comenzando por la clase de la instancia y luego pasando a la clase padre—es cómo funciona la sobrescritura de métodos y cómo las subclases personalizan el comportamiento heredado.
32.2.2) ¿Por qué sobrescribir métodos?
La sobrescritura de métodos te permite crear versiones especializadas de un comportamiento general. Considera una jerarquía de formas:
class Shape:
def __init__(self, name):
self.name = name
def area(self):
return 0 # Implementación por defecto
def describe(self):
return f"{self.name} with area {self.area()}"
class Rectangle(Shape):
def __init__(self, width, height):
Shape.__init__(self, "Rectangle")
self.width = width
self.height = height
def area(self): # Sobrescribe con un cálculo específico de rectángulo
return self.width * self.height
class Circle(Shape):
def __init__(self, radius):
Shape.__init__(self, "Circle")
self.radius = radius
def area(self): # Sobrescribe con un cálculo específico de círculo
return 3.14159 * self.radius ** 2
# Cada forma calcula el área de manera diferente
rect = Rectangle(5, 3)
print(rect.describe()) # Output: Rectangle with area 15
circle = Circle(4)
print(circle.describe()) # Output: Circle with area 50.26544El método describe() es heredado por ambas subclases y funciona correctamente porque cada subclase proporciona su propia implementación de area().
Cuando llamas a rect.describe(), se ejecuta el método heredado describe(), pero self se refiere a la instancia de Rectangle. Así que cuando describe() llama a self.area(), Python busca primero area() en la clase Rectangle y encuentra la versión sobrescrita.
32.2.3) Sobrescribir __init__ y llamar a la inicialización del padre
Cuando sobrescribes __init__, normalmente necesitas llamar al __init__ del padre para asegurarte de que ocurra la inicialización del padre:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def introduce(self):
return f"I'm {self.name}, {self.age} years old"
class Student(Person):
def __init__(self, name, age, student_id, major):
# Llama al __init__ del padre para establecer name y age
Person.__init__(self, name, age)
# Luego establece atributos específicos del estudiante
self.student_id = student_id
self.major = major
alice = Student("Alice", 20, "S12345", "Computer Science")
print(alice.name) # Output: Alice
print(alice.student_id) # Output: S12345Fíjate en que Student.__init__ primero llama a Person.__init__(self, name, age) para inicializar los atributos de la clase padre, y luego añade sus propios atributos.
32.3) Usar super() para acceder al comportamiento del padre
32.3.1) Qué hace super()
En la sección anterior, llamamos a métodos del padre de forma explícita: ParentClass.method(self, ...). Python ofrece una forma más limpia: la función super(). super() devuelve un objeto temporal que te permite llamar a métodos de la clase padre sin nombrar al padre explícitamente.
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} makes a sound"
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name) # Más limpio que Animal.__init__(self, name)
self.breed = breed
def speak(self):
parent_sound = super().speak() # Llama al speak() del padre
return f"{parent_sound} - specifically, Woof!"
buddy = Dog("Buddy", "Labrador")
print(buddy.speak())
# Output: Buddy makes a sound - specifically, Woof!Usar super() tiene varias ventajas:
- No necesitas nombrar la clase padre explícitamente
- Funciona correctamente con herencia múltiple (tratada más adelante)
- Hace que el código sea más fácil de mantener si cambias la clase padre
32.3.2) Usar super() en __init__
El uso más común de super() es llamar al __init__ del padre:
class Employee:
def __init__(self, name, employee_id):
self.name = name
self.employee_id = employee_id
self.is_active = True
def deactivate(self):
self.is_active = False
class Manager(Employee):
def __init__(self, name, employee_id, department):
super().__init__(name, employee_id) # Inicializa los atributos del padre
self.department = department
self.team = [] # Atributo específico de Manager
def add_team_member(self, employee):
self.team.append(employee)
# Manager obtiene todos los atributos de Employee más los suyos
sarah = Manager("Sarah", "M001", "Engineering")
print(sarah.name) # Output: Sarah
print(sarah.is_active) # Output: True
print(sarah.department) # Output: EngineeringAl llamar a super().__init__(name, employee_id), la clase Manager se asegura de que se ejecute toda la lógica de inicialización de Employee, incluyendo establecer is_active en True.
32.3.3) Extender métodos del padre con super()
Puedes usar super() para extender un método del padre en lugar de reemplazarlo por completo:
class BankAccount:
def __init__(self, account_number, balance):
self.account_number = account_number
self.balance = balance
self.transaction_count = 0
def deposit(self, amount):
self.balance += amount
self.transaction_count += 1
return self.balance
class CheckingAccount(BankAccount):
def __init__(self, account_number, balance, overdraft_limit):
super().__init__(account_number, balance)
self.overdraft_limit = overdraft_limit
def deposit(self, amount):
was_overdrawn = self.balance < 0
# Llama al deposit del padre para manejar la lógica básica
new_balance = super().deposit(amount)
# Añade un comportamiento específico de cuenta corriente
if was_overdrawn and new_balance >= 0:
print("Account is no longer overdrawn")
return new_balance
checking = CheckingAccount("C001", -50, 100)
checking.deposit(75)
# Output: Account is no longer overdrawn
print(f"Balance: ${checking.balance}") # Output: Balance: $25
print(f"Transactions: {checking.transaction_count}") # Output: Transactions: 1El método CheckingAccount.deposit() llama a super().deposit(amount) para manejar la lógica básica del depósito (actualizar el saldo y el conteo de transacciones), y luego añade su propia comprobación del estado de sobregiro.
32.3.4) Cuándo usar super() vs llamada directa al padre
Usa super() en la mayoría de los casos:
class Vehicle:
def __init__(self, brand):
self.brand = brand
class Car(Vehicle):
def __init__(self, brand, model):
super().__init__(brand) # Preferido
self.model = modelUsa llamadas directas al padre cuando necesitas llamar a un padre específico en un escenario de herencia múltiple (tratado más adelante) o cuando quieres ser explícito sobre a qué padre estás llamando:
class Car(Vehicle):
def __init__(self, brand, model):
Vehicle.__init__(self, brand) # Explícito, pero menos flexible
self.model = modelPara herencia simple (un padre), super() casi siempre es la mejor opción.
32.3.5) super() con otros métodos
Puedes usar super() con cualquier método, no solo con __init__:
class TextProcessor:
def process(self, text):
# Procesamiento básico: eliminar espacios en blanco
return text.strip()
class UppercaseProcessor(TextProcessor):
def process(self, text):
# Primero hace el procesamiento del padre
processed = super().process(text)
# Luego añade conversión a mayúsculas
return processed.upper()
class PrefixProcessor(UppercaseProcessor):
def __init__(self, prefix):
self.prefix = prefix
def process(self, text):
# Primero hace el procesamiento del padre (que también llama a su padre)
processed = super().process(text)
# Luego añade el prefijo
return f"{self.prefix}: {processed}"
processor = PrefixProcessor("ALERT")
result = processor.process(" system error ")
print(result) # Output: ALERT: SYSTEM ERROR32.4) Polimorfismo: trabajar con clases compatibles
32.4.1) Qué significa polimorfismo
El polimorfismo(polymorphism) (del griego: “muchas formas”) es la capacidad de tratar objetos de distintas clases de la misma manera si proporcionan los mismos métodos.
En Python, si varias clases tienen métodos con el mismo nombre, puedes llamar a esos métodos sin conocer la clase exacta del objeto.
class Dog:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} says: Woof!"
class Cat:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} says: Meow!"
class Bird:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} says: Tweet!"
# Función que funciona con cualquier objeto que tenga un método speak()
def make_animal_speak(animal):
print(animal.speak())
# Funciona con diferentes clases
buddy = Dog("Buddy")
whiskers = Cat("Whiskers")
tweety = Bird("Tweety")
make_animal_speak(buddy) # Output: Buddy says: Woof!
make_animal_speak(whiskers) # Output: Whiskers says: Meow!
make_animal_speak(tweety) # Output: Tweety says: Tweet!La función make_animal_speak() no se preocupa por qué clase es el parámetro animal: solo necesita que el objeto tenga un método speak(). Esto es el polimorfismo en acción.
32.4.2) Polimorfismo con herencia
El polimorfismo es especialmente potente con herencia, donde las subclases sobrescriben métodos del padre:
class PaymentMethod:
def process_payment(self, amount):
return f"Processing ${amount:.2f}"
class CreditCard(PaymentMethod):
def __init__(self, card_number):
self.card_number = card_number
def process_payment(self, amount):
return f"Charging ${amount:.2f} to credit card ending in {self.card_number[-4:]}"
class PayPal(PaymentMethod):
def __init__(self, email):
self.email = email
def process_payment(self, amount):
return f"Sending ${amount:.2f} via PayPal to {self.email}"
class BankTransfer(PaymentMethod):
def __init__(self, account_number):
self.account_number = account_number
def process_payment(self, amount):
return f"Transferring ${amount:.2f} to account {self.account_number}"
# Función que funciona con cualquier PaymentMethod
def complete_purchase(payment_method, amount):
print(payment_method.process_payment(amount))
print("Purchase complete!")
# Todos los métodos de pago funcionan con la misma función
credit = CreditCard("1234567890123456")
paypal = PayPal("user@example.com")
bank = BankTransfer("9876543210")
complete_purchase(credit, 99.99)
# Output: Charging $99.99 to credit card ending in 3456
# Output: Purchase complete!
complete_purchase(paypal, 49.50)
# Output: Sending $49.50 via PayPal to user@example.com
# Output: Purchase complete!
complete_purchase(bank, 199.00)
# Output: Transferring $199.00 to account 9876543210
# Output: Purchase complete!La función complete_purchase() funciona con cualquier subclase de PaymentMethod. Cada subclase proporciona su propia implementación de process_payment(), pero la función no necesita saber con qué clase específica está trabajando.
32.4.3) Duck typing: "Si camina como un pato..."
El polimorfismo de Python no requiere que las clases estén relacionadas mediante herencia. Esto se llama duck typing(duck typing): “Si camina como un pato y grazna como un pato, entonces es un pato”. En otras palabras, a Python le importa lo que un objeto puede hacer(sus métodos), no lo que es (su clase). Si un objeto tiene los métodos que necesitas, puedes usarlo, independientemente de su jerarquía de clases.
class FileWriter:
def __init__(self, filename):
self.filename = filename
def write(self, data):
print(f"Writing to {self.filename}: {data}")
class DatabaseWriter:
def __init__(self, table_name):
self.table_name = table_name
def write(self, data):
print(f"Inserting into {self.table_name}: {data}")
class ConsoleWriter:
def write(self, data):
print(f"Console output: {data}")
# Función que funciona con cualquier objeto que tenga un método write()
def save_data(writer, data):
writer.write(data)
# Las tres clases funcionan, aunque no estén relacionadas
file_writer = FileWriter("data.txt")
db_writer = DatabaseWriter("users")
console_writer = ConsoleWriter()
save_data(file_writer, "User data")
# Output: Writing to data.txt: User data
save_data(db_writer, "User data")
# Output: Inserting into users: User data
save_data(console_writer, "User data")
# Output: Console output: User dataNinguna de estas clases hereda de un padre común, pero todas funcionan con save_data() porque todas tienen un método write(). Esto es duck typing: a la función no le importa la clase, solo la interfaz (los métodos disponibles).
32.4.4) Ejemplo práctico: un sistema de plugins
El polimorfismo permite sistemas flexibles y extensibles. Aquí tienes un sistema de plugins sencillo para el procesamiento de datos:
class DataProcessor:
def process(self, data):
return data # La implementación base no hace nada
class UppercaseProcessor(DataProcessor):
def process(self, data):
return data.upper()
class ReverseProcessor(DataProcessor):
def process(self, data):
return data[::-1]
class RemoveSpacesProcessor(DataProcessor):
def process(self, data):
return data.replace(" ", "")
class DataPipeline:
def __init__(self):
self.processors = []
def add_processor(self, processor):
self.processors.append(processor)
def run(self, data):
result = data
for processor in self.processors:
result = processor.process(result) # Llamada polimórfica
return result
# Construye una canalización de procesamiento
pipeline = DataPipeline()
pipeline.add_processor(UppercaseProcessor())
pipeline.add_processor(RemoveSpacesProcessor())
pipeline.add_processor(ReverseProcessor())
# Procesa datos a través de la canalización
input_data = "Hello World"
output = pipeline.run(input_data)
print(f"Input: {input_data}") # Output: Input: Hello World
print(f"Output: {output}") # Output: Output: DLROWOLLEHDataPipeline no necesita saber qué procesadores específicos contiene: simplemente llama a process() en cada uno. Puedes añadir fácilmente nuevos tipos de procesadores sin cambiar el código de la canalización.
32.5) Comprobar tipos y relaciones de clases (isinstance, issubclass)
32.5.1) Comprobar tipos de instancias con isinstance()
A veces necesitas comprobar si un objeto es una instancia de una clase particular. La función isinstance() hace esto:
class Animal:
pass
class Dog(Animal):
pass
class Cat(Animal):
pass
buddy = Dog()
whiskers = Cat()
# Comprueba si un objeto es una instancia de una clase
print(isinstance(buddy, Dog)) # Output: True
print(isinstance(buddy, Animal)) # Output: True (Dog inherits from Animal)
print(isinstance(buddy, Cat)) # Output: False
print(isinstance(whiskers, Cat)) # Output: True
print(isinstance(whiskers, Animal)) # Output: True
print(isinstance(whiskers, Dog)) # Output: FalseFíjate en que isinstance(buddy, Animal) devuelve True aunque buddy sea una instancia de Dog. Esto se debe a que Dog hereda de Animal, así que una instancia de Dog también se considera una instancia de Animal.
32.5.2) Por qué isinstance() respeta la herencia
La función isinstance() comprueba toda la cadena de herencia:
class Vehicle:
pass
class Car(Vehicle):
pass
class ElectricCar(Car):
pass
tesla = ElectricCar()
# Comprueba todos los niveles de herencia
print(isinstance(tesla, ElectricCar)) # Output: True
print(isinstance(tesla, Car)) # Output: True
print(isinstance(tesla, Vehicle)) # Output: True
print(isinstance(tesla, str)) # Output: FalseEl objeto tesla es una instancia de ElectricCar, pero también es una instancia de Car y Vehicle debido a la herencia.
32.5.3) Comprobar varios tipos a la vez
Puedes comprobar si un objeto es una instancia de cualquiera de varias clases pasando una tupla:
class Dog:
pass
class Cat:
pass
class Bird:
pass
def is_pet(animal):
return isinstance(animal, (Dog, Cat, Bird))
buddy = Dog()
whiskers = Cat()
tweety = Bird()
rock = "just a rock"
print(is_pet(buddy)) # Output: True
print(is_pet(whiskers)) # Output: True
print(is_pet(tweety)) # Output: True
print(is_pet(rock)) # Output: FalseEsto es más conciso que escribir isinstance(animal, Dog) or isinstance(animal, Cat) or isinstance(animal, Bird).
32.5.4) Comprobar relaciones entre clases con issubclass()
La función issubclass() comprueba si una clase es subclase de otra:
class Animal:
pass
class Dog(Animal):
pass
class Cat(Animal):
pass
class Poodle(Dog):
pass
# Comprueba relaciones entre clases
print(issubclass(Dog, Animal)) # Output: True
print(issubclass(Cat, Animal)) # Output: True
print(issubclass(Poodle, Dog)) # Output: True
print(issubclass(Poodle, Animal)) # Output: True (indirect inheritance)
print(issubclass(Dog, Cat)) # Output: False
# Una clase se considera subclase de sí misma
print(issubclass(Dog, Dog)) # Output: TrueTen en cuenta que issubclass() funciona con clases, no con instancias. Usa isinstance() para instancias y issubclass() para clases.
32.5.5) Casos de uso prácticos para la comprobación de tipos
La comprobación de tipos es útil cuando necesitas un comportamiento diferente para diferentes tipos:
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
class Manager(Employee):
def __init__(self, name, salary, bonus):
super().__init__(name, salary)
self.bonus = bonus
class Contractor:
def __init__(self, name, hourly_rate, hours):
self.name = name
self.hourly_rate = hourly_rate
self.hours = hours
def calculate_payment(worker):
# Cuando uses isinstance() con herencia, comprueba las subclases antes que las clases padre
if isinstance(worker, Manager): # Comprueba Manager primero
return worker.salary + worker.bonus
elif isinstance(worker, Employee): # Luego comprueba Employee (clase padre)
return worker.salary
elif isinstance(worker, Contractor):
return worker.hourly_rate * worker.hours
else:
return 0
alice = Employee("Alice", 50000)
bob = Manager("Bob", 70000, 10000)
charlie = Contractor("Charlie", 50, 160)
print(f"Alice's payment: ${calculate_payment(alice)}") # Output: Alice's payment: $50000
print(f"Bob's payment: ${calculate_payment(bob)}") # Output: Bob's payment: $80000
print(f"Charlie's payment: ${calculate_payment(charlie)}")# Output: Charlie's payment: $8000Sin embargo, en muchos casos, el polimorfismo (hacer que cada clase implemente un método común) es mejor que la comprobación de tipos:
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
def calculate_payment(self):
return self.salary
class Manager(Employee):
def __init__(self, name, salary, bonus):
super().__init__(name, salary)
self.bonus = bonus
def calculate_payment(self):
return self.salary + self.bonus
class Contractor:
def __init__(self, name, hourly_rate, hours):
self.name = name
self.hourly_rate = hourly_rate
self.hours = hours
def calculate_payment(self):
return self.hourly_rate * self.hours
# No se necesita comprobación de tipos: el polimorfismo se encarga
workers = [
Employee("Alice", 50000),
Manager("Bob", 70000, 10000),
Contractor("Charlie", 50, 160)
]
for worker in workers:
payment = worker.calculate_payment() # Llamada polimórfica
print(f"{worker.name}'s payment: ${payment}")Output:
Alice's payment: $50000
Bob's payment: $80000
Charlie's payment: $8000Este enfoque polimórfico es más flexible y más fácil de ampliar con nuevos tipos de trabajadores. No necesitas modificar el código que llama cuando añades una nueva clase de trabajador: solo asegúrate de que tenga un método calculate_payment().
La herencia y el polimorfismo son herramientas potentes para organizar el código y crear sistemas flexibles y extensibles. Al crear subclases, puedes reutilizar código existente mientras añades o modificas comportamiento. Al sobrescribir métodos, puedes personalizar cómo funcionan las subclases. Y al usar polimorfismo, puedes escribir código que funcione con muchas clases diferentes a través de una interfaz común.
La clave es usar estas características con criterio:
- Usa herencia(inheritance) cuando exista una relación genuina de “es un” (un
Doges unAnimal) - Sobrescribe métodos para especializar el comportamiento, no para cambiar por completo lo que hace una clase
- Usa
super()para extender el comportamiento del padre en lugar de reemplazarlo por completo - Prefiere el polimorfismo(polymorphism) (métodos comunes) antes que la comprobación de tipos cuando sea posible
A medida que construyas programas más grandes, estas técnicas orientadas a objetos te ayudarán a crear código que sea más fácil de entender, mantener y ampliar.