Salta al contenuto
Note per Studenti Classi ed ereditarietà in Python

Classi ed ereditarietà in Python

In questa pagina 8
In questa pagina 6

Completa la sezione sulle classi di Tipi di dato astrattiUn ADT è definito dalle operazioni e dal loro comportamento, non dalla rappresentazione; interfaccia e implementazione; realizzazione in Python con le classi e in C con header e tipo opaco; esempio di un ADT Frazione.Tipi di dato astratti →: qui l'oggetto non è più un semplice contenitore di dati ma un tipo che si specializza.

Attributi e metodi

python
class Conto:
    n_conti = 0                       # attributo di CLASSE: uno solo, condiviso

    def __init__(self, titolare, saldo=0):
        self.titolare = titolare      # attributi di ISTANZA: uno per oggetto
        self.saldo = saldo
        Conto.n_conti += 1

    def versa(self, x):               # metodo di istanza: riceve self
        self.saldo += x

    @classmethod
    def vuoto(cls, titolare):         # riceve la classe: costruttore alternativo
        return cls(titolare, 0)

    @staticmethod
    def importo_valido(x):            # non riceve né self né cls
        return x > 0


a = Conto("Ada", 100)
b = Conto.vuoto("Bob")
print(Conto.n_conti, a.n_conti)       # 2 2
print(Conto.importo_valido(-5))       # False
  • Un attributo di classe è l'analogo di un attributo static di altri linguaggi: si legge anche da un'istanza, ma a.n_conti = 7 crea un attributo di istanza che oscura quello di classe senza modificarlo.
  • @staticmethod è una funzione "dentro" la classe solo per raggruppamento; @classmethod serve per costruttori alternativi, che con l'ereditarietà creano la sottoclasse giusta.
  • Un metodo di istanza ha sempre un parametro implicito, self; se manca nella definizione, la chiamata a.versa(5) fallisce con TypeError.

Uguaglianza, ordine, stampa

