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
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
staticdi altri linguaggi: si legge anche da un'istanza, maa.n_conti = 7crea un attributo di istanza che oscura quello di classe senza modificarlo. @staticmethodè una funzione "dentro" la classe solo per raggruppamento;@classmethodserve 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 chiamataa.versa(5)fallisce conTypeError.
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) |
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:
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.
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 diC;type(x) == Cno.- Si eredita per una relazione is-a; per "ha un" (una
Squadraha 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.
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:
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()) # 1Una 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:
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 è vuotaUn 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
defcon 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.
x = 10
def f():
print(x) # UnboundLocalError: x è locale per via dell'assegnazione sotto
x = 5Errori tipici
- Dimenticare
selfnella definizione dei metodi o davanti agli attributi. - Non chiamare
super().__init__()in una sottoclasse con un costruttore proprio. - Usare un valore mutabile come default (
def __init__(self, v=[])): la lista è la stessa per tutte le istanze (vedi Funzioni in Pythondef e return, parametri posizionali, con nome e con valore predefinito, *args e **kwargs; passaggio per riferimento a oggetto; ambito delle variabili LEGB; docstring, type hint, lambda.Funzioni in Python →); si usaNonee si crea la lista nel corpo. - Ridefinire
__eq__e aspettarsi che l'oggetto sia ancora usabile come chiave di dizionario. - Confondere attributo di classe e di istanza:
self.n += 1su un attributo di classe crea un attributo di istanza. - Ereditare per riusare codice quando non c'è una relazione is-a: meglio la composizione.
Versione ripasso
Attributi e metodi
- Attributo di classe (uno solo, condiviso, come
static) e di istanza (self.x, uno per oggetto):a.n = 7crea un attributo di istanza che oscura quello di classe. - Metodo di istanza (
self),@classmethod(ricevecls, costruttori alternativi),@staticmethod(néselfnécls).
Uguaglianza, ordine, stampa
__eq__per==,in;__lt__per<,sorted,min,max(con@total_orderingsi ricavano gli altri confronti);__str__perprint;__repr__per il programmatore. Altri ordini:sorted(x, key=...).- Ridefinire
__eq__senza__hash__rende l'oggetto non hashable. @property(+@nome.setter) mantiene l'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 →) con la sintassi di un attributo.
Ereditarietà
class Sotto(Sopra): eredita tutto; override = stesso nome;super().metodo(...)richiama la versione della superclasse; il costruttore deve chiamaresuper().__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.