Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 è una Persona → ha senso usare l’ereditarietà: Studente deriva da Persona.
  • Un Cerchio è una Forma → ha senso usare l’ereditarietà: Cerchio deriva da Forma.
  • Un’Automobile ha un Motore → ha senso usare la composizione: Automobile contiene un oggetto Motore.
  • Una Rubrica ha una lista di Contatti → ha senso usare la composizione: Rubrica contiene oggetti Contatto.

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’oggetto Libro con quel titolo, o None se non trovato;
  • libriDiPiu(n): ritorna la lista dei libri con più di n pagine.

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