Python & AI Tutorials Logo
Programmazione Python

32. Estendere le classi con ereditarietà e polimorfismo

Nel Capitolo 30, abbiamo imparato a creare le nostre classi per modellare concetti del mondo reale. Abbiamo costruito classi come BankAccount e Student che raggruppavano dati e comportamento insieme. Ma cosa succede quando devi creare una nuova classe simile a una esistente, ma con alcune differenze o aggiunte?

L'ereditarietà(inheritance) è il meccanismo di Python per creare nuove classi basate su quelle esistenti. Invece di copiare e incollare codice, puoi creare una sottoclasse(subclass) che ottiene automaticamente tutti gli attributi e i metodi da una classe padre(parent class) (chiamata anche classe base(base class) o superclasse(superclass)), e poi aggiungere o modificare ciò di cui hai bisogno.

Questo capitolo esplora come l'ereditarietà ti consenta di costruire gerarchie di classi correlate, come personalizzare il comportamento ereditato e come il polimorfismo(polymorphism) permetta a classi diverse di essere usate in modo intercambiabile quando condividono interfacce comuni.

32.1) Creare sottoclassi a partire da classi esistenti

32.1.1) La sintassi di base dell'ereditarietà

L'ereditarietà consente di creare una nuova classe (chiamata sottoclasse(subclass) o classe figlia(child class)) basata su una classe esistente (chiamata classe padre(parent class) o classe base(base class)). La sottoclasse eredita automaticamente tutti i metodi e gli attributi dalla classe padre.

Quando crei una sottoclasse, specifichi la classe padre tra parentesi dopo il nome della classe.

python
# Classe padre
class Animal:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        return f"{self.name} makes a sound"
 
# Sottoclasse che eredita da Animal
class Dog(Animal):
    pass  # Non serve ancora alcun codice aggiuntivo
 
# Crea un'istanza di Dog
buddy = Dog("Buddy")
print(buddy.speak())  # Output: Buddy makes a sound
print(buddy.name)     # Output: Buddy

Anche se Dog non ha codice proprio (solo pass), eredita tutto da Animal. La classe Dog ha automaticamente il metodo __init__ e il metodo speak dal suo genitore.

Animal

+name

+init(name)

+speak()

Dog

32.1.2) Perché l'ereditarietà è importante

L'ereditarietà risolve un problema comune nella programmazione: la duplicazione del codice. Immagina di costruire un sistema per gestire diversi tipi di dipendenti:

python
# Senza ereditarietà - tanta duplicazione
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})"

Nota come name, employee_id e get_info() sono duplicati. Con l'ereditarietà, possiamo eliminare questa duplicazione:

python
# Con ereditarietà - codice condiviso nella classe 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)  # Chiama __init__ del padre
        # Nota: Impareremo un modo migliore per farlo con super() nella Sezione 32.3
        self.salary = salary
 
class PartTimeEmployee(Employee):
    def __init__(self, name, employee_id, hourly_rate):
        Employee.__init__(self, name, employee_id)  # Chiama __init__ del padre
        self.hourly_rate = hourly_rate
 
# Entrambe le sottoclassi ereditano 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)

Ora gli attributi e i metodi comuni vivono in Employee, e ciascuna sottoclasse definisce solo ciò che la rende unica.

32.1.3) Aggiungere nuovi metodi alle sottoclassi

Le sottoclassi possono aggiungere i propri metodi che il padre non ha:

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):  # Nuovo metodo specifico di 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):  # Nuovo metodo specifico di Motorcycle
        return "Vroom vroom!"
 
# Ogni sottoclasse ha i metodi del padre più i propri
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 classe Car ha sia get_description() (ereditato) sia honk() (proprio). La classe Motorcycle ha get_description() (ereditato) e rev_engine() (proprio).

32.1.4) Aggiungere nuovi attributi alle sottoclassi