Metodo Usato da
__eq__(self, altro) ==, !=, in, list.index
__lt__(self, altro) <, sorted, min, max, list.sort
__str__(self) print, str (testo per l'utente)
__repr__(self) rappresentazione per il programmatore (REPL, debugger, elementi di una lista stampata)
python
from functools import total_ordering

@total_ordering                       # da __eq__ e __lt__ ricava <=, >, >=
class Misura:
    def __init__(self, t, v):
        self.t, self.v = t, v

    def __eq__(self, altra):
        return self.t == altra.t
    def __lt__(self, altra):          # ordine naturale: per tempo
        return self.t < altra.t
    def __repr__(self):
        return f"({self.t} {self.v})"


m = [Misura(3, 1.5), Misura(1, 2.5), Misura(2, 0.5)]
print(sorted(m))                      # [(1 2.5), (2 0.5), (3 1.5)]
print(max(m))                         # (3 1.5)

L'ordine "naturale" di una classe si decide una volta, in __lt__; altri ordinamenti si ottengono con sorted(m, key=lambda x: x.v). Se si ridefinisce __eq__ senza __hash__, l'oggetto diventa non hashable (non può essere chiave di un dict).

Proprietà

@property fa sembrare un attributo un metodo con controllo:

python
class Cerchio:
    def __init__(self, r):
        self.raggio = r               # passa dal setter

    @property
    def raggio(self):
        return self._r

    @raggio.setter
    def raggio(self, r):
        if r < 0:
            raise ValueError("raggio negativo")
        self._r = r

    @property
    def area(self):                   # sola lettura: calcolata ogni volta
        return 3.14159 * self._r ** 2

È il modo di mantenere un invariante (Tipi di dato astrattiUn ADT è definito dalle operazioni e dal loro comportamento, non dalla rappresentazione; interfaccia e implementazione; realizzazione in Python con le classi e in C con header e tipo opaco; esempio di un ADT Frazione.Tipi di dato astratti →) senza costringere chi usa la classe a scrivere c.get_raggio().

Ereditarietà

class Sotto(Sopra): la sottoclasse eredita attributi e metodi, può aggiungerne e ridefinirli (override: stesso nome, stessa interfaccia). super() raggiunge la versione della superclasse.

python
class Archivio:
    def __init__(self):
        self._righe = {}

    def inserisci(self, chiave, valore):
        self._righe[chiave] = valore

    def taglia(self):
        return len(self._righe)


class Squadra(Archivio):                       # "Squadra è un Archivio" (is-a)
    MAX = 3

    def inserisci(self, chiave, valore):       # override con un vincolo in più
        if chiave not in self._righe and self.taglia() >= Squadra.MAX:
            raise ValueError("squadra al completo")
        super().inserisci(chiave, valore)      # riusa il codice ereditato


s = Squadra()
for g in "abc":
    s.inserisci(g, 1)
print(s.taglia(), isinstance(s, Archivio), issubclass(Squadra, Archivio))   # 3 True True
  • Il costruttore di una sottoclasse che ne definisce uno proprio deve chiamare super().__init__(...), altrimenti gli attributi della superclasse non vengono creati.
  • Una sottoclasse eredita tutto; in Python non ci sono membri davvero privati, solo la convenzione _nome (e __nome, che cambia il nome interno per evitare collisioni tra classi).
  • isinstance(x, C) è vera anche per le istanze delle sottoclassi di C; type(x) == C no.
  • Si eredita per una relazione is-a; per "ha un" (una Squadra ha un archivio di giocatori) si usa la composizione: un attributo che contiene l'altro oggetto.

Polimorfismo

Lo stesso messaggio, comportamenti diversi: il metodo eseguito dipende dalla classe effettiva dell'oggetto, scelta a tempo di esecuzione.

python
class Forma:
    def area(self):
        raise NotImplementedError

class Quadrato(Forma):
    def __init__(self, l): self.l = l
    def area(self): return self.l ** 2

class Cerchio2(Forma):
    def __init__(self, r): self.r = r
    def area(self): return 3.14159 * self.r ** 2

print([round(f.area(), 2) for f in (Quadrato(2), Cerchio2(1))])   # [4, 3.14]

Python non richiede nemmeno la superclasse comune: basta che l'oggetto abbia il metodo (duck typing: "se cammina come un'anatra...").

Classi astratte e interfacce

Un'interfaccia è l'elenco dei metodi che una classe deve fornire, senza codice. In Python si ottiene con abc:

python
from abc import ABC, abstractmethod

class Pila(ABC):                     # "interfaccia" dell'ADT pila
    @abstractmethod
    def push(self, x): ...
    @abstractmethod
    def pop(self): ...
    @abstractmethod
    def is_empty(self): ...


class PilaLista(Pila):               # un'implementazione
    def __init__(self): self._d = []
    def push(self, x): self._d.append(x)
    def pop(self): return self._d.pop()
    def is_empty(self): return not self._d


# Pila()            -> TypeError: non si istanzia una classe astratta
p = PilaLista(); p.push(1); print(p.pop())    # 1

Una classe che non ridefinisce tutti i metodi astratti resta astratta e non si può istanziare. Il codice che usa solo i metodi di Pila funziona con qualunque implementazione (Pila e codaPila (LIFO) e coda (FIFO) come ADT: operazioni e costi; realizzazione in Python con list e collections.deque; realizzazione in C con array e indici, coda circolare; applicazioni (parentesi bilanciate, stack delle chiamate, visite).Pila e coda →).

Eccezioni personalizzate

Una nuova eccezione è una classe che eredita da Exception (o da una più specifica); il tipo basta a distinguere l'errore:

python
class PilaVuota(Exception):
    """pop o top su una pila vuota"""

def top(d):
    if not d:
        raise PilaVuota("la pila è vuota")
    return d[-1]

try:
    top([])
except PilaVuota as e:
    print("errore:", e)             # errore: la pila è vuota

Un except per una classe cattura anche le sue sottoclassi: except Exception cattura quasi tutto; ereditare da LookupError (come KeyError, IndexError) rende l'errore catturabile da chi gestisce errori di ricerca (vedi Eccezioni in PythonEccezioni e traceback, eccezioni predefinite più comuni, try/except/else/finally, raise per segnalare errori, propagazione lungo le chiamate.Eccezioni in Python →).

Overloading e shadowing

  • Python non ha overloading per tipo/numero di parametri: una seconda def con lo stesso nome sostituisce la prima. Si usano parametri di default (def f(a, b=0)) o *args.
  • Shadowing (oscuramento): un nome locale nasconde uno più esterno con lo stesso nome. Un attributo di istanza nasconde l'attributo di classe omonimo; un parametro nasconde una variabile globale. Se in una funzione si assegna a un nome, quel nome è locale in tutta la funzione: leggerlo prima dell'assegnazione solleva UnboundLocalError.
python
x = 10
def f():
    print(x)        # UnboundLocalError: x è locale per via dell'assegnazione sotto
    x = 5

Errori tipici

Versione ripasso

Attributi e metodi

  • Attributo di classe (uno solo, condiviso, come static) e di istanza (self.x, uno per oggetto): a.n = 7 crea un attributo di istanza che oscura quello di classe.
  • Metodo di istanza (self), @classmethod (riceve cls, costruttori alternativi), @staticmethod (né self né cls).

Uguaglianza, ordine, stampa

Ereditarietà

  • class Sotto(Sopra): eredita tutto; override = stesso nome; super().metodo(...) richiama la versione della superclasse; il costruttore deve chiamare super().__init__(...).
  • Is-a → ereditarietà; "ha un" → composizione. isinstance(x, C) vale anche per le sottoclassi.
  • Polimorfismo: il metodo eseguito dipende dalla classe effettiva dell'oggetto (duck typing: basta avere il metodo).

Classi astratte e interfacce

ABC e @abstractmethod: elenco dei metodi da fornire; non si istanzia una classe astratta; chi non ridefinisce tutti i metodi astratti resta astratto (Pila e codaPila (LIFO) e coda (FIFO) come ADT: operazioni e costi; realizzazione in Python con list e collections.deque; realizzazione in C con array e indici, coda circolare; applicazioni (parentesi bilanciate, stack delle chiamate, visite).Pila e coda →).

Eccezioni personalizzate

class PilaVuota(Exception); except cattura anche le sottoclassi; ereditare da LookupError per renderla un errore di ricerca (Eccezioni in PythonEccezioni e traceback, eccezioni predefinite più comuni, try/except/else/finally, raise per segnalare errori, propagazione lungo le chiamate.Eccezioni in Python →).

Overloading e shadowing

Nessun overloading: una seconda def sostituisce la prima (parametri di default, *args). Un nome assegnato in una funzione è locale in tutta la funzione (UnboundLocalError se letto prima).

Errori tipici: self dimenticato; super().__init__() omesso; default mutabile; __eq__ senza __hash__; self.n += 1 su un attributo di classe; ereditarietà senza relazione is-a.

Esercizi su questo argomento