home page::doc::obj
Mimante.net

Avviso!

Questo articolo è basato su una versione obsoleta di python; può ancora essere utile come tutorial basilare, ma non copre le molte features introdotte nel corso degli ultimi anni.

A volte ritornano

A parte citare film horror, in questa seconda parte (che originalmente segue la prima, nella quale abbiamo introdotto questo interessante linguaggio interpretato) vedremo come poterlo utilizzare seguendo i paradigmi della programmazione ad oggetti.

La cosa. Anzi, le cose

Gli oggetti nei linguaggi di programmazione sono né più né meno che astrazioni degli oggetti del mondo reale. Nel caso abbiate difficoltà a concepire oggetti fisici, vi consiglio un corso intensivo di immersione nella realtà e tempo una dozzina d'anni dovreste essere in grado di proseguire la lettura di questo articolo. Esattamente come gli oggetti fisici (avete presente la caffettiera che vi rifornisce del carburante fondamentale ad ogni programmatore?) anche quelli che andremo a creare programmando avranno caratteristiche (come potrebbero essere il colore, la marca, il materiale, le dimensioni, etc.) e saranno in grado di compiere azioni (aprirsi, ricevere acqua, chiudersi, scaldare l'acqua, esplodere...)

A grande richiesta direi si possa passare alla presentazione di un piccolo esempio, rimandando ai commenti del codice l'ingrato mestiere di spiegare i dettagli implementativi della programmazione ad oggetti e della sintassi del python.

Miva! Lanciami i componenti!

Quello che segue è un modulo che ci sarà utile come base su cui creare altre applicazioni; per il semplice motivo che più avanti lo utilizzeremo con questo nome, siete pregati di far conto che il file in cui è salvato si chiami cibolib.py.
Prima che mi copriate di insulti: sono perfettamente conscio che come esempio non è un granché, ma non è facile farsi venire idee originali per un esempio di programmazione ad oggetti che debba occupare poche linee di codice; inoltre lo stile di programmazione è volutamente essenziale e non ho certo puntato a creare una applicazione solida o ben progettata: semplicemente ho cercato di inserirvi elementi che ritenevo interessanti da mostrare, lasciando alla buona volontà del lettore il compito di approfondire gli argomenti secondo le sue necessità; discorso analogo andrebbe fatto circa la terminologia usata, che in alcuni punti si discosta da quella formale nel tentativo di aggiungere chiarezza all'esposizione. Anche alcuni sottili dettagli vengono ignorati, omessi o semplificati: questo vuole essere un articolo introduttivo e non certo un'analisi approfondita del linguaggio.
"""Definisce le classi necessarie ad un pranzo sano, completo e nutriente.

Ecco lo schema di derivazione delle classi:

Cibo
|
+-- Primo
+-- Secondo
+-- Dolce

Author: Davide Alberani <alberanid@libero.it>
Updated: 32 febbraio dell'anno prossimo.
"""

from types import *
import sys

# scala di qualità
qual = ['ripugnante', 'fetente', 'media', 'buona', 'ottima']

class Cibo:
    """Classe generica; meglio non istanziarne oggetti.

   Definisce i seguenti metodi:
   get_nome()        restituisce il nome del piatto.
   set_nome(string)  setta il nome del piatto.
   get_calorie()     riporta il numero di calorie per unità di quantità.
   set_calorie(int)  indovinate.
   get_calorie_totali()    restituisce le calorie totali.
   get_quanto()      restituisce la quantità.
   set_quanto(int)   setta la quantità.
   get_come()        ci dice la bontà del piatto.
   set_come(int)     fa qualcosa di impensabile.
   """

    def __init__(self):
        self.nome = ''
        self.calorie_per_unita = 0
        self.quantita = 0
        self.qualita = qual.index('buona')
    def get_nome(self):
        return self.nome
    def set_nome(self, n):
        if type(n) is not StringType:
            print '"' + n + '" non è un nome valido'
            sys.exit(1)
        self.nome = n
    def get_calorie(self):
        return self.calorie_per_unita
    def set_calorie(self, c):
        if type(c) is not IntType or c < 0:
            print '"' + c + '" non è un valore valido per le calorie'
            sys.exit(1)
        self.calorie_per_unita = c
    def get_calorie_totali(self):
        return self.calorie_per_unita * self.quantita
    def get_quanto(self):
        return self.quantita
    def set_quanto(self, x):
        if type(x) is not IntType or x < 0:
            print 'Posso dubitare del fatto che "' + x + \
                    '" sia una quantità valida?'
            sys.exit(1)
        self.quantita = x
    def get_come(self):
        return self.qualita
    def set_come(self, q):
        if type(q) is not IntType:
            print 'Errore nella definizione della qualità'
            sys.exit(1)
        if q >= len(qual) or q < 0:
            print 'Valore di qualità fuori dal range'
            sys.exit(1)
        self.qualita = q

