Orientamento agli oggetti

Nel capitolo introduttivo abbiamo detto che Python è un linguaggio multiparadigma in grado di lavorare con la programmazione strutturata, come stavamo facendo fino ad ora, o con programmazione orientata agli oggetti o con la programmazione funzionale.
La programmazione orientata agli oggetti (OOP o POO secondo l’acronimo in inglese) è un paradigma di programmazione in cui i concetti del mondo reale rilevanti per il nostro problema sono modellati da classi e oggetti, e dove il nostro programma è una serie di interazioni tra questi oggetti.
Classi e Oggetti
Per capire questo paradigma, dobbiamo prima sapere cos’è una classe e cos’è un oggetto. Un oggetto è un’entità che raggruppa uno stato e una funzionalità relativa. Lo stato dell’oggetto è definito da variabili chiamate attributi, mentre la funzionalità è modellata da funzioni che sono noti con il nome di metodi dell’oggetto.
Un esempio di oggetto può essere una macchina, che avrebbe degli attributi quali il marchio, il numero di porte o del tipo di carburante e metodi come avviare e arrestare. O qualsiasi altra combinazione di attributi e metodi rilevante per il nostro programma.
Una classe, d’altra parte, non è altro che un modello generico da cui istanziare gli oggetti; modello dove si definiscono quali attributi e metodi avranno gli oggetti della classe.
Tornando al nostro esempio: nel mondo reale vi è un insieme di oggetti che chiamiamo macchine e che dispongono di un insieme di attributi comuni e dei comportamenti comuni, questo è ciò che noi chiamiamo classe. Tuttavia, la mia macchina non è come la macchina del mio vicino, e anche se appartengono alla stessa classe di oggetti sono diversi oggetti.
In Python, le classi sono definite utilizzando la parola chiave class seguita dal nome della classe, due punti (:) e poi rientrato, il corpo della classe. Come nel caso delle funzioni, se la prima riga del corpo è una stringa di testo, questa sarà la documentazione della classe o docstring.
			class Macchina:
			    """Astrazione dell'oggetto macchina."""
			    def __init__(self, benzina):
			        self.benzina = benzina
			        print "Abbiamo", benzina, "litri"
			    def avviare(self):
			        if self.benzina > 0:
			            print "Avvia"
			        else:
			            print "Non si avvia"
			    def guidare(self):
			        if self.benzina > 0:
			            self.benzina -= 1
			            print "Rimangono", self.benzina, "litri"
			        else:
			            print "Non si muove"
La prima cosa che spicca nell’esempio qui sopra è il nome curioso che ha il metodo __init__. Questo nome è una convenzione e non un capriccio. Il metodo __init__, con doppio carattere di sottolineatura all’inizio e alla fine del nome, viene eseguito subito dopo la creazione di un nuovo oggetto della classe, un processo noto come istanziazione. Il metodo __init__ viene utilizzato, come suggerisce il nome, per eseguire qualunque processo di inizializzazione necessaria.
Come si vede, il primo parametro di __init__ e del resto dei metodi della classe è sempre self. Questo è un’idea ispirata in Modula-3 e viene utilizzato per fare riferimento all’oggetto corrente. Questo meccanismo è necessario per accedere agli attributi degli oggetti e metodi differenziando, ad esempio, una variabile locale mia_var da un attributo dell’oggetto self.mia_var.
Tornando al metodo __init__ della nostra classe Macchina, vediamo come viene utilizzato self per assegnare all’attributo benzina dell’oggetto (self.benzina) il valore che il programmatore ha specificato per il parametro benzina. Il parametro benzina viene distrutta alla fine della funzione, mentre l’attributo benzina si conserva (e si può accedere) mentre l’oggetto è vivo.
Per creare un oggetto si scrive il nome della classe seguito da qualsiasi parametro, che sia necessario, tra parentesi. Questi parametri devono essere passati al metodo __init__, che come detto è il metodo che viene chiamato per istanziare la classe.
			mia_macchina = Macchina(3)
Vi chiederete allora come è possibile che al momento di creare il nostro primo oggetto passiamo un singolo parametro a __init__, il numero 3, quando nella definizione della funzione si indica chiaramente che richiede due parametri (self e benzina). Questo è così perché Python passa il primo argomento (il riferimento all’oggetto che viene creato) automagicamente.
Ora che abbiamo creato il nostro oggetto, siamo in grado di accedere ai suoi attributi e metodi utilizzano la sintassi objeto.atributo e objeto.metodo():
			>>> print mia_macchina.benzina
			3
			>>> mia_macchina.avviare()
			Avvia
			>>> mia_macchina.guidare()
			Rimangono 2 litri
			>>> mia_macchina.guidare()
			Rimangono 1 litri
			>>> mia_macchina.guidare()
			Rimangono 0 litri
			>>> mia_macchina.guidare()
			Non si muove
			>>> mia_macchina.avviare()
			Non si avvia
			>>> print mia_macchina.benzina
			0