Le sottoclassi possono anche aggiungere i propri attributi di istanza. Di solito lo fai nel metodo __init__ della sottoclasse:

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  # Nuovo attributo
    
    def apply_interest(self):  # Nuovo metodo che usa il nuovo attributo
        interest = self.balance * self.interest_rate
        self.balance += interest
        return interest
 
# SavingsAccount ha tutti gli attributi di BankAccount più interest_rate
savings = SavingsAccount("SA001", 1000, 0.03)
savings.deposit(500)  # Metodo ereditato
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

Il SavingsAccount ha account_number e balance da BankAccount, più il proprio attributo interest_rate.

32.1.5) Più livelli di ereditarietà

Le classi possono ereditare da classi che a loro volta ereditano da altre classi, creando una gerarchia di ereditarietà:

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 eredita da Animal, che eredita da LivingThing
max_dog = Dog("Max", "Golden Retriever")
 
# I metodi di tutti e tre i livelli funzionano
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)
 
# Gli attributi di tutti e tre i livelli esistono
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 eredita da Animal, che eredita da LivingThing. Ciò significa che Dog ha accesso a metodi e attributi da entrambe le classi padre.

32.2) Fare override dei metodi nelle sottoclassi

32.2.1) Cosa significa fare override di un metodo

A volte una sottoclasse deve cambiare il modo in cui funziona un metodo ereditato. L'override dei metodi(method overriding) significa definire un metodo nella sottoclasse con lo stesso nome di un metodo nella classe padre. Quando chiami quel metodo su un'istanza della sottoclasse, Python usa la versione della sottoclasse invece di quella 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):  # Override del metodo speak del padre
        return f"{self.name} says: Woof!"
 
class Cat(Animal):
    def speak(self):  # Override con comportamento diverso
        return f"{self.name} says: Meow!"
 
# Ogni classe ha la propria versione di 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!

Quando chiami buddy.speak(), Python cerca prima il metodo speak nella classe Dog, dato che buddy è un'istanza di Dog. Poiché Dog definisce il proprio metodo speak, Python usa quella versione. Se Dog non avesse un metodo speak, Python cercherebbe poi nella classe padre Animal e userebbe invece quella versione.

Questo ordine di ricerca—partendo dalla classe dell'istanza e poi passando alla classe padre—è il modo in cui funziona l'override dei metodi e come le sottoclassi personalizzano il comportamento ereditato.

32.2.2) Perché fare override dei metodi?

L'override dei metodi ti permette di creare versioni specializzate di un comportamento generale. Considera una gerarchia di forme:

python
class Shape:
    def __init__(self, name):
        self.name = name
    
    def area(self):
        return 0  # Implementazione predefinita
    
    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):  # Override con un calcolo specifico del rettangolo
        return self.width * self.height
 
class Circle(Shape):
    def __init__(self, radius):
        Shape.__init__(self, "Circle")
        self.radius = radius
    
    def area(self):  # Override con un calcolo specifico del cerchio
        return 3.14159 * self.radius ** 2
 
# Ogni forma calcola l'area in modo diverso
rect = Rectangle(5, 3)
print(rect.describe())  # Output: Rectangle with area 15
 
circle = Circle(4)
print(circle.describe())  # Output: Circle with area 50.26544

Il metodo describe() è ereditato da entrambe le sottoclassi e funziona correttamente perché ogni sottoclasse fornisce la propria implementazione di area().

Quando chiami rect.describe(), il metodo ereditato describe() viene eseguito, ma self si riferisce all'istanza Rectangle. Quindi, quando describe() chiama self.area(), Python cerca prima area() nella classe Rectangle e trova la versione in override.

32.2.3) Fare override di __init__ e chiamare l'inizializzazione del padre

Quando fai override di __init__, in genere devi chiamare __init__ del padre per assicurarti che l'inizializzazione del padre avvenga:

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):
        # Chiama __init__ del padre per impostare name e age
        Person.__init__(self, name, age)
        # Poi imposta gli attributi specifici dello studente
        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

Nota come Student.__init__ chiami prima Person.__init__(self, name, age) per inizializzare gli attributi della classe padre, e poi aggiunga i propri attributi.