class Primo(Cibo):
    """Per creare un oggetto primo piatto."""
    # qualche caratteristica specifica.
    brodo = 0
    formaggio = 0

class Secondo(Cibo):
    """Particolarità dei secondi piatti."""
    pesce = 0
    def e_un_pesce(self):
        return self.pesce
    def set_pesce(self, v):
        if type(v) is not IntType:
            print 'Non sei né carne né pesce...'
            sys.exit(2)
        if v != 0 and v != 1:
            print 'Valore fuori range'
            sys.exit(2)
        self.pesce = v

class Dolce(Cibo):
    """Classe derivata per i dolci (slurp)."""
    porzioni = 0
    def __init__(self, n, p):
        """Inizializzazione."""
        if type(p) is not IntType or p < 0:
            print '"' + p + '" non è un numero valido'
            sys.exit(3)
        self.set_nome(n)
        self.set_quanto(50 * p)
        self.set_come(4)
        self.set_calorie(4)
        self.porzioni = p
    def get_calorie_totali(self):
        """Ridefinisce questa funzione."""
        return self.calorie_per_unita * self.quantita * self.porzioni

Niente panico

C'è un bel po' di carne al fuoco; vediamo di fare ordine con calma e metodo.
L'inizio del file non dovrebbe spaventare: una semplice docstring su più linee che descrive le principali caratteristiche di questo modulo, ovvero le classi, le funzioni e le costanti che definisce più qualche informazione sull'autore e sul file stesso.

Modulo aereo!

Di cosa sono e a cosa servono i moduli abbiamo già discusso, e come si vede crearne uno è estremamente semplice: basta scrivere il codice che ci interessa in un file la cui estensione sia .py, per poi inserirlo in una delle directory in cui l'interprete python lo cercherà.
Una cosa nuova che noterete dopo aver testato l'intero piccolo progetto che vi sto presentando come esempio è che per motivi di prestazione i moduli, se possibile, vengono pseudocompilati dall'interprete python la prima volta che sono richiesti, lasciando nella medesima directory un file con lo stesso nome del modulo ma con estensione .pyc; in questo modo la successiva volta che uno script python includerà il dato modulo questo non dovrà essere nuovamente letto e compilato, ma si userà direttamente il .pyc.
Ovviamente vengono fatti i dovuti controlli sulle date dei file in modo da assicurarsi che il file sorgente non sia stato nel frattempo modificato (nel qual caso ovviamente si procederà a nuova ricompilazione).

Proseguiamo con l'analisi del sorgente: le righe import dovrebbero esservi chiare; come sempre invito a riferirsi alla documentazione dei vari moduli per approfondirne l'uso.
Vediamo ora qualcosa di cui non avevamo ancora parlato:


linea 14 |  qual = ['ripugnante', 'fetente', 'media', 'buona', 'ottima']

Uno strano tipo

Python include una serie di tipi di dati predefiniti molto utili. Quella che abbiamo appena definito è una lista di nome qual i cui elementi sono le stringhe racchiuse tra le parentesi quadre separati da virgole.
Prima di addentrarci nella programmazione ad oggetti, vediamo quali sono questi tipi e come possono tornarci utili.

Sequenza di autodistruzione attivata...

Esistono tre tipi diversi di sequenze: le stringhe, le liste e le tuples (un termine italiano che vi si avvicini come significato potrebbe essere ennuple, ma preferisco attenermi alla denominazione originale).
Le stringhe e le tuples sono tipi di dati immutabili (ovvero non è possibile modificare direttamente i loro elementi, anche se esistono vari "trucchi" per ottenere risultati analoghi) mentre le liste sono un tipo di dato mutabile.

Stringhe

Non guardatevi le scarpe: le stringhe non sono altro che una serie di caratteri alfanumerici uno di seguito all'altro. Come già visto le stringhe in python vengono create semplicemente includendo i caratteri tra coppie di 'apici singoli', "doppi apici" o """tripli apici""" per testi su più righe.
Le stringhe possono essere concatenate tra loro con l'operatore + (ad esempio 'Ciao ' + 'mondo' sarà equivalente a 'Ciao mondo'; in realtà se i due elementi adiacenti sono stringhe il + può essere omesso, ma il programma non ne guadagna in leggibilità) e ripetute con * ('Ciao' * 3 risulterà uguale a 'CiaoCiaoCiao'). È possibile accedere alla stringa tramite degli indici (racchiusi tra parentesi quadre, che oltre a semplici numeri di indice possono contenere anche degli intervalli, separati da due punti); qualche esempio dovrebbe chiarire tutti i casi tipici:

stringa = 'Ciao mondo!'

# solo il primo carattere
print stringa[0]

==> C

# il secondo carattere
print stringa[1]

==> i

# l'ultimo carattere
print stringa[-1]

==> !

# il penultimo carattere
print stringa[-2]

==> o

# dal sesto al settimo carattere (estremi inclusi)
print stringa[5:7]

==> mo

# i primi quattro caratteri
print stringa[:4]

==> Ciao

# tutta la stringa tranne i primi tre caratteri
print stringa[3:]

==> o mondo!

# gli ultimi due caratteri
print stringa[-2:]

==> o!

# tutta la stringa tranne gli ultimi tre caratteri
print stringa[:-3]

==> Ciao mon

Riferirsi al solito tutorial per i casi particolari e la gestione di eventuali errori.

Liste

Le liste sono elenchi di dati, anche di tipo diverso, ed è possibile modificare i singoli elementi di una lista e anche variarne il numero. Anche qui possiamo accedere agli elementi tramite indici e intervalli, sempre ricordandosi che il primo elemento è il numero zero. Qualche esempio:

lista = ['fagioli', 5, 2, 'gatti']

print lista[0]

==> fagioli

# "tagliando" una lista si ottiene una nuova lista
print lista[1:]

==> [5, 2, 'gatti']

# modificare un elemento
lista[1] = 'peperoni'
print lista

==> ['fagioli', 'peperoni', 2, 'gatti']

# altre operazioni sulle liste

# elimina un elemento
del lista[2]
print lista

==> ['fagioli', 'peperoni', 'gatti']

# aggiunge un elemento
lista.append('cani')
print lista

==> ['fagioli', 'peperoni', 'gatti', 'cani']

Come c'era da aspettarsi anche per le liste funzionano gli operatori +, * e alcuni altri metodi specifici: count(obj) restituisce il numero di campi della lista che hanno valore obj, index(obj) restituisce il primo indice in cui viene trovato l'oggetto obj, remove(obj) rimuove il primo oggetto obj dalla lista, reverse() e sort() che rispettivamente rovesciano e ordinano la lista. È inoltre possibile effettuare sostituzioni e modifiche ai vari elementi di una lista utilizzando degli intervalli come riferimento.

Tuples

Sono elenchi simili alle liste, ma sono immutabili; i vari elementi sono racchiusi tra parentesi tonde e separati da virgole, anche se nelle dichiarazioni le parentesi possono essere omesse. Come al solito qualche esempio:

t1 = 'abc', 3, 56, 'ciao'
t2 = (2, 4, 5, 'gatto')

print t1

==> ('abc', 3, 56, 'ciao')

print t2
==> (2, 4, 5, 'gatto')

# tuple di un solo elemento (singoletto, singleton in originale)
t3 = ('solo',)

# tuple vuota
t4 = ()

È interessante vedere come gli elementi di una tuple possano essere "spacchettati" (in originale "tuple unpacking", in contrapposizione al processo di creazione di una tuple, detto "tuple packing") in modo molto semplice assegnando i vari elementi a variabili separate:

t = 'primo', 'secondo, 'terzo'

# NB: il numero di variabili a sinistra deve corrispondere
# con il numero di elementi della tuple
e1, e2, e3 = t

print e1

==> primo

Qualcosa di analogo può essere fatto anche sugli elementi di una lista, con la sintassi:

lista = ['primo', 2, 3]
[var1, var2, var3] = lista

print var1

==> primo

Da ricordare infine che su tutti i tipi di dati visti finora sono validi alcuni operatori e metodi standard come obj in seq (la sua negazione è not in) che restituisce vero se l'oggetto obj è contenuto nella sequenza di dati seq; len(seq) che ne restituisce il numero di elementi; min(seq) e max(seq) che restituiscono rispettivamente il valore minore e maggiore tra quelli contenuti nella sequenza.

Dizionari

Un diverso tipo di dato sono i dizionari, composti da coppie di keyword: valore, dove keyword deve essere unica all'interno del vocabolario.
Un piccolo esempio, per presentarne le peculiarità:

# definiamo un dizionario che ad ogni soggetto associa la sua età
anni = {'Maria': 30, 'Giovanna': 18, 'Lucia': 21, 'Sandra': 24}

# legge un valore
anni['Maria']

==> 30

# cambia un valore
anni[Giovanna] = 19

# cancella una voce
del anni['Lucia']

# verifica se una chiave è contenuta
vero_o_falso = anni.has_key('Margarita Cansino')

# crea una lista contenente solo le keywords
lista_nomi = anni.keys()

# lista con solo i valori
lista_valori = anni.values()

# restituisce una lista di tuples di due elementi (chiave, valore)
lista_coppie = anni.items()

Diamo i numeri

Un tipo di dati che abbiamo ampiamente utilizzato ma su cui non ci siamo soffermati, in fondo perché di solito li si considera scontati, sono i numeri. Python definisce gli interi, i long, i float e i complessi. Gli interi sono implementati internamente come i long in C: tutti i numeri che non ricadano nelle altre categorie vengono di default considerati interi. I long vengono specificati aggiungendo al numero il suffisso l o L (è consigliata la seconda forma, per non rischiare di confondere la elle minuscola con la cifra uno) e non hanno limiti di dimensioni. I float sono tutti i numeri che abbiano una parte decimale. I complessi sono sempre considerati come dei double in C; la parte immaginaria è indicata dal suffisso j o J, i numeri che hanno sia parte reale che immaginaria possono essere creati con la funzione complex(re, im) o più semplicemente sommando un intero o float ad una parte immaginaria; sarà poi possibile accedere alle due parti tramite le notazioni numero.real e numero.imag.
Python si occupa in maniera trasparente all'utente di effettuare le opportune conversioni di tipo quando necessarie; in ogni caso sono disponibili alcune funzioni built-in per forzarle, come ad esempio int(n), long(n), float(n).

linea 15 |  class Cibo:

Che classe!

Veniamo finalmente alla descrizione di cosa sono e come si costruiscono le classi e gli oggetti da esse derivate. Un oggetto classe definisce un tipo generico di oggetti; la sintassi è molto simile alle dichiarazioni di funzioni, dove al posto dell'istruzione def si utilizza class. Le classi possono essere concepite come "idee" o descrizioni generiche di oggetti; da queste verranno poi istanziati gli oggetti veri e propri. Al loro interno le classi possono contenere dichiarazioni di variabili e di funzioni, secondo necessità. Per amor di formalità va ricordato che in python le funzioni appartenenti ad una classe vengono chiamate metodi quando vengono richiamate tramite un oggetto istanziato (come abbiamo già visto) e l'insieme di funzioni e variabili è detto attributi.

linea 28 |  nome = ''
linea 29 |  calorie_per_unita = 0
linea 30 |  quantita = 0
linea 31 |  qualita = qual.index('buona')

Qui vengono dichiarate alcune variabili utili a descrivere le caratteristiche dell'oggetto Cibo, come si vede si può far riferimento senza problemi a variabili esterne precedentemente definite.

linea 32 |  def get_nome(self):
linea 33 |      return self.nome
linea 34 |  def set_nome(self, n):
linea 35 |      if type(n) is not StringType:
linea 36 |          print '"' + n + '" non è un nome valido'
linea 37 |          sys.exit(1)
linea 38 |      self.nome = n

Queste sono le dichiarazioni di un paio di funzioni: la prima restituisce il contenuto della variabile nome mentre la seconda la modifica dopo aver fatto i controlli del caso sul tipo passato come parametro.

Usare le classi e derivarne oggetti

La classe in sé è un oggetto: è possibile accedere alle sue variabili e utilizzarne le funzioni; ad esempio possiamo riferirci alla variabile Cibo.nome e leggerla e modificarla a nostro piacimento. Molto più interessante e pratico è invece istanziare oggetti partendo da una classe; la sintassi che si segue è analoga a quella usata per le normali funzioni. Esempio chiarificatore:

# creiamo l'oggetto antipasto, istanza della classe Cibo.
# NB: le parentesi sono MOLTO importanti, se omesse si
# creerà solamente un altro riferimento all'oggetto classe Cibo.
antipasto = Cibo() 

# chiamiamo il metodo set_nome() proprio dell'oggetto antipasto
antipasto.set_nome('nuvole di drago')

# leggiamo il valore precedentemente registrato
nome1 = antipasto.get_nome()
print nome1

==> nuvole di drago

