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.
# 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: BuddyAnche 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.
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:
# 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:
# 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:
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:
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.00Il 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à:
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)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.
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:
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.26544Il 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:
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: S12345Nota 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.
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:
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: EngineeringChiamando 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:
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: 1Il 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:
class Vehicle:
def __init__(self, brand):
self.brand = brand
class Car(Vehicle):
def __init__(self, brand, model):
super().__init__(brand) # Preferito
self.model = modelUsa 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:
class Car(Vehicle):
def __init__(self, brand, model):
Vehicle.__init__(self, brand) # Esplicito, ma meno flessibile
self.model = modelPer 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__:
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 ERROR32.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.
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:
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.
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 dataNessuna 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:
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: DLROWOLLEHLa 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:
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: FalseNota 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à:
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: FalseL'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:
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: FalseQuesto è 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:
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: TrueNota 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:
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: $8000Tuttavia, in molti casi, il polimorfismo (avere ogni classe che implementa un metodo comune) è migliore del controllo dei tipi:
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: $8000Questo 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è unAnimal) - 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.