32.3) Usare super() per accedere al comportamento del padre

32.3.1) Cosa fa super()

Nella sezione precedente, abbiamo chiamato esplicitamente i metodi del padre: ParentClass.method(self, ...). Python fornisce un modo più pulito: la funzione super(). super() restituisce un oggetto temporaneo che ti consente di chiamare metodi della classe padre senza nominare esplicitamente il 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 __init__(self, name, breed):
        super().__init__(name)  # Più pulito di Animal.__init__(self, name)
        self.breed = breed
    
    def speak(self):
        parent_sound = super().speak()  # Chiama speak() del padre
        return f"{parent_sound} - specifically, Woof!"
 
buddy = Dog("Buddy", "Labrador")
print(buddy.speak())
# Output: Buddy makes a sound - specifically, Woof!

Usare super() ha diversi vantaggi:

  • Non devi nominare esplicitamente la classe padre
  • Funziona correttamente con l'ereditarietà multipla (trattata più avanti)
  • Rende il codice più facile da mantenere se cambi la classe padre

32.3.2) Usare super() in __init__

L'uso più comune di super() è chiamare __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)  # Inizializza gli attributi del padre
        self.department = department
        self.team = []  # Attributo specifico di Manager
    
    def add_team_member(self, employee):
        self.team.append(employee)
 
# Manager ottiene tutti gli attributi di Employee più i propri
sarah = Manager("Sarah", "M001", "Engineering")
print(sarah.name)        # Output: Sarah
print(sarah.is_active)   # Output: True
print(sarah.department)  # Output: Engineering

Chiamando super().__init__(name, employee_id), la classe Manager assicura che tutta la logica di inizializzazione di Employee venga eseguita, inclusa l'impostazione di is_active a True.

32.3.3) Estendere i metodi del padre con super()

Puoi usare super() per estendere un metodo del padre invece di sostituirlo completamente:

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
        
        # Chiama deposit del padre per gestire la logica di base
        new_balance = super().deposit(amount)
        
        # Aggiungi comportamento specifico del conto corrente
        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

Il metodo CheckingAccount.deposit() chiama super().deposit(amount) per gestire la logica di deposito di base (aggiornando il saldo e il conteggio delle transazioni), e poi aggiunge il proprio controllo sullo stato di scoperto.

32.3.4) Quando usare super() vs chiamata diretta al padre

Usa super() nella maggior parte dei casi:

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

Usa chiamate dirette al padre quando devi chiamare un padre specifico in uno scenario di ereditarietà multipla (trattata più avanti) o quando vuoi essere esplicito su quale padre stai chiamando:

python
class Car(Vehicle):
    def __init__(self, brand, model):
        Vehicle.__init__(self, brand)  # Esplicito, ma meno flessibile
        self.model = model

Per l'ereditarietà singola (un solo padre), super() è quasi sempre la scelta migliore.

32.3.5) super() con altri metodi

Puoi usare super() con qualsiasi metodo, non solo con __init__:

python
class TextProcessor:
    def process(self, text):
        # Elaborazione di base: rimuovere gli spazi ai bordi
        return text.strip()
 
class UppercaseProcessor(TextProcessor):
    def process(self, text):
        # Prima fai l'elaborazione del padre
        processed = super().process(text)
        # Poi aggiungi la conversione in maiuscolo
        return processed.upper()
 
class PrefixProcessor(UppercaseProcessor):
    def __init__(self, prefix):
        self.prefix = prefix
    
    def process(self, text):
        # Prima fai l'elaborazione del padre (che chiama anche il suo padre)
        processed = super().process(text)
        # Poi aggiungi il prefisso
        return f"{self.prefix}: {processed}"
 
processor = PrefixProcessor("ALERT")
result = processor.process("  system error  ")
print(result)  # Output: ALERT: SYSTEM ERROR

32.4) Polimorfismo: lavorare con classi compatibili

