17: ANCORA SULLE FUNZIONI

FUNZIONI COME SOTTOPROGRAMMI

In informatica un sinonimo comune del termine "funzione" è "sottoprogramma" (Inglese: subprogram, subroutine). Ciò perchè un altro dei vantaggi dell'uso delle funzioni è quello di poter suddividere un programma lungo e complicato in "pezzi" separati, che possono essere sviluppati e testati separatamente (addirittura in parallelo, affidati a programmatori diversi). Ad esempio, il programma principale di un videogioco potrebbe assomigliare a questo:


inizializza_grafica()
carica_opzioni()
personaggio = scegli_personaggio()
esito = partita(personaggio)
if esito == True:
    vittoria()
else:
    sconfitta()
esci()

In una software house un programmatore esperto nella grafica potrebbe occuparsi della prima funzione, mentre gli esperti in Intelligenza Artificiale potrebbero sviluppare la funzione partita(), ecc.

Un precetto generale dell'informatica dice che le funzioni dovrebbero essere completamente indipendenti dal programma principale, cioè non fare riferimento ad alcuna sua risorsa: l'unica comunicazione tra programma e funzioni dovrebbe avvenire tramite i parametri (in entrata), ed il return (in uscita). In questo modo è possibile sviluppare le funzioni separatemente dal programma principale: ad un programmatore basterebbe sapere solo che dati prende in ingresso la sua funzione e cosa deve restituire. Ma c'è di più: una volta programmata una funzione che "fa qualcosa", si potrebbe riutilizzarla facilmente in un altro programma.

VARIABILI GLOBALI E VARIABILI LOCALI

Il principale problema pratico di questo approccio sta nell'uso delle variabili: se la funzione è una parte di codice separata dal programma principale, le sue variabili saranno anch'esse separate o saranno sempre le stesse? Se ci pensiamo bene sorgono molte domande:

I primi linguaggi di programmazione seguivano un approccio molto semplice: ad ogni nome corrispondeva una e una sola variabile, sempre la stessa, che poteva essere liberamente letta e modificata in qualsiasi punto del programma (nel programma principale o all'interno di una funzione). In programmi molto grandi con molte variabili questo portava a numerosi inconvenienti. Ad esempio, supponiamo che due programmatori diversi debbano scrivere due funzioni diverse dello stesso programma: con questo approccio essi dovrebbero "mettersi d'accordo" sul nome delle variabili da usare, per non correre il rischio che una delle funzioni modifichi le variabili dell'altra. Inoltre ogni volta che nel programma fosse necessario aggiungere nuove variabili, bisognerebbe sempre verificare di non aver già usato il loro nome da qualche altra parte.

I linguaggi più moderni hanno risposto a questi problemi creando il concetto di variabile globale e variabile locale:

Quindi i parametri e le variabili create all'interno di una funzione sono separate da quelle fuori da essa, anche se hanno lo stesso nome: in particolare i parametri, come ho già detto, sono delle nuove variabili che contengono una copia del valore usato nella chiamata. Vediamo alcuni esempi per chiarirci le idee:

NUOVO PROGRAMMA: variabili_locali.py

def prova(x):          # x e' il parametro (variabile locale)
    x = x + 1          # cambiamo il valore del parametro
    print ("Dentro la funzione x =", x)   
                       # scrivera' "Dentro la funzione x = 11"
                       # all'uscita dalla funzione x viene distrutta
	
# inizio del programma principale
a = 10                 # creiamo la variabile a
print ("Prima della chiamata a =", a)
                       # scriverà "Prima della chiamata a = 10"
prova(a)               # passiamo a come parametro alla funzione
print ("Dopo la chiamata a =", a)
                       # scrivera' "Dopo la chiamata a = 10": a non e' cambiata fuori dalla funzione

Nel primo esempio chiamiamo una funzione che modifica il valore del suo parametro: vediamo che all'uscita della funzione la variabile usata nella chiamata è rimasta immutata (la funzione ha modificato la variabile locale x ma non quella globale a).

Ora facciamo una cosa più ardita: usiamo lo stesso nome sia nel programma principale che nella funzione. Modificate così:


def prova(x):          # x e' il parametro (variabile locale)
    x = x + 1          # cambiamo il valore del parametro
    print ("Dentro la funzione x =", x)   
                       # scrivera' "Dentro la funzione x = 11"
                       # all'uscita dalla funzione x viene distrutta
	
# inizio del programma principale
x = 10                 # la variabile globale ha lo stesso nome del parametro
print ("Prima della chiamata x =", x)
                       # scriverà "Prima della chiamata x = 10"
prova(x)               # passiamo x come parametro alla funzione
print ("Dopo la chiamata x =", x)
                       # scrivera' "Dopo la chiamata a = 10": x non e' cambiata fuori dalla funzione

Vediamo che il risultato è esattamente lo stesso: la variabile x definita nel programma principale è globale e non è la stessa x (locale) parametro della funzione.

Infine creiamo delle variabili all'interno della funzione e vediamo cosa succede:


def prova(x):          # x e' il parametro (variabile locale)
    x = x + 1          # cambiamo il valore del parametro
    y = 20             # creiamo una nuova variabile
    print ("Dentro la funzione x =", x, "e y =", y)   
                       # scrivera' "Dentro la funzione x = 11 e y = 20"
                       # all'uscita dalla funzione x ed y vengono distrutte
	
# inizio del programma principale
x = 10                 # creiamo la variabile x
print ("Prima della chiamata x =", x)
                       # scriverà "Prima della chiamata x = 10"
prova(x)               # passiamo x come parametro alla funzione
print ("Dopo la funzione x =", x, "e y =", y)
                       # darà un NameError: y e' locale e non esiste fuori dalla funzione

Vediamo ora la risposta all'ultima domanda: può una funzione leggere o scrivere una variabile globale definita fuori da essa? Il cambiamento di una variabile globale all'interno di una funzione è considerato dai programmatori una pratica da evitare, in quanto nel programma principale chiameremo la funzione e dopo la chiamata la variabile risulterà misteriosamente "cambiata", senza nessuna assegnazione (e quindi senza che il programmatore se ne accorga).Per questo Python mette in atto una strategia piuttosto complicata, ma che è necessario conoscere:

Dalle due regole derivano queste conseguenze:

Questo meccanismo ci permette di creare nel corpo di una funzione tutte le variabili che vogliamo, senza preoccuparci dei loro nomi: questi non influenzeranno mai le variabili definite al di fuori dalla funzione. Vediamo un esempio:

NUOVO PROGRAMMA: variabili_locali_2.py

def nuova_a():
    a = 10      # qui a e' locale
    print ("Nella funzione a vale", a, "e b vale", b)

a = 5           # qui a e' globale (non e' la stessa a della funzione)
b = 20
print ("Prima della funzione a vale", a, "e b vale", b)
nuova_a()
print ("Dopo la funzione a vale", a, "e b vale", b)

L'output del programma è questo:


Prima della funzione a vale 5 e b vale 20
Nella funzione a vale 10 e b vale 20
Dopo la funzione a vale 5 e b vale 20

La variabile b è sempre rimasta la stessa (definita globalmente nel programma principale, e solamente letta dalla funzione), mentre la variabile a, definita nella funzione, è locale e quindi diversa (anche se ha lo stesso nome) da quella globale definita nel programma principale.

ECCEZIONI ALLE REGOLE

La ferrea Regola 2 impedisce completamente di modificare dall'interno di una funzione una variabile esterna ad essa. Questo comportamento, come abbiamo detto, è considerato pericoloso, ma a volte è impossibile da evitare. Pensiamo ad esempio ad un gioco che abbia varie opzioni globali, (la risoluzione video, il volume audio, il livello di difficolta ...): queste saranno probabilmente memorizzate in alcune variabili globali. Se adesso volessi implementare una serie di funzioni modifica_audio(), modifica_video(), modifica_difficolta() per cambiare queste opzioni, esse dovrebbero necessariamente agire su tali variabili globali, cosa impossibile con le istruzioni viste finora. Ecco perchè (in genere i linguaggi di programmazione non seguono alla lettera i dettami dell'informatica teorica!) è previsto un allentamento della Regola 2.

