Python & AI Tutorials Logo
Programación Python

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.

python
# 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: Buddy

Aunque 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.

Animal

+name

+init(name)

+speak()

Dog

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:

python
# 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:

python
# 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:

python
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:

python
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.00

SavingsAccount 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:

python
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)

LivingThing

+name

+is_alive()

Animal

+species

+move()

Dog

+breed

+bark()

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.

python
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:

python
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.26544

El 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:

python
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: S12345

Fí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.

python
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:

python
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: Engineering

Al 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:

python
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: 1

El 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:

python
class Vehicle:
    def __init__(self, brand):
        self.brand = brand
 
class Car(Vehicle):
    def __init__(self, brand, model):
        super().__init__(brand)  # Preferido
        self.model = model

Usa 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:

python
class Car(Vehicle):
    def __init__(self, brand, model):
        Vehicle.__init__(self, brand)  # Explícito, pero menos flexible
        self.model = model

Para 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__:

python
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 ERROR

32.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.

python
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:

python
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.

python
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 data

Ninguna 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:

python
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: DLROWOLLEH

DataPipeline 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:

python
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: False

Fí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:

python
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: False

es un

es un

es un

instancia tesla

ElectricCar

Car

Vehicle

El 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:

python
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: False

Esto 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:

python
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: True

Ten 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:

python
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: $8000

Sin embargo, en muchos casos, el polimorfismo (hacer que cada clase implemente un método común) es mejor que la comprobación de tipos:

python
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: $8000

Este 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 Dog es un Animal)
  • 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.

© 2025. Primesoft Co., Ltd.
support@primesoft.ai