Come nota finale ricordare che in Python, come discusso molte volte in precedenza, tutto è oggetto. Le stringhe, ad esempio, hanno metodi come upper(), che restituisce il testo in maiuscolo o count(sub), che restituisce il numero di volte che la stringa sub viene trovata nel testo.
Eredità
Ci sono tre concetti che sono alla base di qualsiasi linguaggio di programmazione orientato agli oggetti: l’incapsulamento, l’ereditarietà e il polimorfismo.
In un linguaggio orientato agli oggetti quando creiamo una classe (sottoclasse) che eredita da un’altra classe (superclasse) stiamo facendo che la sottoclasse contenga tutti gli attributi e metodi che ha la superclasse. Comunque l’atto di ereditare da una classe viene spesso chiamato anche “estendere una classe”.
Supponiamo di voler modellare gli strumenti musicali in una band, allora avremo una classe Chitarra, una classe Batteria, una classe Basso, ecc. Ciascuna di queste classi avranno una serie di attributi e metodi, ma accade che, per il semplice fatto che sono strumenti musicali, queste classi condividono molti dei loro attributi e metodi, un esempio potrebbe essere il metodo suonare().
È più facile creare un tipo di oggetto Strumenti con gli attributi e metodi comuni e indicare al programma che Chitarra, Basso e Batteria sono tipi di Strumenti perché ereditano da Strumenti.
Per indicare che una classe eredita da un altra si scrive il nome della classe che viene ereditata tra parentesi dopo il nome della classe:
			class Strumenti:
			    def __init__(self, prezzo):
			        self.prezzo = prezzo
			    def suonare(self):
			        print "Stiamo suonando della musica"
			    def rompere(self):
			        print "Questo lo paghi te"
			        print "Sono", self.prezzo, "€uro"
			
			class Batteria(Strumenti):
			    pass
			class Chitarra(Strumenti):
			    pass
Come Batteria e Chitarra ereditano da Strumenti, entrambi hanno il metodo suonare() e il metodo rompere(), che vengono inizializzati passando il parametro prezzo. Ma cosa accadrebbe se vogliamo specifiacre un nuovo parametro tipo_corda durante la creazione dell’oggetto Chitarra? Basterebbe scrivere un nuovo metodo __init__ nella classe Chitarra che si eseguirebbe al posto di __init__ di Strumenti.
Questo processo è noto come sovrascrittura di metodi.
Ora, può verificarsi in alcuni casi che abbiamo bisogno di ridefinire o sovrascrivere un metodo della classe padre, ma in questo metodo vogliamo eseguire il metodo della classe padre, perché nel nostro nuovo metodo è necessario eseguire un paio di nuove istruzioni supplementari. In questo caso abbiamo usato la sintassi SuperClasse.metodo(self, args) per chiamare il metodo con lo stesso nome della classe padre. Ad esempio, per chiamare il metodo __init__ di Strumenti dalla classe Chitarra useremo Strumenti .__init__(self, prezzo).
Si noti che in questo caso è necessario specificare il parametro di self.
Eredità multipla
In Python, a differenza di altri linguaggi come Java o C#, l’ereditarietà multipla è consentita, ossia una classe può ereditare da più classi contemporaneamente. Per esempio, potremmo avere una classe Coccodrillo che eredita dalla classe Terrestre, con metodi come camminare() e attributi come velocita_caminare e dalla classe Acquatico, con metodi come il nuotare() e attributi come velocita_nuotare. Basta elencare le classi che eredita separate da virgole:
			class Coccodrillo(Terrestre, Acquatico):
			    pass
Nel caso in cui una delle classi padre hanno metodi con lo stesso nome e numero di parametri, le classi sovrascriverebbero le implementazioni dei metodi delle classi più a destra della definizione.
Nell’esempio che segue, siccome Terrestre è la più a sinistra allora sarebbe la definizione del metodo spostare di questa classe la prevalente, e quindi se chiamiamo al metodo spostare di un oggetto di tipo Coccodrillo si stamperebbe “L’animale camina”.
			class Terrestre:
			    def spostare(self):
			        print "L'animale camina"
			class Acquatico:
			    def spostare(self):
			        print "L'animale nuota"
			class Coccodrillo(Terrestre, Acquatico):
			    pass
			c = Coccodrillo()
			c.spostare()
