Classi — dati e comportamento insieme
`class`, `__init__` e `self`, metodi, una forma stampabile con `__repr__`, uguaglianza con `__eq__`, livello di classe contro livello d'istanza, e quando una classe non serve.
Come vuoi imparare?
Il problema che stiamo risolvendo
Finora abbiamo tenuto i vari fatti su una cosa in un dizionario.
order = {"name": "pen", "price": 15.0, "quantity": 3}
print(order["nmae"])KeyError: 'nmae'L'errore è stato catturato, che già è qualcosa — ma solo quando quella riga viene eseguita. Nascosto dentro una condizione, potrebbe apparire per la prima volta un mese dopo.
E il problema non è solo l'ortografia. Qualsiasi chiave può essere messa in un dizionario, quindi nulla da nessuna parte dice cosa una riga d'ordine dovrebbe contenere. La funzione che la totalizza è da un'altra parte, e nessuno controlla che il dizionario abbia ogni campo prima di chiamarla. I dati stanno in un posto, cosa si può fare con loro in un altro, e a chi appartiene cosa esiste solo nella tua testa.
class OrderLine:
def __init__(self, name, price, quantity):
self.name = name
self.price = price
self.quantity = quantity
def total(self):
return round(self.price * self.quantity, 2)
line = OrderLine("pen", 15.0, 3)
print(line.name)
print(line.total())pen
45.0Una classe è uno stampo: dice cosa una cosa di questo genere contiene, e cosa si può fare con essa.
Alla fine di questo capitolo sarai in grado di
- Scrivere una
class, e dire cosa fanno__init__eself - Scrivere metodi, e dire come differiscono dalle funzioni normali
- Dare a un oggetto una forma stampabile con
__repr__ - Dire cosa separa un attributo di classe da un attributo d'istanza — ed evitare una trappola nota
- Rendere due oggetti confrontabili con
__eq__ - Dire quando una classe non serve
Prerequisiti: JSON e CSV.
Cos'è davvero self
self non è una parola magica — è il primo parametro, e ciò che ci finisce dentro è l'oggetto su cui il metodo è stato chiamato.
Scrivere line.total() significa "esegui il metodo total di OrderLine, e passagli line come self". Per questo self.price dentro dà il prezzo di quella particolare linea.
Dimentica self e il messaggio all'inizio è confuso:
class OrderLine:
def __init__(self, name):
self.name = name
def shout():
return "hello"
line = OrderLine("pen")
print(line.shout())TypeError: OrderLine.shout() takes 0 positional arguments but 1 was given"1 was given" — ma le parentesi erano vuote. L'argomento passato è line stesso, silenziosamente, e shout non ha lasciato spazio per riceverlo.
__init__ è un metodo normale pure lui, con un nome speciale: Python lo chiama appena dopo che l'oggetto è stato creato. Dentro, self.name = name imposta gli attributi.
Un errore di ortografia produce un errore più chiaro
class OrderLine:
def __init__(self, name):
self.name = name
line = OrderLine("pen")
print(line.nmae)AttributeError: 'OrderLine' object has no attribute 'nmae'Per essere onesti: una classe non coglie questo errore presto. Un AttributeError, proprio come un KeyError, appare solo quando la riga viene eseguita. Nascosto dentro una condizione, anche questo potrebbe apparire per la prima volta un mese dopo.
Qual è allora il vantaggio? In tre posti.
- L'errore dice di più. Il messaggio nomina la classe —
'OrderLine' object— così vedi su che genere di cosa il nome sbagliato è stato cercato. - Cosa dovrebbe esserci è scritto in un posto solo. Leggere
__init__mostra quali attributi ha unOrderLine. Un dizionario non ha una lista del genere. - Il tuo editor può aiutare. Un editor legge la classe e suggerisce nomi di attributi dopo che scrivi
line., e molti editor sottolineano un nome sconosciuto — prima che il codice venga eseguito. Con le chiavi di dizionario un editor di solito non può farlo.
Ricorda anche il limite: un errore di ortografia non è catturato quando scrivi. Proprio come order["nmae"] = 5 crea silenziosamente una nuova chiave, line.nmae = 5 crea silenziosamente un nuovo attributo.
E cosa c'è dentro si può guardare:
class OrderLine:
def __init__(self, name):
self.name = name
line = OrderLine("pen")
print(type(line))
print(isinstance(line, OrderLine))
print(line.__dict__)<class '__main__.OrderLine'>
True
{'name': 'pen'}__dict__ mostra che c'è davvero un dizionario dentro l'oggetto. La classe non ha eliminato il dizionario — gli ha dato un nome e una forma.
__repr__ — cosa mostra la stampa
Senza un __repr__:
class OrderLine:
def __init__(self, name):
self.name = name
print(OrderLine("pen"))<__main__.OrderLine object at 0x79b769f03830>Il numero alla fine è un indirizzo di memoria, e cambia a ogni esecuzione — vedrai un numero diverso sul tuo schermo. In fase di debug non serve a nulla.
class OrderLine:
def __init__(self, name, price):
self.name = name
self.price = price
def __repr__(self):
return f"OrderLine(name={self.name!r}, price={self.price!r})"
line = OrderLine("pen", 15.0)
print(line)
print([line, line])OrderLine(name='pen', price=15.0)
[OrderLine(name='pen', price=15.0), OrderLine(name='pen', price=15.0)]La seconda riga è il vero vantaggio: funziona anche dentro una lista. Quando Python stampa una lista chiama __repr__ di ogni elemento, non il suo __str__. Per questo, dei due, __repr__ è quello da scrivere per primo.
La convenzione è restituire una stringa che sembra il codice che ha costruito l'oggetto. Usare !r mette le virgolette intorno alle stringhe dentro — per la stessa ragione del capitolo 24.
Uguaglianza — il default non è ciò che vuoi
class OrderLine:
def __init__(self, name):
self.name = name
a = OrderLine("pen")
b = OrderLine("pen")
print(a == b)
print(a == a)False
TrueI due oggetti contengono la stessa cosa, eppure == dà False. Perché l'uguaglianza di default è l'identità — == si comporta proprio come is, chiedendo "è esattamente lo stesso oggetto?".
Cambiarlo significa definire cosa significa "uguale":
class OrderLine:
def __init__(self, name):
self.name = name
def __eq__(self, other):
return self.name == other.name
a = OrderLine("pen")
b = OrderLine("pen")
print(a == b)
print(a is b)True
FalseOra == dice "simile" e is dice "lo stesso" — la distinzione del capitolo 21, questa volta in un tipo tuo.
Una cautela: scrivere __eq__ lascia l'oggetto non hashable di default, quindi non può andare in un insieme né essere chiave di dizionario. Il dataclass del prossimo capitolo fa entrambi i lavori in una riga.
Livello di classe e livello d'istanza
class Basket:
TAX_RATE = 0.15
items = []
def add(self, name):
self.items.append(name)
a = Basket()
b = Basket()
a.add("pen")
b.add("bag")
print(a.items)
print(b.items)
print(a.items is b.items)['pen', 'bag']
['pen', 'bag']
TrueDue cestini separati, ma una sola lista.
La riconosci? Questa è la trappola del default mutabile del capitolo 21, in un posto nuovo. Il items = [] appena sotto la riga class gira una volta sola, quando la classe è creata — non una volta per oggetto. Quindi la lista appartiene alla classe, e ogni istanza la condivide.
La soluzione è analoga: ciò che appartiene a ogni oggetto, costruisci dentro __init__.
class Basket:
TAX_RATE = 0.15
def __init__(self):
self.items = []
def add(self, name):
self.items.append(name)
a = Basket()
b = Basket()
a.add("pen")
b.add("bag")
print(a.items)
print(b.items)
print(a.TAX_RATE, b.TAX_RATE)['pen']
['bag']
0.15 0.15TAX_RATE è rimasto a livello di classe, e a ragione — è lo stesso per ogni cestino, e non intendiamo cambiarlo. Ma nota che Python non ti impedisce di cambiarlo: Basket.TAX_RATE = 0.2 non solleva errori. Il nome in maiuscolo è solo una convenzione che dice al lettore "tratta questo come una costante, non cambiarlo". La regola in una riga: costante a livello di classe; tutto ciò che cambia in __init__.
Quando non scrivere una classe
Una volta imparate le classi, è tentante fare di tutto una classe. Qualche segno che la classe non serve:
La classe ha solo un __init__ e un metodo. È una funzione, divisa in due senza motivo.
Nessun metodo, solo dati. Il dataclass del prossimo capitolo è progettato proprio per questo, con molte meno righe.
L'oggetto non cambia mai e tiene una cosa sola. Una tupla, o un valore semplice, basta.
Una classe è lo strumento giusto dove dati e comportamento stanno davvero insieme: alcuni dati che cambiano, e poche operazioni che girano sempre su quei dati.
Un esempio completo
"""An order built from two small classes."""
TAX_RATE = 0.15
class OrderLine:
"""One item on an order: what it is, what it costs, how many."""
def __init__(self, name, price, quantity):
if quantity < 1:
raise ValueError(f"quantity must be at least 1: {quantity}")
self.name = name
self.price = price
self.quantity = quantity
def total(self):
"""Price for this line, tax included."""
return round(self.price * self.quantity * (1 + TAX_RATE), 2)
def __repr__(self):
return f"OrderLine({self.name!r}, {self.price!r}, {self.quantity!r})"
class Order:
"""A named collection of lines, and the questions you ask about it."""
def __init__(self, customer):
self.customer = customer
self.lines = []
def add(self, name, price, quantity):
self.lines.append(OrderLine(name, price, quantity))
return self
def total(self):
return round(sum(line.total() for line in self.lines), 2)
def largest(self):
"""The most expensive line, or None for an empty order."""
if not self.lines:
return None
return max(self.lines, key=lambda line: line.total())
def __repr__(self):
return f"Order({self.customer!r}, {len(self.lines)} lines)"
def main():
order = Order("rafi")
order.add("pen", 15.0, 3)
order.add("bag", 850.0, 1)
order.add("ink", 120.0, 2)
for line in order.lines:
print(f"{line.name:<6} {line.total():>9.2f}")
print(f"{'total':<6} {order.total():>9.2f}")
print()
print(order)
print("largest:", order.largest())
try:
order.add("clip", 5.0, 0)
except ValueError as err:
print("rejected:", err)
if __name__ == "__main__":
main()pen 51.75
bag 977.50
ink 276.00
total 1305.25
Order('rafi', 3 lines)
largest: OrderLine('bag', 850.0, 1)
rejected: quantity must be at least 1: 0Quattro cose da notare.
__init__ valida. Con quantity < 1 l'oggetto non viene creato per nulla. Questa è la cosa più utile che una classe offre: avere un OrderLine significa che la sua quantità è almeno uno — dovunque sia usato non deve ricontrollare. Un dizionario non può dare quella garanzia.
Order non sa come una riga è prezzata. Chiama line.total() e basta. Come si applica la tassa è responsabilità di OrderLine, e cambiarlo cambia un posto solo.
Entrambe le classi hanno un __repr__, e l'output di largest() mostra perché importa. Senza, quell'ultima riga sarebbe stata un indirizzo di memoria, che non ti dice nulla.
add restituisce se stesso (return self). Questo rende possibile order.add(...).add(...). Non è usato qui, deliberatamente — avere l'opzione e usarla su ogni riga sono cose diverse.
Quando si rompe
TypeError: ... takes 0 positional arguments but 1 was given Al metodo manca self. Aggiungi self come primo parametro.
AttributeError: 'X' object has no attribute 'y' Un errore di ortografia, o self.y non è mai stato impostato in __init__. print(obj.__dict__) mostra cosa c'è davvero.
TypeError: X() takes no arguments Non c'è __init__, o il nome è scritto male (__init__, non __int__, due underscore da ogni lato).
Ogni oggetto condivide una lista La lista è a livello di classe. Spostala dentro __init__.
Due oggetti contengono la stessa cosa e == dà False L'uguaglianza di default è l'identità. Scrivi __eq__ — o usa il dataclass del prossimo capitolo.
Dopo aver scritto __eq__ non va in un insieme Scrivere __eq__ rimuove l'hash di default. dataclass(frozen=True) dà entrambi.
La stampa dà <__main__.X object at 0x...> Non c'è __repr__.
Passo 4 di 6 — Prevedi
Verifica cosa hai capito
shout() è chiamata senza argomenti. Cosa succede?
class OrderLine:
def __init__(self, name):
self.name = name
def shout():
return "hello"
line = OrderLine("pen")
print(line.shout())- AUn `TypeError` — ne accetta uno, e zero sono stati passati
- BUn `TypeError` — ne accetta zero, e uno è stato dato
- CUn `AttributeError`
- D`hello` viene stampato
Due cestini separati, un oggetto ciascuno. Cosa viene stampato?
class Basket:
items = []
def add(self, name):
self.items.append(name)
a = Basket()
b = Basket()
a.add("pen")
b.add("bag")
print(a.items)
print(b.items)- A['pen', 'bag'] []
- B['pen'] ['pen', 'bag']
- C['pen'] ['bag']
- D['pen', 'bag'] ['pen', 'bag']
I due oggetti contengono lo stesso nome. Cosa viene stampato?
class OrderLine:
def __init__(self, name):
self.name = name
a = OrderLine("pen")
b = OrderLine("pen")
print(a == b)
print(a == a)- AFalse True
- BUn `TypeError`
- CFalse False
- DTrue True
Per rispondere serve un account
Accedi per verificare le tue risposte
Le domande sono qui sopra, e ragionarci a mente è la parte che conta. Accedi per vedere le risposte, le spiegazioni e i suggerimenti a tre livelli.
Tocca a te
Scrivi due classi.
Book — costruito da title, author e pages. Solleva un ValueError quando pages è minore di uno. Dagli un __repr__, e un __eq__ che considera due libri uguali quando titolo e autore coincidono.
Shelf — raccoglie libri, e offre add(book), total_pages(), longest(), e by_author(name) che restituisce una lista.
Poi cinque esperimenti:
- Sposta
books = []diShelffuori da__init__e sotto la rigaclass. Crea due scaffali separati, aggiungi un libro a uno, poi stampa l'altro. Cosa vedi? - Rimuovi il
__repr__e stampa una lista di libri. Quanto più difficile diventa il debug? - Rimuovi
__eq__e confronta due libri con stesso titolo e autore. - Rimuovi
selfda un metodo e chiamalo. Quanti argomenti dice il messaggio che sono stati dati? - Crea un
Booke scrivibook.pagse = 100(scritto male apposta). C'è un errore? Poiprint(book.__dict__)— cosa vedi?
L'ultimo punto è importante perché mostra il limite di una classe. Un nome sbagliato solleva un errore quando leggi, non quando scrivi — impostare un nuovo attributo è sempre permesso. Il prossimo capitolo mostra che gli hint di tipo e un type checker possono colmare quella lacuna anche loro.
Passo 6 di 6
Sfida — il quiz del capitolo
Dieci domande dalla più facile alla più difficile. Le ultime sono difficili di proposito.
Accedi per fare il quiz