Come si vede, nel codice che la definisce la funzione accetta un parametro (self), mentre nell'invocazione questo non viene specificato: molto semplicemente quando viene invocato un metodo di un oggetto istanziato, viene nella pratica chiamata la funzione propria della classe con l'oggetto stesso come primo argomento (e tutti gli argomenti passati dall'utente a seguire). Infatti le sintassi:

# sintassi 1 (viene invocato il metodo dell'oggetto)
antipasto1.set_nome('aria fritta')

# sintassi 2 (viene usata la funzione definita nella classe)
Cibo.set_nome(antipasto2, 'aria molto fritta')

# sintassi 1
nome1 = antipasto1.get_nome()

# sintassi 2
nome2 = Cibo.get_nome(antipasto2)

sono del tutto equivalenti; entrambe le metodologie di lavoro con gli oggetti sono perfettamente lecite.
Nell'esempio il primo parametro nelle definizioni di funzione viene sempre denominato self e viene usato nel corpo delle funzioni per riferirsi ad altri attributi dell'oggetto stesso; il nome è del tutto arbitrario, anche se per coerenza e leggibilità sarebbe opportuno seguire questa convenzione.

Il resto del codice della classe Cibo non dovrebbe presentare difficoltà anche per i meno esperti; unica nota da sottolineare sulla sintassi è l'istruzione:


linea 52 |  print 'Posso dubitare del fatto che "' + x + \
linea 53 |          '" sia una quantità valida?'

Che come si vede è divisa su più righe per migliorare la leggibilità del codice: la barra rovesciata indica all'interprete che l'istruzione prosegue alla riga seguente; può essere omessa quando ciò che si va a spezzare è contenuto in parentesi di vario genere (liste, tuples, dizionari, parametri di funzioni, etc.)

linea 66 |  class Primo(Cibo):
linea 67 |      """Per creare un oggetto primo piatto."""
linea 68 |      # qualche caratteristica specifica.
linea 69 |      brodo = 0
linea 70 |      formaggio = 0

Ciiiro! Figlio mioooo!

Le classi servirebbero a ben poco se non fosse possibile crearne di nuove senza dover riscrivere l'intero codice, ma solo modificando una classe generica aggiungendovi solo le peculiarità richieste dal nuovo tipo di oggetto generando così una gerarchia ramificata di classi.
In questo esempio definiamo la classe Primo come derivata dalla classe Cibo da cui eredita (questo meccanismo si chiama infatti ereditarietà) tutti gli attributi; in più vi aggiunge le proprie variabili e le proprie funzioni (vedere anche la classe derivata Secondo). La classe da cui se ne deriva un'altra (Cibo, in questo caso) è detta classe base.
Ovviamente ogni oggetto di queste classi derivate potrà indistintamente accedere a variabili e funzioni definite tanto nella classe da cui ha ereditato (come ad esempio il metodo set_nome()) quanto a quelle definite nel proprio corpo (come la variabile formaggio).
Altra funzionalità molto utile è la possibilità di sovrascrivere un metodo definito nella classe "madre" in modo da modificarne il comportamento; un esempio in merito lo potete vedere analizzando la funzione get_calorie_totali() della classe Dolce alla riga 97.

Ereditarietà multipla

È anche possibile che una nuova classe debba ereditare da più classi base; la sintassi del python anche in questo caso è molto semplice: è sufficiente indicare tra parentesi il nome di tutte le classi da cui si eredita separate da virgole. Ad esempio:

class Coltellino_Svizzero(Coltello, Accetta, Motosega, Cavatappi):
              .
              .
              .

Da notare che questa tecnica, seppure potente, può portare a conflitti tra variabili e metodi definiti in più classi che creerebbero errori difficilmente identificabili; è quindi bene usarla con cautela e solo dopo aver attentamente progettato sulla carta lo schema di derivazione. Per gli scopi di questo articolo basti ricordare che quando si cercherà di accedere ad un attributo di un oggetto appartenente ad una classe derivata da più classi, questo viene cercato, se non definito dalla classe a cui ci si riferisce, nelle classi da cui questa deriva in ordine da sinistra a destra rispetto a come specificate tra le parentesi della dichiarazione. Ad esempio invocando il metodo sradica() di un oggetto Coltellino_Svizzero questo viene cercato, se non definito nella classe Coltellino_Svizzero stessa, prima nella classe Coltello, poi in Accetta e così via.

linea 87 |  def __init__(self, n, p):
linea 88 |      """Inizializzazione."""
linea 89 |      if type(p) is not IntType or p < 0:
linea 90 |          print '"' + p + '" non è un numero valido'
linea 91 |          sys.exit(3)
linea 92 |      self.set_nome(n)
linea 93 |      self.set_quanto(50 * p)
linea 94 |      self.set_come(4)
linea 95 |      self.set_calorie(4)
linea 96 |      self.porzioni = p

Spesso è utile, istanziando un oggetto da una classe, eseguire alcune funzioni di default, inizializzare delle variabili e così via; questo può essere fatto tramite il metodo __init__(), che viene eseguito quando un oggetto viene istanziato. In questo esempio quando andremo ad istanziare un oggetto Dolce dovremo passare due parametri, rispettivamente una stringa che ne rappresenti il nome e il numero di porzioni. Può essere una buona idea utilizzare nella dichiarazione dei valori di default per i vari parametri. Per chiarire, potremmo cambiare il codice in questo modo:

def __init__(self, n='Torta della nonna', p=1):
              .
              .
              .

A questo punto sarebbero ugualmente accettabili le invocazioni:

a = Dolce()
b = Dolce('Tiramisù')
c = Dolce('Torta di mele', 2)

Variabili private

Chi ha già esperienza di programmazione orientata agli oggetti con altri linguaggi come C o Java avrà notato che la sintassi e l'implementazione di questi concetti in python è molto meno complessa e stringente: gran parte del lavoro necessario ad un corretto uso degli oggetti è lasciato all'utente stesso, che è libero di comportarsi come meglio crede. Una caratteristica che subito salta agli occhi è la possibilità di accedere sempre e comunque a tutti gli attributi di un oggetto, senza limitazioni di accesso e senza possibilità di nascondere i dati all'interno delle classi stesse. Questo non è del tutto vero: anche in python è possibile utilizzare quelle che in C vengono chiamate variabili (e funzioni) private; qualsiasi attributo il cui nome cominci con due underscore e termini con al più un underscore viene considerato privato, e quindi non accessibile direttamente dall'esterno. Un esempio:

class Gino:
    # variabile privata
    __pippo = 4
    # metodo per accedere in lettura alla variabile __pippo dall'esterno
    def get_pippo(self):
        return self.__pippo

x = Gino()

# leggiamo il valore di __pippo tramite la funzione preposta
print x.get_pippo()

==> 4

# un accesso diretto porterà ad un errore
print x.__pippo

==>
Traceback (innermost last):
  File "./prova", line 13, in ?
    print x.__pippo
AttributeError: __pippo

è bene tenere a mente che, per come questa funzionalità è implementata in python, con estrema semplicità è comunque possibile un accesso diretto ai dati, quindi non si può fare affidamento sulla loro intoccabilità che è ancora una volta lasciata alla buona volontà del programmatore.

Usi avanzati degli oggetti

Come in molti altri linguaggi anche in python è possibile ottenere comportamenti piuttosto complessi per quanto riguarda gli oggetti; ad esempio si può, sovrascrivendo opportuni metodi, gestire le regole che sovraintendono al confronto tra due oggetti distinti, consentire un accesso ai dati di un oggetto tramite notazioni ad indice simili a quelle delle sequenze viste sopra, sovrascrivere gli operatori matematici in modo da garantire un comportamento coerente con le caratteristiche degli oggetti stessi e molto altro ancora; tutto questo va ben oltre gli scopi introduttivi di questo articolo e rimando quindi gli interessati al Python Reference Manual, in particolare alla sezione 3.3 Special method names.

Usiamo il nostro modulo

#!/usr/bin/python

# importiamo il nostro modulo
from cibolib import *
import sys, string, pickle

# lista di un menu
lista_piatti = []

def domande_standard(oggetto):
    """Domande da porre sia per i primi che per i secondi piatti"""
    try:
        nome = raw_input('Nome: ')
        oggetto.set_nome(nome)
        cal = raw_input('Calorie per unità di peso: ')
        oggetto.set_calorie(int(cal))
        quant = raw_input('Quantità: ')
        oggetto.set_quanto(int(quant))
        for i in range(len(qual)):
            print '\t' + `i` + '- ' + `qual[i]`
        q = raw_input('Qualità: ')
        oggetto.set_come(int(q))
    except ValueError:
        print 'Uno dei valori immessi non è un numero'
        sys.exit(3)

primo = Primo()
print '\nDefinisci un primo:\n'
# pone le domande standard
domande_standard(primo)
# pone le domande specifiche per i primi piatti
brodo = raw_input('è in brodo? [s/n] ')
if brodo[0] == 's':
        primo.brodo = 1
form =  raw_input('Una spolveratina di formaggio? [s/n] ')
if form[0] == 's':
        primo.formaggio = 1
# aggiunge questo oggetto alla lista dei primi piatti
lista_piatti.append(primo)

secondo = Secondo()
print '\nDefinisci un secondo:\n'
domande_standard(secondo)
pesce = raw_input('È un pesce? [s/n] ')
if pesce[0] == 's':
    secondo.set_pesce(1)
else:
    secondo.set_pesce(0)
lista_piatti.append(secondo)

# i dolci
nome_dolce = raw_input('\nNome del dolce: ')
porzioni = raw_input('Numero di porzioni del dolce: ')
try:
    dolce = Dolce(nome_dolce, int(porzioni))
except ValueError:
    print 'Numero di porzioni errato'
    sys.exit(4)
lista_piatti.append(dolce)

# stampa le calorie delle singole portate e il totale
print '\n\tMENU DEL GIORNO\n\t---------------\n'

# calorie totali
cal_tot = 0

for i in lista_piatti:
    cal = i.get_calorie_totali()
    print string.rjust(`i.get_quanto()` + 'grammi di ' + i.get_nome() +
            '| ', 40) + `cal` + 'calorie'
    cal_tot = cal_tot + cal

print string.rjust('---|-', 40) + '--'

print string.rjust('Calorie totali: | ', 40) + `cal_tot`

if primo.brodo:
    print 'Il primo è in brodo.'
if primo.formaggio:
    print 'Sul primo c\'è del formaggio.'
if secondo.e_un_pesce():
    print 'Il secondo è un pesce.'

salva = raw_input('\nSalvo cotanta squisitezza? ')
if salva[0] == 's':
    try:
        f = open('ultimo_menu', 'w')
    except  IOError, errore:
        print 'Errore nell\'aprire il file di log', errore
        sys.exit(5)
    pickle.dump(lista_piatti, f)
    f.close()

What's goin on in the kitchen?

Ok, citeremo anche rapper, e allora?
Una tipica sessione di uso del programma, apparirà simile a questa (in grassetto le parti digitate dall'utente):

$ ./cibo.py

Definisci un primo:

Nome: Spaghetti al ragù
Calorie per unità di peso: 2
Quantità: 150
        0 - 'ripugnante'
        1 - 'fetente'
        2 - 'media'
        3 - 'buona'
        4 - 'ottima'
Qualità: 3
È in brodo? [s/n] n
Una spolveratina di formaggio? [s/n] s

Definisci un secondo:

Nome: Pollo arrosto
Calorie per unità di peso: 3
Quantità: 200
        0 - 'ripugnante'
        1 - 'fetente'
        2 - 'media'
        3 - 'buona'
        4 - 'ottima'
Qualità: 2
È un pesce? [s/n] n

Nome del dolce: Profiterole
Numero di porzioni del dolce: 2

        MENU DEL GIORNO
        ---------------

      150 grammi di Spaghetti al ragù | 300 calorie
          200 grammi di Pollo arrosto | 600 calorie
            100 grammi di Profiterole | 400 calorie
                                   ---|---
                      Calorie totali: | 1300

Sul primo c'è del formaggio.

Salvo cotanta squisitezza? n

Il codice in sé dovrebbe essere immediatamente comprensibile; ci soffermeremo ora solo su alcune righe che introducono novità o che hanno bisogno di qualche spiegazione in più.

linea 10 |  nome = raw_input('Nome: ')

raw_input() è una funzione built-in che consente di catturare input da tastiera dopo aver presentato l'eventuale prompt specificato come argomento; per maggiori informazioni su questa e altre funzioni riferirsi alla documentazione delle librerie standard, sezione "Built-in Functions".

linea 16 |  for i in range(len(qual)):

Anche la funzione range() è built-in nell'interprete python: serve a creare al volo una lista numerica da zero fino al valore specificato come argomento meno uno; può anche venir indicato un numero di partenza ed un eventuale passo per gli incrementi.

linea 23 |  primo = Primo()

Viene istanziato un oggetto della classe Primo.

linea 35 |  lista_piatti.append(primo)

Aggiunge un oggetto alla lista creata in precedenza.

linea 60 |  print string.rjust(`i.get_quanto()` + 'grammi di ' + i.get_nome() +
linea 61 |          '| ', 40) + `cal` + 'calorie'

Il modulo string contiene numerose funzioni per manipolare stringhe; in questo caso usiamo la funzione rjust per giustificare a destra il testo creando una tabella più facilmente leggibile; come si vede i parametri richiesti sono semplicemente la stringa stessa e il numero di caratteri che formano il campo in cui giustificare il testo.

linea 65 |  if primo.brodo:
linea 66 |      print 'Il primo è in brodo.'
linea 67 |  if primo.formaggio:
linea 68 |      print 'Sul primo c\'è del formaggio.'
linea 69 |  if secondo.e_un_pesce():
linea 70 |      print 'Il secondo è un pesce.'

Due modi di accedere ai dati di un oggetto: le prime due istruzioni if accedono direttamente alle variabili, mentre l'ultima invoca un metodo.

linea 74 |  f = open('ultimo_menu', 'w')

File

I file sono un altro tipo di oggetti predefinito in python; sono creati tramite l'istruzione built-in open() che richiede due argomenti entrambi stringhe: il primo sarà il nome del file, il secondo il tipo di accesso che si desidera ('r' indica lettura, 'w' scrittura, 'r+' lettura e scrittura; nei sistemi Microsoft e Macintosh si può aggiungere alla stringa il suffisso b che sta ad indicare un accesso di tipo binario, ad esempio 'r+b' apre un file in modalità binaria per lettura e scrittura); se il secondo argomento viene omesso, l'accesso in sola lettura viene scelto come default.
In caso di errori viene sollevata l'eccezione IOError.
Una volta aperto il file, possiamo usare alcune funzioni per accedere al suo contenuto:

# apriamo il file
f = open('MoanaStory.txt', 'r+')

# leggiamo tutto il suo contenuto
tutto = f.read()

# leggiamo la prima riga del file.
# successive chiamate a questa funzione leggeranno
# le righe seguenti.
riga = f.readline()

# scrittura
f.write('Tanto tempo fa, in una galassia molto, molto lontana...')

# chiudiamo il file
f.close()

È possibile "spostarsi" all'interno del file indicando il byte su cui ci si vuole posizionare per poi iniziare la lettura o scrittura:

# si posiziona all'inizio del file
f.seek(0)

# si posiziona al centesimo byte
f.seek(100)

# si posiziona quattro byte dopo l'attuale posizione
f.seek(4, 1)

# si posiziona a cinque byte dalla fine del file
f.seek(5, 2)

Come si vede il secondo argomento specifica come vada considerato il primo: se è 0 o omesso si usa la posizione dall'inizio del file, se 1 si considera rispetto alla posizione attuale, se 2 si calcola rispetto alla fine del file. Se si desidera conosce la posizione corrente si deve ricorrere alla funzione tell(). Tutte le funzioni che leggono dati da file (come read() e readline()) accettano un parametro opzionale intero, che sta ad indicare il numero massimo di byte da leggere.

linea 78 |  pickle.dump(lista_piatti, f)

Il modulo pickle torna utile in tutte quelle occasioni in cui si vogliano salvare dei dati per un successivo riutilizzo. Quella che qui andiamo ad utilizzare è una funzione (il modulo mette a disposizione anche oggetti per ottenere risultati simili) che salva su un file precedentemente aperto in scrittura un qualsiasi oggetto. In seguito sarà possibile accedere ai dati salvati in maniera molto semplice. Ad esempio:

#!/usr/bin/python

import pickle

f = open('ultimo_menu', 'r')

# carica l'oggetto salvato
lista = pickle.load(f)

f.close()

# a questo punto potremo utilizzare lista
# come siamo abituati a fare.

Il modulo pickle si presta ad usi molto più avanzati che potrete approfondire grazie alla Library References, dove c'è documentazione anche per moduli che implementano funzioni più o meno analoghe come cPickle, shelve e marshal.

Conclusioni

Sperando di aver chiarito più dubbi di quanti non ne abbia creato, vi lascio sperimentare python da soli e vi ricordo di tenere a portata di mano la vostra fida documentazione: python offre ancora moltissime cose da scoprire come ad esempio moduli per le regular expression, per pressoché tutti i servizi di rete, compressione, numeri random e inoltre debugger, profiler, possibilità di integrazione con Tk per creare interfacce grafiche, di integrazione col C e molto altro. Di alcune di queste cose (forse ;-) potrei parlare in futuri articoli.
Come sempre invito a scrivermi senza ritegno a <alberanid@libero.it> per dubbi, domande e suggerimenti. Se riuscite a farle arrivare fresche anche le torte al cioccolato sono molto apprezzate. ;-)

Grande concorso: chi indovinerà tutte le citazioni cinematografiche/culturali presenti in questo testo avrà l'onore di venire pubblicamente deriso nel prossimo articolo! O forse no.

Link