E' possibile dichiarare che una variabile all'interno di una funzione è globale con l'istruzione global, seguita dal nome della variabile (o da più nomi separati da virgole).

Modificate l'ultimo programma così:


def nuova_a():
    global a
    a = 10      # ora a e' globale
    print ("Nella funzione a vale", a, "e b vale", b)

a = 5           # qui a e' globale
b = 20
print ("Prima della funzione a vale", a, "e b vale", b)
nuova_a()
print ("Dopo la funzione a vale", a, "e b vale", b)

Ed ecco l'output:


Prima della funzione a vale 5 e b vale 20
Nella funzione a vale 10 e b vale 20
Dopo la funzione a vale 10 e b vale 20

Cosa è cambiato? Nell'ultima riga abbiamo che ora a è uguale a 10. Questo perchè questa volta la variabile a dentro la funzione è stata dichiarata global, quindi la funzione agisce sulla stessa variabile del programma principale.

CREIAMO IL NOSTRO MODULO

Nelle lezioni precedenti abbiamo incontrato spesso situazioni in cui l'utente doveva immettere un numero, per esempio, da 1 a 6, oppure una risposta "s" o "n". Queste situazioni generavano una discreta quantità di codice, perchè era necessario anche controllare che l'input fosse corretto ed eventualmente ripetere la domanda. Uno dei precetti base dell'informatica è il riutilizzo del codice: i programmatori detestano dover riscrivere più volte lo stesso codice (più fatica ma soprattutto più possibilità di errori), quindi una volta scritto e verificato un insieme di righe che sicuramente "fa quello che deve fare", si cerca di solito di trasformarlo in funzione in modo da poterlo riutilizzare tutte le volte che si vuole.

NUOVO PROGRAMMA: myinput.py

Questa volta non scriveremo un programma, ma un modulo di funzioni, in cui saranno contenute varie funzioni di input (senza alcun programma principale che le chiami). Potremo poi importare il modulo dai nostri programmi alla stessa maniera dei moduli predefiniti di Python (a patto che il programma ed il modulo siano nella stessa directory): from myinput import *.

Ecco la prima funzione:


def input_sn(prompt):
    while True:
        risp = input(prompt)
        if risp in ["s", "n"]:
            return risp
        print("Input scorretto")

Capire cosa fa la funzione dovrebbe essere abbastanza semplice: essa prende come parametro il prompt (cioè la domanda che dobbiamo fare all'utente) e restituisce la stringa immessa dall'utente, controllando che essa sia effettivamente "s" o "n". Un esempio di utilizzo potrebbe essere il seguente (ATTENZIONE! Non scrivete queste righe nel file, servono solo da esempio):


from myinput import *
. . .
risp = input_sn("Vuoi continuare? (s/n) ")

Una funzione un po' più sofisticata può richiedere come secondo parametro una lista di risposte possibili, sempre controllando che la risposta dell'utente corrisponda ad una di esse; aggiungete dopo la input_sn() quest'altra funzione (come sempre saltate una riga tra una funzione e l'altra per migliorare la leggibilità):


def input_lista(prompt, esatte):
    while True:
        risp = input(prompt)
        if risp in esatte:
            return risp
        print("Input scorretto")

Ed ecco un esempio del suo utilizzo:


from myinput import *
. . .
risp = input_lista("Vuoi la pasta o la pizza? ", ["pasta", "pizza"])