Polimorfismo
La parola polimorfismo, dal greco poly morphos (varie forme), si riferisce alla capacità di oggetti di diverse classi di rispondere allo stesso messaggio. Ciò può essere ottenuto per via ereditaria: un oggetto di una classe derivata è anche un oggetto della classe padre, in modo che, quando sia necessario un oggetto della classe padre può anche essere utilizzato uno della classe figlia.
Essendo di tipizzazione dinamica, Python, non impone restrizioni ai tipi che possono essere passati ad una funzione, ad esempio, oltre al comportamento atteso dell’oggetto: se si vuole chiamare un metodo f() dell’oggetto passato come parametro, evidentemente l’oggetto dovrà contare con questo metodo. Per questo motivo, e diversamente da altri linguaggi staticamente tipizzati come Java o C++, il polimorfismo in Python non è di grande importanza.
A volte viene utilizzato il termine polimorfismo anche per riferirsi al sovraccarico di metodi, un termine che viene definito come la capacità del linguaggio di determinare quale metodo eseguire tra i diversi metodi con lo stesso nome secondo il tipo o numero di parametri passati. In Python non esiste il sovraccarico di metodi (l’ultimo metodo sovrascriverebbe le implementazioni dei precedenti), anche se è possibile ottenere un comportamento simile utilizzando le funzioni con valori di default per i parametri o la sintassi *params o **params, spiegato nel capitolo sulle funzioni in Python, o anche utilizzando decoratori (meccanismo che vedremo di seguito).
Incapsulazione
L’incapsulamento si riferisce al impedimento di accesso a determinati metodi e attributi degli oggetti stabilendo così quello che può essere utilizzato al di fuori della classe.
Questo viene fatto in altri linguaggi di programmazione come Java utilizzano i modificatori di accesso per definire se chiunque può accedere a tale funzioni o variabil (public) o limitato alla propria classe (private).
In Python non ci sono modificatori di accesso; ciò che di solito si fa’ è che l’accesso a una variabile o funzione è determinato dal suo nome: se il nome inizia con due underscore (e non termina anche con due underscore) si tratta di una variabile o di una funzione privata, altrimenti è pubblico. I metodi i cui nomi iniziano e finiscono con due underscore sono metodi speciali che Python chiama automaticamente in determinate circostanze, come vedremo alla fine del capitolo.
Nel seguente esempio solo si stamperà la stringa corrispondente al metodo pubblico(), durante il tentativo di chiamata al metodo __privato(), Python solleverà un’eccezione lamentando che non c’è (ovviamente c’è, ma non lo possiamo vedere perché è privato) il metodo.
			class Esempio:
				def pubblico(self):
print "Publico"
				def __privato(self):
print "Privado"
			    
			ej = Esempio()
			ej.pubblico()
			ej.__privato()
Questo meccanismo si basa su nomi che iniziano con una doppia sottolineatura che vengono rinominati per includere il nome della classe (caratteristica che è conosciuta con il nome di name mangling). Ciò implica che il metodo o l’attributo non è realmente privato, e possiamo accedere attraverso un piccolo trucco:
			ej._Esempio__privato()
A volte può capitare che si desidera consentire l’accesso ad un attributo del nostro oggetto, ma che avvenga in modo controllato. Per questo possiamo scrivere metodi il cui unico compito è questo; metodi che normalmente e per convenzione, hanno nomi come GetVariable e setVariable e quindi noti anche con il nome di getter e setter.
			class Data():
			    def __init__(self):
			        self.__giorno = 1
			    def getGiorno(self):
			        return self.__giorno
			    def setGiorno(self, giorno):
			        if giorno > 0 and giorno < 31:
			            self.__giorno = giorno
			        else:
			            print "Errore"
			
			mia_data = Data()
			mia_data.setGiorno(33)
Questo potrebbe essere semplificato utilizzando le proprietà, che astraggono all’utente il fatto che si sta utilizzando metodi “dietro le quinte” per ottenere e impostare i valori dell’attributo:
			class Data(object):
			    def __init__(self):
			        self.__giorno = 1
			    def getGiorno(self):
			        return self.__giorno
			    def setGiorno(self, giorno):
			        if giorno > 0 and giorno < 31:
			            self.__giorno = giorno
			        else:
			            print "Errore"
			
			    giorno = property(getGiorno, setGiorno)
			
			mia_data = Data()
			mia_data.giorno = 33
