Gestione di programmi più grandi
All’inizio di questo sito, abbiamo visto quattro modelli di programmazione di base
utilizzati per scrivere programmi:
• Codice sequenziale
• Codice condizionale (istruzioni if)
• Codice ripetitivo (cicli)
• Memorizza e riutilizza (funzioni).
Negli capitoli precedenti abbiamo esplorato l’uso delle variabili semplici e delle
strutture per la gestione dei dati come elenchi, tuple e dizionari.
Durante lo sviluppo programmi, progettiamo le strutture con cui conserviamo i
dati e scriviamo il codice necessario per manipolarle. Esistono molti modi per
scrivere programmi e, a questo punto, probabilmente avrai scritto alcuni script
“non particolarmente eleganti” e altri “più eleganti”. Anche se i tuoi script sono
di dimensioni abbastanza contenute, stai iniziando a capire come ci sia un po’ di
“arte” ed “estetica” nello sviluppare codice.
Mano a mano che i programmi raggiungono i milioni di righe diventa sempre più
importante scrivere codice che sia facile da interpretare. Se stai lavorando su un
programma di milioni di righe, non potrai mai tenere in mente allo stesso tempo
l’intero programma. Abbiamo bisogno di trovare modi per spezzare il programma
in più pezzi in modo che risolvere problemi, correggere un bug o aggiungere una
nuova caratteristica sia il più semplice possibile.
In un certo senso, la programmazione ad oggetti è un modo per organizzare il
codice in modo tale da poter concentrare la nostra attenzione su 500 righe di
codice ignorando momentaneamente le altre 999.500
Come Iniziare
Come capita per molti aspetti della programmazione è necessario imparare i con-
cetti di base della programmazione ad oggetti prima di poterla utilizzare in modo efficace. Quindi considera questo capitolo come un modo per studiare alcuni termi-
ni e concetti elementari illustrati attraverso alcuni semplici esempi con lo scopo di gettare le basi per l’apprendimento futuro. Anche se nel resto dell’articolo adotteremo
la programmazione ad oggetti in molti programmi non ne svilupperemo di nuovi.
Quello che vogliamo ottenere con questo capitolo è una conoscenza di base di come
siano fatti gli oggetti, di funzionano e, soprattutto, come sfruttarne le possibilità
offerte dalle librerie di Python.
Utilizzare gli oggetti
Realizzerai che in realtà abbiamo utilizzato gli oggetti durante tutto il corso:
Python ci mette a disposizione molti oggetti già integrati al suo interno.
Ecco un semplice script le cui prime poche righe dovrebbero sembrarti molto
semplici e familiari.
stuff = list()
stuff.append('python')
stuff.append('chuck')
stuff.sort()
print (stuff[0])
print (stuff.getitem__(0))
print (list.__getitem(stuff,0))
Piuttosto che concentrarci su ciò che potresti ottenere tramite queste poche righe di codice, vediamo cosa sta realmente accadendo dal punto di vista della programma-
zione ad oggetti. Non preoccuparti se la prima volta che leggi i prossimi paragrafi ti sembra che non abbiamo molto senso dato che non ti ho ancora spiegato il
significato di molti dei termini presenti.
La prima riga sta costruendo un oggetto di tipo lista, la seconda e la terza linea
chiamano il metodo append(), la quarta riga chiama il metodo sort() e la quinta
riga sta ottenendo l’elemento in posizione 0.
La sesta riga chiama il metodo getitem __()
nell’elenco stuff con parametro zero. print (stuff.__getitem(0))
La settima riga è un modo ancora più dettagliato di recuperare l’elemento che
occupa la posizione zero nell’elenco.
I PRIMI SCRIPT
print (list.getitem__(stuff,0)) In questo script, chiamiamo il metodo __getitem della classe list, lo passiamo nella lista (stuff) con l’elemento che vogliamo recuperare dalla lista come secondo parametro.
Le ultime tre righe del programma sono completamente equivalenti, ma è più
semplice utilizzare la sintassi con le parentesi quadre per cercare un elemento in
una posizione specifica di un elenco.
Possiamo dare un’occhiata alle potenzialità di un oggetto studiando l’output della
funzione dir():
stuff = list()
dir (stuff)
['add__', '_class_', '_contains_', '_delattr_', '_delitem_', '_dir_', '_doc_', '_eq_', '_format_', '_ge_', '_getattribute_', '_getitem_', '_gt_', '_hash_', '_iadd_', '_imul_', '_init_', '_iter_', '_le_', '_len_', '_lt_', '_mul_', '_ne_', '_new_', '_reduce_', '_reduce_ex_', '_repr_', '_reversed_', '_rmul_', '_setattr_', '_setitem_', '_sizeof_', '_str_', '__subclasshook','append', 'clear', 'copy', 'count','extend', 'index','insert', 'pop', 'remove', 'reverse', 'sort']
Più precisamente dir() elenca i metodi e gli attributi di un oggetto.
Il resto di questo capitolo ti fornirà una definizione più precisa di tutti i termi-
ni già riportati, quindi, dopo aver completato questo capitolo, rileggi i paragrafi precedenti per verificare quanto to abbia veramente appreso.
I primi script
Un programma nella sua forma più semplice richiede uno o più input, fa qualche
elaborazione e produce un output. lo script seguente per la conversione dei numeri
di piano in un ascensore é molto breve ma completo e presenta tutti e tre questi
passaggi.
usf = input('Enter the US Floor Number: ')
wf = int(usf) - 1
print('Non-US Floor Number is',wf)
Se ci concentriamo un po’ di più su questo programma: possiamo vedere che
convivono il “mondo esterno” e quello interno al programma stesso. Le operazioni
di input e l’output rappresentato il modo con cui programma interagisce con il
mondo esterno. All’interno del programma abbiamo il codice e dati necessari per
svolgere il compito per cui é stato progettato.
Un programma
All’interno del programma hanno luogo alcune interazioni ben definite con il mondo
“esterno” che generalmente non sono qualcosa su cui ci focalizziamo. Quando
scriviamo del codice ci preoccupiamo solo dei dettagli “all’interno del programma”.
Un programma come rete di oggetti
Un modo per pensare alla programmazione orientata agli oggetti è il voler cercare
di separare il nostro programma in più “zone”. Ogni “zona” é composta da codice
e dati (come se fosse un programma a se stante) e ha interazioni ben definite con
il mondo esterno e con le altre zone all’interno del programma. Se riprendiamo
in considerazione lo script di estrazione dei collegamenti in cui abbiamo usato la
libreria BeautifulSoup possiamo vedere un programma costituito da più oggetti
che interagiscono tra loro per svolgere un compito:
In questo momento la cosa più importante non è comprendere appieno il funzio-
namento di questo programma, quanto piuttosto vedere com’é stato strutturato questo insieme di oggetti e di come ne viene orchestrato lo scambio di informazioni.
È anche importante notare che quando studiavi il funzionamento di uno script
presente nei primi capitoli di questo sito, eri in grado di capire appieno cosa stava
succedendo senza nemmeno renderti conto che il programma stava “gestendo il
movimento dei dati tra gli oggetti presenti”. Allora per te erano solo righe di
codice che portavano a termine il lavoro.
Suddividere un problema – l’incapsulamento
Uno dei vantaggi dell’approccio della programmazione ad oggetti è che la possi-
bilità di ridurre la complessità di uno script: anche se hai la necessità di sapere
come sfruttare urllib e BeautifulSoup, non hai bisogno di sapere come funzionino
internamente tali librerie. Ciò ti consente di concentrarti sulla parte del problema
che devi risolvere lasciando perdere altre parti del programma.
Questa capacità di concentrarci sulla parte di un programma che ci interessa igno-
rando il resto del programma è utile anche per gli sviluppatori degli oggetti stessi: i
Ignorare i dettagli quando si costruisce un oggetto
Il nostro primo oggetto Python
Semplificando al massimo, un oggetto è una porzione codice con proprie strutture di
dati di dimensioni minori dell’intero programma. Il definire una funzione significa
indicare tramite un nome una porzione di codice ben definita che possiamo invocare
successivamente secondo le nostre necessità utilizzando solo il nome che le abbiamo
assegnato.
Un oggetto può contenere un certo numero di funzioni (che chiamiamo “metodi”) e
i dati utilizzati da tali funzioni. Definiamo dati gli elementi che compongono degli
“attributi” dell’oggetto.
La parola chiave class viene utilizzata per definire i dati e il codice che compongo
ciascuno degli oggetti. class include inoltre il nome della classe e inizia con un
blocco di codice indentato in cui includiamo gli attributi (dati) e i metodi (codice).
class PartyAnimal:
x = 0
def party(self) :
self.x = self.x + 1
print("So far",self.x)
an = PartyAnimal()
an.party()
an.party()
an.party()
Ogni metodo che ha l’aspetto di una funzione, inizia con la parola chiave def ed è
costituito da un blocco di codice indentato. L’esempio precedente ha un attributo
(x) e un metodo (party). I metodi hanno uno primo parametro speciale chiamato
per convenzione self.
Proprio come la parola chiave def non provoca l’esecuzione del codice della funzio-
ne, la parola chiave class non crea un oggetto. Piuttosto la parola chiave class definisce un modello che indica quali dati e codice saranno contenuti in ogni oggetto
di tipo PartyAnimal. Potresti pensare che la classe sia uno stampino per biscotti e
che gli oggetti creati utilizzando la classe siano i biscotti sai che la glassa non va messa sullo stampino quanto piuttosto sui biscotti e hai sempre la possibilità di mettere una glassa diversa su ciascun biscotto.
Una classe e due oggetti
Ma ora continuiamo ad analizzare il codice di esempio. Osserva la prima riga di
codice eseguibile:
an = PartyAnimal()
questo è il punto in cui indichiamo a Python dove costruire (ad es. creare) un
oggetto o “l’istanza della classe denominata PartyAnimal”. Sembra una chiamata
di funzione alla classe stessa e Python costruisce l’oggetto con i dati e i metodi
corretti e restituisce l’oggetto che viene quindi assegnato alla variabile an. In
un certo senso tutto questo è abbastanza simile alla riga che abbiamo usato fin
dall’inizio:
counts = dict()
Qui stiamo dicendo a Python di creare un oggetto usando il template dict (già
presente nel linguaggio), restituire l’istanza del dizionario e assegnarla alla variabile
counts.
Quando vogliamo utilizzare la classe PartyAnimal per costruire un oggetto, la
variabile an ci permette di puntare a quell’oggetto, accedere al codice e ai dati che
quella particolare istanza di un oggetto PartyAnimal contiene.
Ogni oggetto/istanza Partyanimal contiene al suo interno una variabile x e un
metodo/funzione party che viene richiamato in questa riga:
an.party()
Quando viene chiamato il metodo party, il primo parametro (chiamato per con-
venzione self) punta alla particolare istanza dell’oggetto PartyAnimal che viene chiamata all’interno di party. All’interno del metodo party, possiamo vedere la riga:
self.x = self.x + 1
Questa sintassi utilizzando l’operatore ‘punto’ indica ‘la x dentro self’. Quindi ogni
volta che viene chiamato party() viene incrementato di 1 il valore interno x che
viene poi visualizzato.
Per aiutarti a comprendere la differenza tra una funzione globale e un metodo
all’interno di una classe/oggetto, nella riga seguente puoi vedere un altro modo
per chiamare il metodo party all’interno dell’oggetto an:
PartyAnimal.party(an)
In questa variante stiamo accedendo al codice dalla classe e passando esplicitamen-
te il puntatore dell’oggetto an come primo parametro (ad esempio self all’interno del metodo).
Puoi pensare a an.party() come ad un’abbreviazione della riga precedente.
Quando il programma viene eseguito verrà generato il seguente output:
So far 1 So far 2 So far 3 So far 4
L’oggetto è stato costituito e il metodo party è chiamato quattro volte, incremen-
tando e visualizzando il valore di x all’interno l’oggetto an.
Le classi come tipi
Come abbiamo visto, in Python tutte le variabili hanno un type che possiamo
esaminare tramite la funzione integrata dir. queste funzionalità sono estese alle
classi che creiamo.
class PartyAnimal:
x = 0
def party(self) :
self.x = self.x + 1
print("So far",self.x)
an = PartyAnimal()
print ("Type", type(an))
print ("Dir ", dir(an))
print ("Type", type(an.x))
print ("Type", type(an.party))
che può produrre l’output seguente:
Type
Dir [‘class__’, ‘_delattr_‘, … ‘_sizeof_‘, ‘_str_‘, ‘_subclasshook_‘, ‘__weakref‘, ‘party’, ‘x’]
Type
Type
Puoi notare che abbiamo creato un nuovo type utilizzando la parola chiave class
e nell’output di dir che sia l’attributo intero x sia il metodo party sono disponibili
nell’oggetto.
Ciclo di vita dell’oggetto
Negli esempi precedenti abbiamo definito una classe (template) e l’abbiamo uti-
lizzata per creare un’istanza (oggetto) che poi abbiamo utilizzato fino al termine
del programma, momento in cui tutte le variabili sono state eliminate. Di solito
non pensiamo molto alla creazione e distruzione delle variabili ma capita spesso
che, quando i nostri oggetti diventano più complessi, dobbiamo agire all’interno
dell’oggetto per sistemare le cose mentre l’oggetto viene costruito e possibilmente
pulire le cose prima che esso sia eliminato.
Se vogliamo che il nostro oggetto si renda conto di questi momenti di costruzio-
ne e distruzione, dobbiamo aggiungere dei metodi che hanno una denominazione speciale:
class PartyAnimal:
x = 0
def init__(self): print('I am constructed') def party(self) : self.x = self.x + 1 print('So far',self.x) def __del(self):
print('I am destructed', self.x)
an = PartyAnimal()
an.party()
an.party()
an = 42
print('an contains',an)
Questo programma darà luogo al seguente output:
I am contructed
So far 1
So far 2
I am destructed 2
an contains 42
Appena Python inizia a costruire il nostro oggetto, chiama il nostro metodo
init__ per darci la possibilità di impostare alcuni valori iniziali o predefiniti da passare all’oggetto. Quando Python incontra la riga: an = 42 In effetti “cestina il nostro oggetto” in modo da poter riutilizzare la variabile an per memorizzare il valore 42. Proprio nel momento in cui il nostro oggetto an viene “distrutto” viene chiamato il codice distruttore (__del). Non possiamo
impedire che la nostra variabile venga distrutta ma possiamo fare ogni operazione
di pulizia necessaria prima che il nostro oggetto non esista più.
Durante lo sviluppo di oggetti è abbastanza comune aggiungere un costruttore a
un oggetto per impostarne i valori iniziali ed è relativamente altrettanto raro che
sia necessario impostarne un distruttore.
14.9 Molte istanze
Finora, abbiamo definito una classe, creato un singolo oggetto che abbiamo usato
ed infine eliminato. Ma la vera potenza della programmazione orientata agli oggetti
si rivela quando utilizziamo molte istanze della nostra classe.
Quando creiamo più oggetti dalla nostra classe potremmo aver bisogno di impo-
stare valori iniziali diversi per ciascuno di essi. Ciò é possibile passando i dati nei costruttori per assegnare a ciascun oggetto un diverso valore iniziale:
class PartyAnimal:
x = 0
name = ''
def init(self, nam):
self.name = nam
print(self.name,'constructed')def party(self) :
self.x = self.x + 1
print(self.name,'party count',self.x)
s = PartyAnimal('Sally')
j = PartyAnimal('Jim')
s.party()
j.party()
s.party()
Il costruttore ha sia un parametro self che punta all’istanza dell’oggetto sia altri
parametri che vengono passati al costruttore mentre l’oggetto viene costruito:
s = PartyAnimal(‘Sally’)
la riga presente all’interno del costruttore:
self.name = nam
Copia il parametro passato in (nam) nell’attributo name all’interno dell’istanza
dell’oggetto.
L’output del programma mostra che ognuno degli oggetti (s e j) contiene le proprie
copie indipendenti di x e nam:
Sally constructed
Sally party count 1
Jim constructed
Jim party count 1
Sally party count 2
Ereditarietà
Un’altra caratteristica potente della programmazione orientata agli oggetti è la
possibilità di creare una nuova classe estendendone una già esistente. Quando
estendiamo una classe, chiamiamo la classe originale ‘classe genitore’ e la nuova
‘classe figlia’.
In questo esempio sposteremo la nostra classe PartyAnimal nel suo file:
class PartyAnimal:
x = 0
name = ''
def init(self, nam):
self.name = nam
print(self.name,'constructed')
def party(self) :
self.x = self.x + 1
print(self.name,'party count',self.x)
Quindi abbiamo la possibilità di ‘importare’ la classe PartyAnimal in un nuovo
file ed estenderla come segue:
from party import PartyAnimal
class CricketFan(PartyAnimal):
points = 0
def six(self):
self.points = self.points + 6
self.party()
print(self.name,"points",self.points)
s = PartyAnimal("Sally")
s.party()
j = CricketFan("Jim")
j.party()
j.six()
print(dir(j))
Nel definire l’oggetto CricketFan abbiamo indicato che stiamo estendendo la classe
PartyAnimal: tutte le variabili (x) e i metodi (party) della classe PartyAnimal
sono ereditate dalla classe CricketFan.
Puoi vedere che all’interno del metodo six nella classe CricketFan possiamo chia-
mare il metodo party dalla classe PartyAnimal. Le variabili e i metodi della classe genitore sono uniti nella classe figlio.
Durante l’esecuzione del programma possiamo vedere che s e j sono istanze in-
dipendenti di PartyAnimal e CricketFan. L’oggetto j ha capacità aggiuntive
rispetto all’oggetto s.
Sally constructed
Sally party count 1
Jim constructed
Jim party count 1
Jim party count 2
Jim points 6
[‘class__’, ‘_delattr_‘, … ‘__weakref‘,
‘name’, ‘party’, ‘points’, ‘six’, ‘x’]
Nell’output dir per l’oggetto j (istanza della classe CricketFan) puoi vedere che
entrambi hanno sia gli attributi e i metodi della classe genitore sia gli attributi e i
metodi che sono stati aggiunti quando la classe è stata estesa per creare la classe
CricketFan.
Sommario
Ti ho dato un’introduzione molto basilare sulla programmazione ad oggetti dove
mi sono concentrato principalmente sulla terminologia e sulla sintassi utilizzate
nella definizione ed utilizzo degli oggetti.
Diamo ora una rapida occhiata al codice che abbiamo utilizzato dall’inizio di questo
capitolo. A questo punto dovresti riuscire a capire appieno cosa stia succedendo.
stuff = list()
stuff.append('python')
stuff.append('chuck')
stuff.sort()
print (stuff[0])
print (stuff.getitem__(0)) print (list.__getitem(stuff,0))
La prima riga costruisce un oggetto list. Durante la sua costruzione viene chia-
mato il metodo constructor (chiamato init) per impostare gli attributi dei
dati interni che verranno utilizzati per memorizzare i dati dell’elenco. Grazie
all’incapsulamento non abbiamo bisogno di sapere o di preoccuparci di come siano
organizzati questi attributi dei dati interni.
Non stiamo passando alcun parametro al costruttore e quando il costruttore ritorna,
usiamo la variabile stuff per puntare all’istanza restituita della classe list.
La seconda e la terza riga chiamano il metodo append con un parametro per ag-
giungere un nuovo elemento alla fine dell’elenco tramite l’aggiornamento degli attri-
buti all’interno di stuff. Nella quarta riga viene chiamato il metodo sort, senza
parametri, per ordinare i dati all’interno dell’oggetto stuff.
Quindi visualizziamo il primo elemento nell’elenco usando le parentesi quadre che
sono una scorciatoia per chiamare il metodo getitem all’interno dell’oggetto stuff. Questo equivale a chiamare il metodo getitem nella classe list pas-
sando l’oggetto stuff come primo parametro e la posizione che stiamo cercando come secondo parametro.
Al termine del programma, prima che venga scartato l’oggetto stuff, viene chia-
mato il distruttore (denominato del) in modo che l’oggetto possa eliminare qualsiasi questione rimasta in sospeso.
Queste sono le basi e la terminologia della programmazione ad oggetti. Ci sono
molti dettagli aggiuntivi su come utilizzare al meglio questo approccio durante lo
sviluppo di applicazioni e librerie di grandi dimensioni ma ciò va oltre lo scopo di
questo capitolo.
Glossario:
Attributo Una variabile che fa parte di una classe.
Classe Un modello che può essere utilizzato per costruire un oggetto. Definisce
gli attributi e i metodi che compongono l’oggetto.
Classe figlia Una nuova classe creata quando viene estesa una classe genitore. La
classe figlia eredita tutti gli attributi e i metodi della classe genitore.
Costruttore Un metodo opzionale con un nome speciale (init__) chiamato nel momento in cui una classe viene usata per costruire un oggetto. Di solito viene utilizzato per impostare i valori iniziali dell’oggetto.
Distruttore Usato raramente, un metodo opzionale con un nome speciale (__del) che viene chiamato appena prima che un oggetto venga distrutto.
Ereditarietà L’atto di estendere una classe esistente (genitore) tramite la crea-
zione di una nuova classe (figlia). La classe figlia ha tutti gli attributi e i metodi della classe genitore più ulteriori attributi e metodi definiti.
Metodo Una funzione che è contenuta all’interno di una classe e degli oggetti
costruiti dalla classe. Alcuni modelli orientati agli oggetti usano la parola
‘messaggio’ invece di ‘metodo’ per descrivere questo concetto.
Oggetto Un’istanza costruita di una classe. Un oggetto contiene tutti gli attributi
e i metodi definiti dalla classe. Alcuni documenti orientati agli oggetti usano
il termine ‘istanza’ in modo intercambiabile con ‘oggetto’.
Classe genitore La classe che viene estesa per creare una nuova classe figlia. La
classe genitore contribuisce con tutti i suoi metodi e attributi alla nuova
classe figlia.