Cos'è la OOP?
Sono confidente sul fatto che chi sta leggendo questo articolo già sappia, anche solo a grandi linee, cosa si intende per OOP. Se volessimo trovare una definizione informale, possiamo dire che la OOP è un tipo di programmazione in cui si dà importanza alle entità in gioco all'interno di un sistema e a come tali entità interagiscono tra loro. Elenchiamo velocemente nel seguito le caratteristiche principali del paradigma:
- Concetti di Classe e Oggetto: la Classe è il "blueprint" di un'entità; l'Oggetto è una delle istanze dell'entità.
- Attributi e Metodi: ogni Classe contiene Attributi che definiscono lo stato delle istanze, e Metodi che definiscono funzionalità proprie dell'istanza; si distinguono inoltre Metodi e Attributi di Classe dai più comuni Metodi e Attributi di Istanza.
- Incapsulamento: lo stato degli Oggetti, gli Attributi ed i Metodi sono mantenuti e definiti all'interno dell'Oggetto stesso.
- Ereditarietà: è possibile definire Classi che "estendono" le caratteristiche di altre Classi; classico esempio della Classe
Cane
che estende la ClasseAnimale
. - Polimorfismo: concetto che si lega fortemente alla non ripetizione del codice; più Oggetti di Classi che derivano da una Classe madre possono accedere agli stessi metodi definiti per la Classe madre, senza che questi siano ripetuti nelle definizioni delle Classi figlie; l'esempio delle Classi
Cane
eGatto
che ereditano daAnimale
e condividono il metodocammina()
. - Overriding: le Classi figlie possono ridefinire metodi della Classe madre per scopi più legati alle loro necessità; ancora l'esempio delle Classi
Cane
eGatto
che ereditano daAnimale
e ridefiniscono il metodoemetti_verso()
conprint 'Bau!'
eprint 'Miao!'
Per delle definizioni più formali e precise, vi invito a cercare su internet testi più tecnici di questo articolo e a non saltare le lezioni di Programmazione Orientata agli Oggetti all'Università.
Classi in Python
Prendiamo adesso come esempio una versione semplificata della classe Giornata
del nostro progettino sul Fantacalcio:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
from properties import Costanti class Giornata(object): """Classe per gestire le giornate""" def __init__(self, n_giornata=1): self.squadre = [] self.squadre_pti = {} self.n_giornata = n_giornata @property def squadre(self): """Lista con l'elenco delle squadre partecipanti""" return self.__squadre @squadre.setter def squadre(self, squadre): self.__squadre = squadre @property def squadre_pti(self): """Dizionario formato da key: nome squadra value: fantapunti giornata""" return self.__squadre_pti @squadre_pti.setter def squadre_pti(self, squadre_pti): self.__squadre_pti = squadre_pti @property def n_giornata(self): """Numero d'ordine della giornata""" return self.__n_giornata @n_giornata.setter def n_giornata(self, n_giornata): self.__n_giornata = n_giornata def get_nome_giornata(self): """Restituisce una stringa che identifica il nome della giornata" return "Giornata ".join(str(self.n_giornata)) def __str__(self): """Restituisce una stringa composta dal nome della giornata e dai punti fatti dalle squadre""" return self.nome_giornata + '\n' + '\n'.join(['{} {}'.format(squadra,str(pti)) for (squadra, pti) in zip(self.squadre_pti.keys(), self.squadre_pti.values())]) |
Intestazione
Per definire una Classe in Python si utilizza la parola chiave class
. Nel caso di Giornata
abbiamo:
class Giornata(object):
Ciò che viene inserito tra parentesi dopo l'identificatore della Classe è l'elenco delle classi dalle quali deriva la Classe che stiamo definendo. Come già detto nel nostro articolo introduttivo, in Python tutto deriva dalla classe object
. In Python 3.x non è più necessario esplicitare l'ereditarietà con object
, mentre in Python 2.x è consigliabile esplicitarla per ogni definizione di classe, come fatto nell'esempio di sopra.Se l'ereditarietà non è esplicitata, si può incorrere a problemi nel richiamare funzioni delle classi madre.Ecco per esempio cosa succede se richiamiamo il metodo type()
per classi definite con l'ereditarietà esplicita a object
e per classi senza la dichiarazione esplicita:
Nel primo caso avremo come risposta una istanza, nel secondo avremo come risposta una classe.
Oltre a ciò, se non esplicitata l'eredtarietà con object
si possono avere problemi con il metodo super()
, in caso di ereditarietà multiple, e con la definizione delle @property
, le quali saranno trattate nei paragrafi a seguire.
Il "costruttore"
Il metodo __init__()
è un metodo all'interno del quale avviene l'inizializzazione degli attributi. Provenendo dalla OOP in Java e C++, spesso viene chiamato "costruttore". In realtà, quando il metodo __init__()
è richiamato, esso prende in ingresso come primo parametro un'istanza già creata (self
) e ne inizializza gli attributi. Non si occupa preciò della instanziazione del nuovo Oggetto.
La variabile self
deve essere quindi passata come primo parametro ad ogni metodo di istanza. Tale variabile serve per accedere agli attributi e ai metodi propri della istanza che si sta utilizzando. Faccio notare che self
NON è una parola chiave: se al posto di self
utilizzaste sempre pippo
come primo parametro dei metodi di istanza nel vostro codice, il programma funzionerebbe lo stesso. Tuttavia per convenzione è buona regola utilizzare self
, così da non compromettere la leggibilità del vostro codice.
Le @property
In Python non esiste il concetto di "scope delle variabili": tutti gli attributi di un Oggetto sono pubblici e possono essere acceduti da chiunque tramite un'istanza o una classe. Se avete già avuto a che fare con la OOP in Java o C++ sarete tutti abituati a lasciare i vostri attributi privati e scrivere metodi getter e setter pubblici per gestirli. In Python esistono due possibilità:
- Sapere che chiunque può accedere direttamente agli attributi di una istanza e riassegnarli a piacimento, e quindi convivere con questo fatto.
- Utilizzare le
property
come fossero getter e setter
1 2 3 4 5 6 7 8 |
@property def squadre(self): """Lista con l'elenco delle squadre partecipanti""" return self.__squadre @squadre.setter def squadre(self, squadre): self.__squadre = squadre |
Definendo delle property
, ogni volta che richiamiamo un attributo, quello che stiamo realmente facendo è richiamare dei metodi per assegnare o restituire i valori dei veri attributi "privati" della nostra Classe (per convenzione, variabili e metodi che iniziano con __
sono considerati "privati" in Python).
Risulta consigliabile utilizzare delle property
quando si vuole cambiare il comportamento di un attributo, senza modificarne l'interfaccia per accedervi.
Metodi built-in di object
Abbiamo detto che la Classe Giornata
deriva dalla Classe object
, ereditandone i metodi e gli attributi. Esistono dei metodi speciali, detti built-in, che possono essere richiamati dalle istanze. Si distinguono dai metodi standard dai doppi underscore come prefisso e suffisso del nome. Il metodo __init__()
, del quale abbiamo parlato in precedenza, rientra nella categoria. Nell'esempio, abbiamo fatto l'override del metodo __str__()
, metodo che viene richiamato ogni volta che facciamo un print
di un oggetto e che deve perciò restituire una stringa. Per un elenco completo degli altri metodi, vi invito a leggere la documentazione di Python.
Metodi "di istanza" e "di classe"
Vediamo infine le differenze tra i metodi detti "di istanza" ed i metodi "di classe".
I primi sono metodi che vengono richiamati direttamente dalle singole istanze di una Classe. Sono perciò caratterizzati da un comportamento che dipende direttamente dallo stato interno dell'Oggetto. Nell'esempio, il metodo get_nome_giornata()
è un metodo di istanza, il cui output dipende dal valore dell'attributo di istanza n_giornata
.
Se volessimo invece aggiungere funzionalità alla nostra Classe che non dipendono direttamente dalle istanze, e che quindi possono essere richiamate senza dover forzatamente creare un Oggetto, dobbiamo definire dei metodi di classe.
Riprendendo l'esempio, potremmo scrivere:
1 2 3 4 5 6 7 8 9 10 |
from properties import Costanti class Giornata(object): # definizione ed implementazione # degli attributi e dei metodi di istanza @staticmethod def get_nome_campionato(): return "Campionato ".join(Costanti.NOME_CAMPIONATO) |
La funzione get_nome_campionato()
è definita con il tag @staticmethod
il quale dice all'interprete Python che tale funzione può essere chiamata da una istanza della Classe, ma anche dalla Classe stessa:
print Giornata.get_nome_campionato()
Come sempre, vi invito a lasciare nei commenti i vostri dubbi, domande, le vostre precisazioni o aggiunte.
Vi invito inoltre a segnalarci eventuali bug o problemi riscontrati nell'utilizzo del codice.