Classi New-Style
Nell’esempio di sopra sarete stati colpiti dal fatto che la classe Data derivi da object. La ragione di questo è che poter utilizzare le proprietà la classe deve essere di “nuovo stile“, classi arricchite introdotte in Python 2.2 che saranno poi lo standard in Python 3.0, ma che continuano a vivere con le classi “classiche” per ragioni di retrocompatibilità. Oltre alle proprietà le classi “nuovo stile” aggiunge altre funzionalità, quali i descrittori o i metodi statici.
Per far sì che una classe sia di “nuovo stile” è necessario, per ora, estendere una classe di nuovo stile. E se nel caso non sia necessario ereditare il comportamento o lo stato di alcuna classe, come nel nostro esempio precedente, si può ereditare da object, che è un oggetto vuoto e che serve come base per tutte le classi di “nuovo stile“.
La differenza principale tra le classi “vecchia scuola” e “nuovo stile” è che quando prima si creava una nuova classe non si definiva in realtà un nuovo tipo, ma tutti gli oggetti creati dalle classi, fossero di tipo instance.
Metodi speciali

Abbiamo visto all’inizio di questo articolo l’uso del metodo __init__. Esistono altri metodi con significati particolari, i cui nome iniziano e terminano sempre con due underscore. Di seguito sono elencati alcuni particolarmente utili.
__init__

			__init__(self, args)
Metodo chiamato dopo aver creato l’oggetto per eseguire compiti di inizializzazione.
__new__
			__new__(cls, args)
Metodo esclusivo delle classi di “nuovo stile” che viene eseguito prima di __init__ ed è responsabile per la costruzione e restituzione dell’oggetto stesso. È equivalente a costruttori di C++ o Java. Si tratta di un metodo statico, che significa che esiste indipendentemente dalle istanze della classe: è un metodo di classe, e non di oggetto, e quindi il primo parametro non è self, ma la classe stessa: cls.
__del__
			__del__(self)
Metodo chiamato quando l’oggetto è in procinto di essere cancellato. Chiamato anche distruttore (destroyer), viene utilizzato per compiti di pulizia.
__str__
			__str__(self)
Metodo chiamato per creare una stringa di testo che rappresenta il nostro oggetto. Viene utilizzato quando usiamo print per visualizzare il nostro oggetto o quando si usa la funzione str(obj) per creare una stringa partendo dal nostro oggetto.
__cmp__
			__cmp__(self, altro)
Metodo chiamato quando si utilizzano gli operatori di confronto per verificare se il nostro oggetto è minore, maggiore o uguale all’oggetto passato come parametro. Dovrebbe restituire un numero negativo se il nostro oggetto è minore, zero s’è uguale e un numero positivo se l’oggetto è maggiore. Se questo metodo non è definito e si tenta di confrontare l’oggetto con <, <=, > o >= viene generata un’eccezione. Quando si utilizza il == o != per verificare se due oggetti sono uguali, controlla se sono lo stesso oggetto (se hanno lo stesso id).
__len__
			__len__(self)
Metodo noto per controllare la lunghezza dell’oggetto. Viene usato, per esempio, quando viene chiamata la funzione len(obj) sul nostro oggetto. Come previsto, il metodo deve restituire la lunghezza dell’oggetto.
Ci sono molti altri metodi speciali, che permettono tra l’altro, utilizzare il meccanismo di slicing sul nostro oggetto, utilizzare gli operatori aritmetici o utilizzare la sintassi del dizionario, ma uno studio esaustivo di tutti i metodi è oltre lo scopo del capitolo.


Similari
Overloading di metodi in Java
12% Java
Un metodo overload viene utilizzato per riutilizzare il nome di un metodo ma con argomenti diversi, opzionalmente con un differente tipo di ritorno. [expand title=”Regole per overload” startwrap=”” endwrap=”” excerpt=”⤽” s…
Modi di fare e di non fare in Python
11% Python
Questo documento può essere considerato un compagno del tutorial di Python. Viene illustrato come utilizzare Python, e quasi ancora più importante, come non usare Python. [expand title=”Costrutti del linguaggio che non dov…
redirect 301 usando mod_alias
9% Server
mod_alias è fondamentalmente la versione più semplice di mod_rewrite. Non può fare le cose che fa mod_rewrite, ad esempio modificare la stringa di query. Per eseguire reindirizzamenti nel server web Apache è possibile di u…
Installare Python e Django su Windows
8% Django
Quando ci riferiamo allo sviluppo web con Python, la prima cosa che viene in mente è usare un qualche framework. Il più famoso e utilizzato da tutti è il Django, ma non è l’unico. Ci sono Pylons, Grok, TurboGears e Zope: t…
Metodi magici e costanti predefinite in PHP
8% Php
PHP fornisce un insieme di costanti predefinite e metodi magici per i nostri programmi. A differenza delle normali costanti i quali si impostano con define(), il valore delle costanti predefinite o speciali dipendono da do…