Composizione
Nei capitoli precedenti abbiamo visto come l’ereditarietà permetta di costruire nuove classi a partire da classi esistenti. È uno strumento potente, ma non è l’unico modo di mettere in relazione le classi fra loro.
In questo capitolo vediamo un secondo modo, altrettanto importante: la composizione.
È un… oppure Ha un…?
La differenza fra ereditarietà e composizione si capisce meglio partendo dal linguaggio naturale.
Riprendiamo le classi che abbiamo già incontrato:
- Uno
Studenteè unaPersona→ ha senso usare l’ereditarietà:Studentederiva daPersona. - Un
Cerchioè unaForma→ ha senso usare l’ereditarietà:Cerchioderiva daForma. - Un’
Automobileha unMotore→ ha senso usare la composizione:Automobilecontiene un oggettoMotore. - Una
Rubricaha una lista diContatti→ ha senso usare la composizione:Rubricacontiene oggettiContatto.
La regola è semplice:
Note
Is-a vs Has-a
- Se fra due classi vale la relazione “è un” (is-a), si usa l’ereditarietà.
- Se fra due classi vale la relazione “ha un” (has-a), si usa la composizione.
La composizione si realizza semplicemente creando un oggetto come attributo di un altro oggetto. Non c’è nessuna sintassi speciale: è la stessa cosa che abbiamo fatto dall’inizio del corso con gli attributi, solo che stavolta il valore dell’attributo non è un intero o una stringa, ma un oggetto di un’altra classe.
Un primo esempio
Definiamo la classe Motore e la classe Automobile. Un’automobile ha un motore, quindi Automobile conterrà un oggetto Motore:
class Motore:
def __init__(self, cilindrata, cavalli):
self.cilindrata = cilindrata
self.cavalli = cavalli
def __str__(self):
return f"Motore {self.cilindrata}cc - {self.cavalli}cv"
def avvia(self):
return "Vroom!"
class Automobile:
def __init__(self, marca, modello, cilindrata, cavalli):
self.marca = marca
self.modello = modello
self.motore = Motore(cilindrata, cavalli) # composizione: Motore è un attributo
def __str__(self):
return f"{self.marca} {self.modello} ({self.motore})"
def parti(self):
return f"{self.marca} {self.modello}: {self.motore.avvia()}"
# -------------------------------------------
if __name__ == "__main__":
auto = Automobile("Fiat", "Panda", 1200, 69)
print(auto)
print(auto.parti())
print("Cavalli:", auto.motore.cavalli)
Output:
Fiat Panda (Motore 1200cc - 69cv)
Fiat Panda: Vroom!
Cavalli: 69
L’oggetto Motore vive dentro l’oggetto Automobile come attributo self.motore.
Per accedere ai suoi metodi e attributi si usa la notazione con il punto: auto.motore.avvia(), auto.motore.cavalli.
Composizione 1-a-molti
La composizione non riguarda solo il caso in cui un oggetto ne contiene uno solo: spessissimo un oggetto contiene
una lista di altri oggetti. È il caso, ad esempio, di una Libreria che contiene molti Libro, o di un Ordine
che contiene molte Voce.
Riprendiamo l’esercizio della Rubrica svolto precedentemente: a ben guardare, essa è già un esempio di composizione!
Una rubrica ha molti contatti. Riscriviamola esplicitando questa struttura:
class Contatto:
def __init__(self, nome, numero):
self.nome = nome
self.numero = numero
def __str__(self):
return f"{self.nome}: {self.numero}"
class Rubrica:
def __init__(self):
self.__contatti = [] # lista di oggetti Contatto
def __str__(self):
if not self.__contatti:
return "Rubrica vuota"
righe = ""
for c in self.__contatti:
righe += str(c) + "\n"
return righe
def inserisciContatto(self, nome, numero):
self.__contatti.append( Contatto(nome, numero) )
def trovaNumero(self, nome):
for c in self.__contatti:
if c.nome == nome:
return c.numero
return "NESSUN CONTATTO"
def rimuoviContatto(self, nome):
for c in self.__contatti:
if c.nome == nome:
self.__contatti.remove(c)
return True
return False
# -------------------------------------------
if __name__ == "__main__":
r = Rubrica()
r.inserisciContatto("Giacomo", "333-1234567")
r.inserisciContatto("Francesca", "347-9876543")
r.inserisciContatto("Prof", "555-0000000")
print(r)
print("Numero di Giacomo:", r.trovaNumero("Giacomo"))
r.rimuoviContatto("Prof")
print("--- dopo rimozione ---")
print(r)
Rispetto all’esercizio già svolto, l’unica differenza sostanziale è che la lista non contiene stringhe o dizionari,
ma oggetti Contatto. Il vantaggio è che ogni contatto porta con sé la propria logica e può essere esteso
(ad esempio aggiungendo mail, foto, note) senza toccare la classe Rubrica.
Quando si sbaglia: usare l’ereditarietà al posto della composizione
Un errore comune fra chi inizia è usare l’ereditarietà anche quando la relazione fra due classi è has-a, non is-a. Vediamo un esempio di cosa succede:
# SBAGLIATO: Automobile "è un" Motore? No! Ha un motore.
class Automobile(Motore):
def __init__(self, marca, modello, cilindrata, cavalli):
super().__init__(cilindrata, cavalli)
self.marca = marca
self.modello = modello
Questo codice funziona, ma è concettualmente sbagliato. La conseguenza pratica è che Automobile eredita tutto
quello che appartiene a Motore, comprese cose che non hanno senso per un’automobile (es. se Motore avesse
un metodo sostituisciPistoni()). Col tempo, questo tipo di errore rende il codice difficile da capire e da mantenere.
Tip
Prima di usare l’ereditarietà, fatevi sempre questa domanda: “Ha senso dire che un oggetto di questa classe è un oggetto dell’altra classe?”
Se la risposta è no, usate la composizione.
Composizione ed ereditarietà insieme
Composizione ed ereditarietà non si escludono: spesso una buona progettazione le usa entrambe.
from abc import ABC, abstractmethod
class Motore(ABC):
def __init__(self, cavalli):
self.cavalli = cavalli
@abstractmethod
def tipo_carburante(self):
pass
def __str__(self):
return f"Motore {self.tipo_carburante()} - {self.cavalli}cv"
class MotoreBenzina(Motore):
def tipo_carburante(self):
return "benzina"
class MotoreDiesel(Motore):
def tipo_carburante(self):
return "diesel"
class MotoreElettrico(Motore):
def tipo_carburante(self):
return "elettrico"
class Automobile:
def __init__(self, marca, modello, motore):
self.marca = marca
self.modello = modello
self.motore = motore # composizione: riceve un oggetto Motore già costruito
def __str__(self):
return f"{self.marca} {self.modello} ({self.motore})"
# -------------------------------------------
if __name__ == "__main__":
auto1 = Automobile("Fiat", "Panda", MotoreBenzina(69))
auto2 = Automobile("Volkswagen", "Golf", MotoreDiesel(115))
auto3 = Automobile("Tesla", "Model 3", MotoreElettrico(283))
parco = [auto1, auto2, auto3]
for auto in parco:
print(auto)
Output:
Fiat Panda (Motore benzina - 69cv)
Volkswagen Golf (Motore diesel - 115cv)
Tesla Model 3 (Motore elettrico - 283cv)
Qui Motore è una classe astratta con sottoclassi concrete (ereditarietà), e Automobile ha un Motore (composizione).
Le due tecniche lavorano insieme in modo naturale.
Esercizio svolto: Computer
Un Computer ha una CPU e ha una SchedaVideo. Definiamo le tre classi separatamente e le componiamo:
class CPU:
def __init__(self, produttore, modello, ghz):
self.produttore = produttore
self.modello = modello
self.ghz = ghz
def __str__(self):
return f"CPU {self.produttore} {self.modello} @ {self.ghz}GHz"
class SchedaVideo:
def __init__(self, produttore, modello, vram_gb):
self.produttore = produttore
self.modello = modello
self.vram_gb = vram_gb
def __str__(self):
return f"GPU {self.produttore} {self.modello} ({self.vram_gb}GB VRAM)"
class Computer:
def __init__(self, nome, cpu, scheda_video, ram_gb):
self.nome = nome
self.cpu = cpu # composizione
self.scheda_video = scheda_video # composizione
self.ram_gb = ram_gb
def __str__(self):
s = f"Computer: {self.nome}\n"
s += f" {self.cpu}\n"
s += f" {self.scheda_video}\n"
s += f" RAM: {self.ram_gb}GB"
return s
def adatto_al_gaming(self):
return self.scheda_video.vram_gb >= 8 and self.ram_gb >= 16
# -------------------------------------------
if __name__ == "__main__":
pc1 = Computer(
"PC Studio",
CPU("Intel", "Core i7-13700K", 5.4),
SchedaVideo("NVIDIA", "RTX 4070", 12),
32
)
pc2 = Computer(
"PC Ufficio",
CPU("AMD", "Ryzen 5 5600G", 4.4),
SchedaVideo("AMD", "Radeon RX 6400", 4),
16
)
for pc in [pc1, pc2]:
print(pc)
print(f" Adatto al gaming: {pc.adatto_al_gaming()}")
print()
Output:
Computer: PC Studio
CPU Intel Core i7-13700K @ 5.4GHz
GPU NVIDIA RTX 4070 (12GB VRAM)
RAM: 32GB
Adatto al gaming: True
Computer: PC Ufficio
CPU AMD Ryzen 5 5600G @ 4.4GHz
GPU AMD Radeon RX 6400 (4GB VRAM)
RAM: 16GB
Adatto al gaming: False
Esercizi di comprensione
Esercizio 1601
Definire la classe Indirizzo con attributi via, numero civico, città e CAP.
Definire la classe Persona con attributi nome, cognome e un oggetto Indirizzo come attributo residenza.
Definire 3 persone con indirizzi diversi e visualizzarle.
Esercizio 1602
Definire la classe Libro con attributi titolo, anno di pubblicazione e numero di pagine.
Definire la classe Libreria che contiene una lista di oggetti Libro.
Implementare i seguenti metodi:
aggiungiLibro(libro): aggiunge un libro alla libreria;rimuoviLibro(titolo): rimuove il libro con quel titolo, ritorna True o False;cercaLibro(titolo): ritorna l’oggettoLibrocon quel titolo, o None se non trovato;libriDiPiu(n): ritorna la lista dei libri con più dinpagine.
Definire una libreria, inserire almeno 5 libri e testare tutti i metodi.
Esercizio 1603
Riprendere la classe Contatto e Rubrica mostrate nel capitolo.
Aggiungere alla classe Contatto un terzo attributo mail, inizialmente stringa vuota.
Aggiungere alla classe Rubrica il metodo modificaNumero(nome, nuovoNumero) e trovaMail(nome).
Testare la rubrica con almeno 4 contatti.
Esercizio 1604
Definire la classe Squadra con attributi nome e una lista di oggetti Giocatore
(riprendere la classe Giocatore dell’esercizio 1111).
Implementare i seguenti metodi nella classe Squadra:
aggiungiGiocatore(giocatore): aggiunge un giocatore alla rosa;rimuoviGiocatore(nome): rimuove il giocatore con quel nome, ritorna True o False;giocatoriPerRuolo(ruolo): ritorna la lista dei giocatori con quel ruolo;rosa(): visualizza tutti i giocatori con nome, numero e ruolo.
Definire una squadra, inserire almeno 8 giocatori con ruoli diversi e testare tutti i metodi.
Esercizio 1605: DIFFICILE
Definire la classe Motore con attributi cilindrata e cavalli e metodo avvia().
Definire la classe astratta Veicolo (con i metodi astratti tipo() e descrivi())
che in __init__ riceve marca, modello e un oggetto Motore.
Derivare da Veicolo le classi Auto, Moto e Camion, ciascuna con la propria implementazione di tipo() e descrivi().
Creare almeno 2 oggetti per ogni tipo di veicolo con motori diversi e visualizzarli tutti.
Nota: questo esercizio combina composizione (il veicolo ha un motore) ed ereditarietà (Auto, Moto, Camion sono veicoli).