32.4.1) Cosa significa polimorfismo

Il polimorfismo(polymorphism) (dal greco: "molte forme") è la capacità di trattare allo stesso modo oggetti di classi diverse se forniscono gli stessi metodi.

In Python, se più classi hanno metodi con lo stesso nome, puoi chiamare quei metodi senza conoscere la classe esatta dell'oggetto.

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!"
 
# Funzione che funziona con qualsiasi oggetto che abbia un metodo speak()
def make_animal_speak(animal):
    print(animal.speak())
 
# Funziona con classi diverse
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 funzione make_animal_speak() non si preoccupa di quale sia la classe del parametro animal—ha solo bisogno che l'oggetto abbia un metodo speak(). Questo è il polimorfismo in azione.

32.4.2) Polimorfismo con ereditarietà

Il polimorfismo è particolarmente potente con l'ereditarietà, in cui le sottoclassi fanno override dei metodi 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}"
 
# Funzione che funziona con qualsiasi PaymentMethod
def complete_purchase(payment_method, amount):
    print(payment_method.process_payment(amount))
    print("Purchase complete!")
 
# Tutti i metodi di pagamento funzionano con la stessa funzione
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 funzione complete_purchase() funziona con qualsiasi sottoclasse di PaymentMethod. Ogni sottoclasse fornisce la propria implementazione di process_payment(), ma la funzione non ha bisogno di sapere con quale classe specifica sta lavorando.

32.4.3) Duck typing: "Se cammina come un'anatra..."

Il polimorfismo di Python non richiede che le classi siano correlate tramite ereditarietà. Questo si chiama duck typing(duck typing): "Se cammina come un'anatra e starnazza come un'anatra, allora è un'anatra." In altre parole, Python si interessa di ciò che un oggetto può fare(i suoi metodi), non di ciò che è (la sua classe). Se un oggetto ha i metodi di cui hai bisogno, puoi usarlo, indipendentemente dalla gerarchia delle classi.

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}")
 
# Funzione che funziona con qualsiasi oggetto che abbia un metodo write()
def save_data(writer, data):
    writer.write(data)
 
# Tutte e tre le classi funzionano, anche se non sono correlate
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

Nessuna di queste classi eredita da un padre comune, ma tutte funzionano con save_data() perché tutte hanno un metodo write(). Questo è il duck typing: la funzione non si interessa della classe, ma solo dell'interfaccia (i metodi disponibili).

32.4.4) Esempio pratico: un sistema di plugin

Il polimorfismo abilita sistemi flessibili ed estensibili. Ecco un semplice sistema di plugin per l'elaborazione dei dati:

python
class DataProcessor:
    def process(self, data):
        return data  # L'implementazione base non fa nulla
 
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)  # Chiamata polimorfica
        return result
 
# Costruisci una pipeline di elaborazione
pipeline = DataPipeline()
pipeline.add_processor(UppercaseProcessor())
pipeline.add_processor(RemoveSpacesProcessor())
pipeline.add_processor(ReverseProcessor())
 
# Elabora i dati attraverso la pipeline
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

La DataPipeline non ha bisogno di sapere quali processori specifici contenga—chiama semplicemente process() su ciascuno. Puoi aggiungere facilmente nuovi tipi di processore senza cambiare il codice della pipeline.

32.5) Verificare tipi e relazioni tra classi (isinstance, issubclass)

32.5.1) Verificare i tipi di istanza con isinstance()

A volte devi controllare se un oggetto è un'istanza di una particolare classe. La funzione isinstance() lo fa:

python
class Animal:
    pass
 
class Dog(Animal):
    pass
 
class Cat(Animal):
    pass
 
buddy = Dog()
whiskers = Cat()
 
# Controlla se un oggetto è un'istanza di una classe
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

Nota che isinstance(buddy, Animal) restituisce True anche se buddy è un'istanza di Dog. Questo perché Dog eredita da Animal, quindi un'istanza di Dog è considerata anche un'istanza di Animal.