(notate che il secondo parametro deve essere una lista, quindi va scritta tra parentesi quadre (andava comunque bene anche una tuple, tra parentesi tonde).

Ecco una funzione che accetta come parametri, oltre al prompt, due numeri e restituisce un numero immesso dall'utente, solo se esso è compreso tra il minimo ed il massimo.


def input_int(prompt, min, max):
    while True:
        risp = int(input(prompt))
        if risp >= min and risp <= max:
            return risp
        print("Input scorretto")

Infine una funzione più sofisticata, che richiede all'utente una scelta tra un menu di opzioni: essa prende come parametro una lista con le varie opzioni, le stampa su più righe precedute da un numero, e restituisce il numero immesso dall'utente:


def input_menu(opzioni):
    for i in range(len(opzioni)):
        print(i + 1, " ", opzioni[i])  # per scrivere i numeri tra 1 e len(opzioni)
    while True:
        risp = int(input("? "))
        if risp >= 1 and risp <= len(opzioni):
            return risp
        print("Input scorretto")

Ad esempio potremmo chiamare la funzione in questo modo:


from myinput import *
. . .
opz = input_menu(["Nuovo", "Apri", "Salva", "Salva con nome", "Esci"])
if opz == 1:
    nuovo()
elif opz == 2:
    apri()
elif opz == 3:
    salva()
elif opz == 4:
    salva_nome()
else:
    esci()

Ora il nostro file consiste di quattro funzioni; se cercate di "lanciarlo" non accadrà nulla, perchè non c'è un programma principale da eseguire. Limitatevi a salvare il file: nel prossimo esercizio lo importeremo ed useremo le funzioni in esso definite.

ESERCIZIO 17.1 Trasformiamo gioco_porte.py in un programma a menu
Cercheremo di simulare quello che succede nei videogiochi, dove nella schermata iniziale è possibile scegliere tra varie opzioni. L'idea è quella di permettere all'utente di scegliere tra le seguenti: Per prima cosa importiamo i moduli necessari: oltre a random dovrete importare anche myinput, cioè il nostro modulo appena scritto. Possiamo fare subito una verifica lanciando il programma: se qualcosa è andato storto Python vi darà un ImportError: No module named 'myinput' (dovete salvare su disco, anche con Wing101, sia il file gioco_porte.py che il modulo myinput.py, nella stessa directory).

Decidiamo adesso quali saranno le variabili globali, quelle che dovranno rimanere per tutto il programma:
stringa_inizio = . . . come nel programma precedente (ci servirà per le istruzioni del gioco) stringa_morte = . . . idem stringa_credits = . . . scrivete qui il nome del gioco, il vostro nome e la data di creazione lista_menu = [ "Istruzioni", "Gioca", "Record", "Credits", "Esci" ] il nostro menu record = 100000000000 il record iniziale (vedi la discussione nell'ESERCIZIO 8.4)

Ecco ora il ciclo principale del programma: esso è tipico dei programmi strutturati come CLI (ad esempio IDLE). Il programma attende che l'utente inserisca un'opzione e poi in base ad essa esegue i vari compiti. L'insieme dei vari if ... elif viene detto in Inglese polling. Notate che nel ciclo usiamo la stringa ottenuta mediante la funzione input_menu() del nostro modulo.

while True:
    opz = input_menu(lista_menu)
    if opz == 1:
        mostra_istruzioni()
    elif opz == 2:
        partita()
    elif opz == 3:
        mostra_record()
    elif opz == 4:
        mostra_credits()
    else:
        break
Ora sta a voi scrivere le funzioni mostra_istruzioni(), partita(), mostra_record(), mostra_credits() (sono tutte funzioni che non prendono argomenti e che non restituiscono valori). La prima, la terza e la quarta sono del tutto banali (si tratta solo di stampare una stringa) e potreste obiettare che si potrebbero tranquillamente scrivere nel corpo del ciclo while. Come al solito, si preferisce spesso scrivere anche una sola riga di codice come funzione, perchè questo rende più leggibile il programma principale e più facile il successivo sviluppo. Per la seconda funzione dovrete riadattare il programma già sviluppato (tenete presente che essa dovrà modificare la variabile globale record ...)
SOLUZIONI

ESERCIZIO 17.2 (Molto difficile!)
Potreste anche stabilire un livello di difficoltà (ad esempio: Livello 1 = 8 porte, Livello 2 = 12 porte, Livello 3 = 16 porte). Questo porterebbe a ristrutturare molte parti del programma (tenete presente che anche i records dovrebbero andare in una lista, uno per ogni livello). Chi vuole può provarci.

Fine della lezione