Chapter 17 Classi e metodiAnche se abbiamo usato alcune delle caratteristiche object-oriented di Python, i programmi degli ultimi due capitoli non sono del tutto orientati agli oggetti, perché non mettono in evidenza le relazioni che esistono tra i tipi personalizzati e le funzioni che operano su di essi. Il passo successivo è di trasformare queste funzioni in metodi, in modo da rendere esplicite queste relazioni. Il codice degli esempi di questo capitolo è scaricabile dal sito http://thinkpython2.com/code/Time2.py, e le soluzioni degli esercizi da http://thinkpython2.com/code/Point2_soln.py. 17.1 Funzionalità orientate agli oggettiPython è un linguaggio di programmazione orientato agli oggetti, in altre parole contiene delle funzionalità a supporto della programmazione orientata agli oggetti, che ha le seguenti caratteristiche distintive:
Per esempio, la classe Tempo definita nel Capitolo 16 corrisponde al modo in cui le persone pensano alle ore del giorno, e le funzioni che abbiamo definite corrispondono al tipo di operazioni che le persone fanno con il tempo. Allo stesso modo, le classi Punto e Rettangolo nel Capitolo 15 corrispondono ai rispettivi concetti matematici. Finora, non abbiamo tratto vantaggio dalle capacità di supporto della programmazione orientata agli oggetti fornite da Python. A dire il vero, queste funzionalità non sono indispensabili; piuttosto, forniscono una sintassi alternativa per fare le cose che abbiamo già fatto. Ma in molti casi questa alternativa è più concisa e si adatta in modo più accurato alla struttura del programma. Ad esempio, nel programma Time1.py non c’è una chiara connessione tra la definizione della classe e le definizioni di funzione che seguono. A un esame più attento, è però evidente che tutte queste funzioni ricevono almeno un oggetto Tempo come argomento. Questa osservazione giustifica l’esistenza dei metodi; un metodo è una funzione associata ad una particolare classe. Abbiamo già visto qualche metodo per le stringhe, le liste, i dizionari e le tuple. In questo capitolo, definiremo dei metodi per i tipi personalizzati. Da un punto di vista logico, i metodi sono la stessa cosa delle funzioni, ma con due differenze sintattiche:
Nei prossimi paragrafi prenderemo le funzioni scritte nei due capitoli precedenti e le trasformeremo in metodi. Questa trasformazione è puramente meccanica e si fa seguendo una serie di passi: se siete in grado di convertire da funzione a metodo e viceversa, riuscirete anche a scegliere la forma migliore, qualsiasi cosa dobbiate fare. 17.2 Stampa di oggettiNel Capitolo 16, abbiamo definito una classe chiamata
Tempo, e nel Paragrafo 16.1, avete scritto una funzione di nome class Tempo: """Rappresenta un'ora del giorno.""" def stampa_tempo(tempo): print('%.2d:%.2d:%.2d' % (tempo.ora, tempo.minuto, tempo.secondo)) Per chiamare questa funzione occorre passare un oggetto Tempo come argomento: >>> inizio = Tempo() >>> inizio.ora = 9 >>> inizio.minuto = 45 >>> inizio.secondo = 00 >>> stampa_tempo(inizio) 09:45:00 Per trasformare class Tempo: def stampa_tempo(tempo): print('%.2d:%.2d:%.2d' % (tempo.ora, tempo.minuto, tempo.secondo)) Ora ci sono due modi di chiamare >>> Tempo.stampa_tempo(inizio) 09:45:00 In questo uso della notazione a punto, Tempo è il nome della classe e
Il secondo modo, più conciso, è usare la sintassi dei metodi: >>> inizio.stampa_tempo() 09:45:00 Sempre usando la dot notation, All’interno del metodo, il soggetto viene assegnato al primo dei parametri: in questo caso, inizio viene assegnato a tempo. Per convenzione, il primo parametro di un metodo viene chiamato self, di conseguenza è bene riscrivere class Tempo: def stampa_tempo(self): print('%.2d:%.2d:%.2d' % (self.ora, self.minuto, self.secondo)) La ragione di questa convenzione è una metafora implicita:
Questo cambio di prospettiva sarà anche più elegante, ma cogliere la sua utilità non è immediato. Nei semplici esempi che abbiamo visto finora, può non esserlo. Ma in altri casi, spostare la responsabilità dalle funzioni agli oggetti rende possibile scrivere funzioni (o metodi) più versatili e rende più facile mantenere e riusare il codice. Come esercizio, riscrivete 17.3 Un altro esempioEcco una versione di incremento (vedere Paragrafo 16.3), riscritto come metodo: # all'interno della classe Tempo: def incremento(self, secondi): secondi += self.tempo_in_int() return int_in_tempo(secondi) Questa versione presuppone che Ecco come invocare incremento: >>> inizio.stampa_tempo() 09:45:00 >>> fine = inizio.incremento(1337) >>> fine.stampa_tempo() 10:07:17 Il soggetto, inizio, viene assegnato quale primo parametro, a self. L’argomento, 1337, viene assegnato quale secondo parametro, a secondi. Questo meccanismo può confondere le idee, specie se commettete qualche errore. Per esempio, se invocate incremento con due argomenti ottenete: >>> fine = inizio.incremento(1337, 460) TypeError: incremento() takes 2 positional arguments but 3 were given Il messaggio di errore a prima vista non è chiaro, perché ci sono solo due argomenti tra parentesi. Ma bisogna tener conto che anche il soggetto è considerato un argomento, ecco perché in totale fanno tre. Tra parentesi, un argomento posizionale è un argomento privo di nome di un parametro; cioè, non è un argomento con nome. In questa chiamata di funzione: sketch(pappagallo, gabbia, morto=True) pappagallo e gabbia sono argomenti posizionali, e morto è un argomento con nome. 17.4 Un esempio più complesso
# all'interno della classe Tempo: def viene_dopo(self, other): return self.tempo_in_int() > other.tempo_in_int() Per usare questo metodo, lo dovete invocare su un oggetto e passare l’altro come argomento: >>> fine.viene_dopo(inizio) True Una particolarità di questa sintassi è che si legge quasi come in italiano: “fine viene dopo inizio?” 17.5 Il metodo speciale initIl metodo init (abbreviazione di initialization, ovvero inizializzazione) è un metodo speciale che viene invocato quando un oggetto viene istanziato. Il suo nome completo è # all'interno della classe Tempo: def __init__(self, ora=0, minuto=0, secondo=0): self.ora = ora self.minuto = minuto self.secondo = secondo È prassi che i parametri di self.ora = ora memorizza il valore del parametro ora come attributo di self. I parametri sono opzionali, quindi se chiamate Tempo senza argomenti, ottenete i valori di default. >>> tempo = Tempo() >>> tempo.stampa_tempo() 00:00:00 Se fornite un argomento, esso va a sovrascrivere ora: >>> tempo = Tempo (9) >>> tempo.stampa_tempo() 09:00:00 Se ne fornite due, sovrascrivono ora e minuto. >>> tempo = Tempo(9, 45) >>> tempo.stampa_tempo() 09:45:00 E se ne fornite tre, sovrascrivono tutti e tre i valori di default. Per esercizio, scrivete un metodo init per la classe Punto che prenda x e y come parametri opzionali e li assegni agli attributi corrispondenti. 17.6 Il metodo speciale __str__
Ecco ad esempio un metodo str per un oggetto Tempo: # all'interno della classe Tempo: def __str__(self): return '%.2d:%.2d:%.2d' % (self.ora, self.minuto, self.secondo) Quando stampate un oggetto con l’istruzione di stampa, Python invoca il metodo str: >>> tempo = Tempo(9, 45) >>> print(tempo) 09:45:00 Personalmente, quando scrivo una nuova classe, quasi sempre inizio con lo scrivere Come esercizio, scrivete un metodo str per la classe Punto. Create un oggetto Punto e stampatelo. 17.7 Operator overloadingNei tipi personalizzati, avete la possibilità di adattare il comportamento degli operatori attraverso la definizione di altri appositi metodi speciali. Per esempio se definite il metodo speciale di nome Ecco come potrebbe essere scritta la definizione: # all'interno della classe Tempo: def __add__(self, other): secondi = self.tempo_in_int() + other.tempo_in_int() return int_in_tempo(secondi) Ed ecco come può essere usata: >>> inizio = Tempo(9, 45) >>> durata = Tempo(1, 35) >>> print(inizio + durata) 11:20:00 Quando applicate l’operatore + agli oggetti Tempo, Python invoca
Cambiare il comportamento degli operatori in modo che funzionino con i tipi personalizzati è chiamato operator overloading (letteralmente, sovraccarico degli operatori). In Python, per ogni operatore esiste un corrispondente metodo speciale, come
Esercitatevi scrivendo un metodo add per la classe Punto. 17.8 Smistamento in base al tipoNel Paragrafo precedente abbiamo sommato due oggetti Tempo, ma potrebbe anche capitare di voler aggiungere un numero intero a un oggetto Tempo. Quella che segue è una versione di # all'interno della classe Tempo: def __add__(self, other): if isinstance(other, Tempo): return self.somma_tempo(other) else: return self.incremento(other) def somma_tempo(self, other): secondi = self.tempo_in_int() + other.tempo_in_int() return int_in_tempo(secondi) def incremento(self, secondi): secondi += self.tempo_in_int() return int_in_tempo(secondi) La funzione predefinita isinstance prende un valore e un oggetto classe, e restiutisce True se il valore è un’istanza della classe. Quindi, se other è un oggetto Tempo, Ecco degli esempi che usano l’operatore + con tipi diversi: >>> inizio = Tempo(9, 45) >>> durata = Tempo(1, 35) >>> print(inizio + durata) 11:20:00 >>> print(inizio + 1337) 10:07:17 Sfortunatamente, questa implementazione di addizione non è commutativa. Se l’intero è il primo operando vi risulterà infatti: >>> print(1337 + inizio) TypeError: unsupported operand type(s) for +: 'int' and 'instance' Il problema è che, invece di chiedere all’oggetto Tempo di aggiungere un intero, Python chiede all’intero di aggiungere un oggetto Tempo, ma l’intero non ha la minima idea di come farlo. Ma a questo c’è una soluzione intelligente: il metodo speciale # all'interno della classe Tempo: def __radd__(self, other): return self.__add__(other) Ed eccolo in azione: >>> print(1337 + inizio) 10:07:17 Come esercizio, scrivete un metodo add per i Punti che possa funzionare sia con un oggetto Punto che con una tupla:
17.9 PolimorfismoLo smistamento in base al tipo è utile all’occorrenza, ma (fortunatamente) non è sempre necessario. Spesso potete evitarlo scrivendo le funzioni in modo che operino correttamente con argomenti di tipo diverso. Molte delle funzioni che abbiamo scritto per le stringhe, funzioneranno anche con qualsiasi altro tipo di sequenza. Per esempio, nel Paragrafo 11.2 abbiamo usato istogramma per contare quante volte ciascuna lettera appare in una parola. def istogramma(s): d = dict() for c in s: if c not in d: d[c] = 1 else: d[c] = d[c]+1 return d Questa funzione è applicabile anche a liste, tuple e perfino dizionari, a condizione che gli elementi di s siano idonei all’hashing, in modo da poter essere usati come chiavi in d. >>> t = ['spam', 'uovo', 'spam', 'spam', 'bacon', 'spam'] >>> istogramma(t) {'bacon': 1, 'uovo': 1, 'spam': 4} Le funzioni che sono in grado di operare con tipi diversi sono dette polimorfiche. Il polimorfismo facilita il riuso del codice. Ad esempio, la funzione predefinita sum, che addiziona gli elementi di una sequenza, funziona alla sola condizione che gli elementi della sequenza siano addizionabili. Dato che agli oggetti Tempo abbiamo fornito un metodo add, funzionano con sum: >>> t1 = Tempo(7, 43) >>> t2 = Tempo(7, 41) >>> t3 = Tempo(7, 37) >>> totale = sum([t1, t2, t3]) >>> print(totale) 23:01:00 In linea generale, se tutte le operazioni all’interno di una funzione si possono applicare ad un dato tipo, la funzione può operare con quel tipo. Il miglior genere di polimorfismo è quello involontario, quando scoprite che una funzione che avete già scritto può essere applicata anche ad un tipo che non avevate previsto. 17.10 DebugÈ consentito aggiungere attributi in qualsiasi momento dell’esecuzione di un programma, ma se avete oggetti dello stesso tipo che non hanno gli stessi attributi, è facile generare errori. Inizializzare tutti gli attributi di un oggetto nel metodo init è considerata una prassi migliore. Se non siete certi che un oggetto abbia un particolare attributo, potete usare la funzione predefinita hasattr (vedere Paragrafo 15.7). Un altro modo di accedere agli attributi è la funzione predefinita vars, che prende un oggetto e restituisce un dizionario che fa corrispondere nomi degli attributi (come stringhe) e i relativi valori: >>> p = Punto(3, 4) >>> vars(p) {'y': 4, 'x': 3} Per gli scopi del debug, può essere utile tenere questa funzione a portata di mano: def stampa_attributi(oggetto): for attr in vars(oggetto): print(attr, getattr(oggetto, attr))
La funzione predefinita getattr prende un oggetto e un nome di attributo (come stringa) e restituisce il valore dell’attributo. 17.11 Interfaccia e implementazioneUno degli scopi della progettazione orientata agli oggetti è di rendere più agevole la manutenzione del software, che significa poter mantenere il programma funzionante quando altre parti del sistema vengono cambiate e poter modificare il programma per adeguarlo a dei nuovi requisiti. Un principio di progettazione che aiuta a raggiungere questo obiettivo è di tenere le interfacce separate dalle implementazioni. Per gli oggetti, significa che i metodi esposti da una classe non devono dipendere da come vengono rappresentati gli attributi. Per esempio, in questo capitolo abbiamo sviluppato una classe che rappresenta un’ora del giorno. I metodi esposti da questa classe comprendono
Quei metodi possono essere implementati in diversi modi. I dettagli dell’implementazione dipendono da come rappresentiamo il tempo. In questo capitolo, gli attributi di un oggetto Tempo sono ora, minuto, e secondo. Come alternativa, avremmo potuto sostituire quegli attributi con un singolo numero intero, come secondi trascorsi dalla mezzanotte. Con questa implementazione, alcuni metodi come Dopo aver sviluppato una nuova classe, potreste scoprire una implementazione migliore. Se altre parti del programma usano quella classe, cambiare l’interfaccia può essere dispendioso in termini di tempo e fonte di errori. Ma se avete progettato l’interfaccia accuratamente, potete cambiare l’implementazione senza cambiare l’interfaccia, che significa che non occorre cambiare altre parti del programma. 17.12 Glossario
17.13 Esercizi
Esercizio 1 Scaricate il codice degli esempi di questo capitolo
(http://thinkpython2.com/code/Time2.py). Cambiate gli attributi di Tempo con un singolo intero che rappresenta i secondi dalla mezzanotte. Quindi modificate i metodi (e la funzione
Esercizio 2
Questo esercizio è un aneddoto monitorio su uno degli errori più comuni e difficili da trovare in Python. Scrivete una definizione di una classe di nome Canguro con i metodi seguenti:
Provate il codice creando due oggetti Canguro, assegnandoli a variabili di nome can e guro, e aggiungendo poi guro al contenuto della tasca di can. Scaricate http://thinkpython2.com/code/BadKangaroo.py. Contiene una soluzione al problema precedente, ma con un grande e serio errore. Trovatelo e sistematelo. Se vi bloccate, potete scaricare http://thinkpython2.com/code/GoodKangaroo.py, che spiega il problema e illustra una soluzione. |
ContributeIf you would like to make a contribution to support my books, you can use the button below. Thank you!
Are you using one of our books in a class?We'd like to know about it. Please consider filling out this short survey.
|