32.5.2) Perché isinstance() rispetta l'ereditarietà

La funzione isinstance() controlla l'intera catena di ereditarietà:

python
class Vehicle:
    pass
 
class Car(Vehicle):
    pass
 
class ElectricCar(Car):
    pass
 
tesla = ElectricCar()
 
# Controlla tutti i livelli di ereditarietà
print(isinstance(tesla, ElectricCar))  # Output: True
print(isinstance(tesla, Car))          # Output: True
print(isinstance(tesla, Vehicle))      # Output: True
print(isinstance(tesla, str))          # Output: False

è un

è un

è un

istanza tesla

ElectricCar

Car

Vehicle

L'oggetto tesla è un'istanza di ElectricCar, ma è anche un'istanza di Car e Vehicle a causa dell'ereditarietà.

32.5.3) Controllare più tipi contemporaneamente

Puoi controllare se un oggetto è un'istanza di una qualsiasi tra diverse classi passando 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

Questo è più conciso che scrivere isinstance(animal, Dog) or isinstance(animal, Cat) or isinstance(animal, Bird).

32.5.4) Verificare relazioni tra classi con issubclass()

La funzione issubclass() controlla se una classe è una sottoclasse di un'altra:

python
class Animal:
    pass
 
class Dog(Animal):
    pass
 
class Cat(Animal):
    pass
 
class Poodle(Dog):
    pass
 
# Controlla le relazioni tra classi
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 classe è considerata sottoclasse di se stessa
print(issubclass(Dog, Dog))       # Output: True

Nota che issubclass() funziona con le classi, non con le istanze. Usa isinstance() per le istanze e issubclass() per le classi.

32.5.5) Casi d'uso pratici per il controllo dei tipi

Il controllo dei tipi è utile quando ti serve un comportamento diverso per tipi diversi:

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):
    # Quando usi isinstance() con l'ereditarietà, controlla le sottoclassi prima delle classi padre
    if isinstance(worker, Manager):  # Controlla prima Manager
        return worker.salary + worker.bonus
    elif isinstance(worker, Employee):  # Poi controlla Employee (classe 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

Tuttavia, in molti casi, il polimorfismo (avere ogni classe che implementa un metodo comune) è migliore del controllo dei tipi:

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
 
# Nessun controllo dei tipi necessario - il polimorfismo lo gestisce
workers = [
    Employee("Alice", 50000),
    Manager("Bob", 70000, 10000),
    Contractor("Charlie", 50, 160)
]
 
for worker in workers:
    payment = worker.calculate_payment()  # Chiamata polimorfica
    print(f"{worker.name}'s payment: ${payment}")

Output:

Alice's payment: $50000
Bob's payment: $80000
Charlie's payment: $8000

Questo approccio polimorfico è più flessibile e più facile da estendere con nuovi tipi di lavoratore. Non devi modificare il codice chiamante quando aggiungi una nuova classe di lavoratore—assicurati solo che abbia un metodo calculate_payment().


L'ereditarietà e il polimorfismo sono strumenti potenti per organizzare il codice e creare sistemi flessibili ed estensibili. Creando sottoclassi, puoi riutilizzare codice esistente aggiungendo o modificando comportamento. Facendo override dei metodi, puoi personalizzare il funzionamento delle sottoclassi. E usando il polimorfismo, puoi scrivere codice che funziona con molte classi diverse tramite un'interfaccia comune.

La chiave è usare queste funzionalità con criterio:

  • Usa l'ereditarietà quando c'è una reale relazione di tipo "è-un" (un Dog è un Animal)
  • Fai override dei metodi per specializzare il comportamento, non per cambiare completamente ciò che fa una classe
  • Usa super() per estendere il comportamento del padre invece di sostituirlo interamente
  • Preferisci il polimorfismo (metodi comuni) al controllo dei tipi quando possibile

Man mano che costruisci programmi più grandi, queste tecniche orientate agli oggetti ti aiuteranno a creare codice più facile da capire, mantenere ed estendere.


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