A questo punto abbiamo già acquisito le principali conoscenze sul linguaggio, e penso sia opportuno fermarci un attimo per consolidare quanto abbiamo appreso. In questa lunga lezione vedremo qualche applicazione delle funzionalità base di Python ed introdurremo qui e là qualche nuova caratteristica. Potete passare alle prossime lezioni anche senza svolgere subito tutti gli esempi che vi propongo qui; talvolta, però, faremo riferimento ad essi più avanti per modificarli introducendo nuove funzionalità.
Nell'informatica avremo spesso il problema di rappresentare una situazione della vita reale mediante un programma. Questo vuol dire che dobbiamo scegliere delle opportune strutture di dati, che contengano le informazioni che ci serve conoscere, e le opportune funzioni che operino su questi dati per ottenere i risultati che vogliamo. Un programma di una certa complessità necessita quindi di una fase di progettazione, che consiste essenzialmente nell'analizzare le informazioni che sarà necessario conoscere ed il modo in cui il programma deve manipolare queste informazioni, traducendo poi tutto ciò in elementi del linguaggio (variabili e funzioni).
Vediamo un semplice esempio: vogliamo rappresentare un mazzo di carte, che ci potrà poi servire per simulare dei giochi. Analizziamo per prima cosa le informazioni che ci serve conoscere:
Vediamo poi le operazioni che possiamo compiere sul mazzo:
Ecco una soluzione per il nostro problema:
range()
che abbbiamo già visto quishuffle(list)
, che si trova nel modulo
random
e che permuta casualmente gli elementi di una listapop()
: essa, come abbiamo
visto, toglie l'ultimo numero dalla lista e lo restituisce al programma chiamante.random
ed inizializzate il generatore di numeri casuali (vedi
qui)semi = ["denari", "coppe", "spade", "bastoni"]
valori = ["asso", "due", "tre", "quattro", "cinque", "sei", "sette", "fante", "cavallo", "re"]
carte
(usando la funzione range()
e poi convertendo il
range
in list
)shuffle()
con la lista come argomento: una
volta chiamata i numeri saranno ordinati casualmentec = carte.pop()
(estrae l'ultima carta del mazzo e la
assegna alla variabile c
)s
la prima cifra di c
(il seme, da 0 a 3) ed alla
variabile val
la seconda (il valore, da 0 a 9: va bene così perchè dobbiamo usarlo come indice
in una lista)semi[s]
c'è il seme della nostra carta ed in valori[val]
c'è il valore: stampateasso di coppe
(oppure tre di spade
ecc.)Vogliamo adesso estrarre più carte di seguito, come si fa in un gioco di carte. Il metodo più semplice è
quello di inserire i passi 5 - 6 - 7 del programma in un ciclo for i in range(40):
che estrarrà
di seguito tutte le carte dal mazzo (provate).
Vogliamo però essere un po' più raffinati, e lasciare che sia
l'utente ad estrarre la carta premendo Invio: stamperemo un messaggio del tipo "Premi q per uscire,
Invio per estrarre una carta"
e poi gli mostreremo la carta uscita. Tra l'altro, dato che l'utente
non sa quante carte rimangono nel mazzo, dovremo gestire anche la possibilità che esso si svuoti: in questo
caso (come si fa in molti giochi) rimescoleremo le carte e ricominceremo da capo. Modificate il programma così:
carte
inizialmente vuota (la "riempiremo" nel ciclo principale, ogni volta
che dobbiamo rimescolare il mazzo)while
senza fine (guardate qui)input()
stampate il messaggio per l'utente e prendete in input la stringa risp
risp
è uguale a "q"
uscite immediatamente dal while
(ripassate
la Lezione 9), altrimenti ..."Attendi: sto mescolando il mazzo ..."
while
)while
, saluta l'utente per confermare che il programma è terminatoPremi q per uscire, Invio per estrarre una carta
Attendi: sto mescolando il mazzo...
asso di denari
Premi q per uscire, Invio per estrarre una carta
fante di spade
Premi q per uscire, Invio per estrarre una carta
fante di coppe
Premi q per uscire, Invio per estrarre una carta q
Fine del programma
SOLUZIONIInfine, dopo questo esercizio, potete utilizzare il mazzo di carte nel gioco banco.py
che abbiamo
già programmato qui: dovete modificare qualcosa sia nel programma originale sia in
questo appena scritto, ve lo lascio come esercizio.
Quando abbiamo introdotto le stringhe abbiamo detto che esistono tre sintassi diverse per definire una stringa:
"mamma", 'mamma', """mamma"""
. Mentre le prime due sono equivalenti, la terza permette di andare a
capo nella definizione della stringa, anche lasciando eventuali spazi bianchi iniziali nelle righe successive. In
IDLE potete spezzare l'istruzione in più righe: dopo aver scritto la prima riga premete Invio, e vedrete che IDLE non vi
risponderà ma vi manderà semplicemente a capo; la risposta arriverà solo quando premerete Invio dopo aver chiuso
le tre virgolette.
>>> print ("""1 2 3 4""")
1 2 3 4
In effetti non è proprio facile interpretare questa (singola) istruzione. Nella prima riga ci sono la
funzione print()
, la parentesi e le triple virgolette aperte; c'è anche il primo carattere della
stringa da stampare: "1". Ora la stringa continua nelle righe sottostanti andando a capo e finisce nella
quarta riga con le triple virgolette e la parentesi chiusa (le altre quattro righe sono la risposta di IDLE);
notate che gli spazi a sinistra nelle righe successive fanno parte della stringa e vengono stampati. Ecco un
altro esempio:
>>> print(""" CCCCC II AAAAAA OOOOOO CC II AA AA OO OO CC II AA AA OO OO CC II AAAAAA OO OO CCCCC II AA AA OOOOOO""")
CCCCC II AAAAAA OOOOOO CC II AA AA OO OO CC II AA AA OO OO CC II AAAAAA OO OO CCCCC II AA AA OOOOOO
qui sono andato a capo subito dopo le """
: questo vuol dire che la
stringa inizia con un a capo (notate la riga bianca tra l'istruzione e la risposta). Ricordo due cose:
print()
: se scrivete
solo il nome della stringa IDLE vi mostrerà gli a capo con la escape sequence
"\n"
PPPPPP AAAAAA CCCCCC MM MM AAAAAA NN NN
PP PP AA AA CC MMM MMM AA AA NNN NN
PPPPPP AA AA CC MMMMMMM AA AA NNNN NN
PP AAAAAA CC MM M MM AAAAAA NN NNNN
PP AA AA CCCCCC MM MM AA AA NN NN
Anche i cicli for
possono essere utilizzati per ottenere dei disegni.
Proviamo per prima cosa questo semplice programmino che usa la moltiplicazione di una stringa per un numero (di seguito è mostrato il suo output)
for i in range(1, 11): # i da 1 a 10
print(i * "*")
*
**
***
****
*****
******
*******
********
*********
**********
Vogliamo ora ottenere questo output:
1
22
333
4444
55555
666666
7777777
88888888
999999999
Saremmo forse tentati di scrivere questo:
for i in range(1, 10): # i da 1 a 9
print(i * i)
ma se proviamo otterremo una serie di numeri, perchè nella seconda riga Python sta
moltiplicando tra loro due numeri. Ci serve la funzione str()
che è un po' l'inversa delle
int()
e float()
, cioè trasforma, ad esempio, il numero 10 nella stringa "10"
for i in range(1, 10): # 1 da 1 a 9
print(i * str(i))
for i in range(1, 11)
non va bene perchè quando i
diventa 10 ...
Dovete scrivere solo l'ultima cifra del numero: è lo stesso problema che abbiamo incontrato nel mazzo di
carte.Infine una tecnica un po' più raffinata è quella di scrivere due cicli for
uno dentro
l'altro (annidati): nel ciclo esterno (con il contatore i
) definiamo una
stringa s
vuota, dopodichè usiamo un altro ciclo interno (con j
) per aggiungere
uno dopo l'altro i caratteri alla stringa. Usciti dal ciclo interno stamperemo la stringa (sareste forse
tentati di stampare i caratteri direttamente nel ciclo interno, ma dovete ricordare che la print()
va a capo ogni volta che viene chiamata, quindi possiamo stampare solo una riga per volta). Cercate di
capire (eventualmente con il debugger) come fa questo programma ad ottenere l'output:
for i in range(1, 11):
s = ""
for j in range(1, 11):
if j == i or j == 11 - i:
s += "+"
else:
s += "."
print(s)
+........+
.+......+.
..+....+..
...+..+...
....++....
....++....
...+..+...
..+....+..
.+......+.
+........+
range()
)
********** A OOOOOOOOOO X X X X X
********* A O O X X X X X
******** A O O X X X X X
******* A O O X X X X X
****** A O O X X X X X
***** A O O X X X X X
**** A O O X X X X X
*** A O O X X X X X
** A O O X X X X X
* A OOOOOOOOOO X X X X X
MM D 1110000
MMMM DDD 0222000
MMMMMM DDDDD 0033300
MMMMMMMM DDDDDDD 0004440
MMMMMMMMMM DDDDDDDDD 0000555
SOLUZIONIL'introduzione del computer nel calcolo matematico ha reso estremamente semplici molti procedimenti che una volta richiedevano pesantissimi calcoli, lunghi e complicati anche con una semplice calcolatrice. In particolare un algoritmo iterativo è un procedimento che cerca di raggiungere un determintato risultato avvicinandosi sempre di più ad esso con varie prove: si parte da un valore di prova iniziale, si valuta l'errore che si è commesso e si cerca un nuovo valore di prova "più vicino" al risultato cercato. Ripetendo il procedimento ci si avvicina sempre più finchè si arriva alla precisione voluta.
Un semplice esempio è il cosiddetto "algoritmo babilonese" per il calcolo della radice quadrata (per una trattazione approfondita vedi Wikipedia). Supponiamo di dover trovare la radice quadrata di un numero n: questo equivale ad "indovinare" il lato di un quadrato che ha area n. Proviamo prima con un rettangolo di base b scelta a caso (ad esempio b = 1): facendo la divisione h = n / b troviamo l'altezza h del rettangolo. Ora, se b == h abbiamo trovato la radice (il rettangolo è un quadrato), se b > h abbiamo scelto b troppo grande e se b < h lo abbiamo scelto troppo piccolo: quindi la radice vera è un numero compreso tra b ed h. Allora basta prendere come nuova base b = (b + h) / 2 (la media tra b e h): questo numero è senz'altro più vicino alla radice che cerchiamo. Iterando questo procedimento otteniamo una successione di numeri b1, b2, b3, ... che si avvicinano sempre più alla nostra radice quadrata (in matematica si parla di successione convergente).
In questo tipo di algoritmi si usa di solito un ciclo while
, perchè non si sa a priori quante volte
si debba ripetere il procedimento. Per capire come dovremo impostare la condizione di controllo è necessario
considerare che nel calcolo scientifico non si testa mai l'eguaglianza tra due numeri float
(cioè con la virgola) con l'operatore ==
, in quanto il computer esegue sempre calcoli approssimati
(seppure con un numero molto grande di cifre decimali): questi calcoli possono portare ad errori
di arrotondamento e quindi i numeri potrebbero risultare diversi anche se teoricamente dovrebbero essere
uguali (o viceversa). Quindi due numeri float a
e b
sono considerati "uguali" se la loro
distanza abs(a − b)
(il valore assoluto di
a − b
) è molto piccola (tradizionalmente in matematica si usa la lettera greca
ε (epsilon) per indicare un numero positivo molto piccolo).
Scriviamo quindi un programma che trova la radice quadrata di un numero; in esso inseriremo anche la stampa di tutti i risultati intermedi, per "vedere" meglio il nostro algoritmo all'opera
epsilon = 0.000001
(Python accetta anche la notazione esponenziale 1 x 10-6: potete scrivere
epsilon = 1e−6
, senza spazi tra i caratteri dell'esponente).n
, il numero (float) di cui cerchiamo la radice.b
(la nostra radice) a 1 e h
a n
(cioè n / b):
questi valori costituiranno la nostra "prima prova".i
a 0 (ci servirà per stampare i risultati intermedi)while abs(b - h) > epsilon:
(ricordate che nel while
dovete indicare le condizioni per ripetere il ciclo, non quelle per uscire!). Nel corpo del
ciclo ...b
come abbiamo detto sopra e riassegnatelo a sè stessah
(sempre come abbiamo detto sopra) e riassegnatelo a sè stessa: ora
abbiamo in b
ed h
i nuovi valori"Passo x: valore yyy"
(che variabili dovete scrivere in x ed y?).
Qui finisce il corpo del while
: Python controllerà di nuovo la distanza tra b
ed h
e ripeterà il procedimento se necessario.Inserisci il numero 81
Passo 1 valore: 41.0
Passo 2 valore: 21.48780487804878
Passo 3 valore: 12.628692450375128
Passo 4 valore: 9.521329066772005
Passo 5 valore: 9.014272376994608
Passo 6 valore: 9.000011298790216
Passo 7 valore: 9.000000000007091
La radice trovata e' 9.000000000007091
SOLUZIONILanciando il programma più volte con numeri diversi vedremo come i valori che trova nei vari passi si avvicinano effettivamente
sempre più al valore esatto (il risultato, però, come vedete, è approssimato: potete renderlo più preciso diminuendo
epsilon
). Notiamo anche che la convergenza è piuttosto veloce: di solito il programma si ferma dopo meno di 10
iterazioni.
b
che otteniamo non è convergente ed il programma entra in un ciclo senza fine. Tutto questo è logico in
quanto la radice quadrata di un numero negativo non esiste. Dopo che l'utente ha inserito il numero, bisogna
quindi verificate che esso sia positivo: se lo è si può proseguire, altrimenti si avvisa l'utente che la
radice del numero non esiste.Hai chiesto la mano della figlia del Re, ma il padre, prima di concedertela, ti ha sottoposto ad una crudele prova. Sei al centro di un labirinto e per dieci volte dovrai scegliere tra una porta a sinistra ed una a destra: se aprirai quella giusta potrai proseguire, ma se sbaglierai troverai un mostro che ti ucciderà.
Nel gioco dovremo quindi indovinare una stringa composta casualmente da 10 caratteri "s" o "d" (ad esempio "ssdsddsdds" oppure "dddssdssdd") digitando un carattere per volta. Ogni volta che sbaglieremo dovremo ricominciare da capo (e quindi ricordare tutti i caratteri giusti già immessi). Per rendere la cosa più simpatica ogni volta che moriremo cercheremo di simulare un minimo di grafica con una stringa "artistica" come quelle che abbiamo già visto.
Nella prima parte del programma inizializzeremo le variabili che ci servono e daremo le istruzioni necessarie all'utente, poi dovremo programmare il gioco vero e proprio.
stringa_inizio = """
Sei imprigionato in un labirinto.
Per 10 volte dovrai aprire una porta a sinistra o una a destra. Se sbaglierai scelta MORIRAI!
Premi INVIO quando sei pronto
"""
stringa_morte = """
AAAARRRRGGGGHHHH! SEI MORTO!
##
######
##
##
##
Ma per fortuna puoi ricominciare!
"""
E' necessario soffermarci un po' sul passo 2, per farvi notare ancora una volta come i programmatori
odiino inserire le costanti (numeri e stringhe) durante il programma: queste stringhe potrebbero tranquillamente
essere scritte nei punti in cui dobbiamo stamparle (ad esempio: print("""Sei imprigionato ...)
,
ma questo porterebbe ad una scarsa leggibilità (le stringhe si estendono su più righe e sarebbe addirittura
difficile capire qual è l'istruzione successiva). Allora si preferisce assegnare il loro valore ad una
variabile e scrivere poi print(stringa_inizio)
che è molto più facile da leggere. Inoltre
inizializzando queste variabili tutte insieme nella prima parte del programma è più facile ritrovarle se
dovessimo decidere di cambiarle (ad esempio potreste sostituire la mia stringa_morte
con una
vostra creazione).
porte
inizialmente vuotafor
concatenate alla stringa (con l'operatore +=
) dieci stringhe
casuali "s" o "d". ATTENZIONE: per creare una stringa che sia casualmente "s" o "d" ci sono più tecniche differenti.
Ripassate le funzioni nel modulo random qui)input(stringa_inizio)
che stamperà la spiegazione del gioco ed aspetterà
che voi premiate Invio: notate che in questo caso non c'è bisogno di assegnare il valore restituito a nessuna
variabileProvate il programma fin qui: dovrebbe stampare la spiegazione, attendere che premiate Invio e poi uscire.
Ora l'utente deve immettere per 10 volte "s" o "d": se il valore sarà uguale a quello corrispondente nella
stringa porte
potrà andare avanti, altrimenti dovrà ricominciare da capo. Questo fatto introduce
una complicazione: avremo naturalmente bisogno di un ciclo che conti le risposte esatte, ma questo dovrà ricominciare
ogni volta che sbagliamo.
Per ottenere questo comportamento non possiamo usare il for
, in quanto in esso Python incrementa
automaticamente la variabile contatore e non permette di modificarla all'interno del ciclo. Questo è invece possibile
con il while
(con la variabile contatore): nel corpo del while
inseriremo la domanda
all'utente e la verifica: se la risposta sarà corretta il contatore sarà incrementato, altrimenti sarà resettato a 0
per ricominciare. Continuate così:
i
a 0while i < 10:
risp
stampando il prompt "Porta n. x (s/d)? "
input()
deve essere un'unica stringa, ottenuta eventualmente
concatenando più stringhe con l'operatore +
i
da 0 a 9 (in modo che corrisponda agli indici della stringa porte
che variano anch'essi da 0 a 9), ma nel prompt le porte dovranno essere numerate da 1 a 10risp
con la risposta giusta che si trova in porte[i]
(ricordate
questo)"Esatto!"
ed incrementate il contatore per affrontare la prossima portastringa_morte
e riportate il contatore a 0 per ricominciare. Qui finisce
il corpo del ciclo while
Provate il programma. Per migliorare l'aspetto ludico possiamo misurare il tempo che il giocatore impiega a finire il gioco
t1 = time()
che ci da il tempo
inizialet2 = time()
per avere il tempo finale.
Il tempo impiegato (in secondi) sarà allora t2 - t1
. Stampatelo nel messaggio finaleA questo punto, come abbiamo visto nel programma indovina_numero.py, possiamo inserire anche la gestione dei record, che lascio a voi.
Riprendiamo l'esempio fatto all'inizio della Lezione 11. Abbiamo fatto notare che per risolvere molti problemi viene naturale l'uso di tipi di dati complessi, formati dall'aggregazione di più dati atomici. I linguaggi di programmazione hanno risposto a quest'esigenza con la programmazione orientata agli oggetti (OOP), che è però un argomento piuttosto avanzato e non tratteremo qui. In questa sede ci limiteremo a mostrare l'uso delle liste di liste (cioè liste i cui elementi sono a loro volta liste).
Immaginiamo di dover scrivere un programma che gestisca lo scrutinio finale di una classe: la classe sarà formata da vari studenti ed ognuno avrà i suoi voti. Per semplificare al massimo faremo le seguenti assunzioni:
Passiamo alla rappresentazione dei dati: potremo rappresentare uno studente con una lista di cinque elementi:
indice nella lista | significato | tipo di dato |
---|---|---|
0 | cognome | str (stringa) |
1 | nome | str |
2 | voto in Italiano | int (numero intero) |
3 | voto in Matematica | int |
4 | esito dello scrutinio | str ("p", "r" o "b") |
Quindi, se a
è un alunno, a[0]
sarà il suo cognome, a[1]
il suo
nome, e così via. La classe sarà allora una lista di alunni (e quindi, come abbiamo detto, una lista
di liste). Come facciamo allora a leggere il voto in Italiano del secondo alunno della classe? La sintassi
è molto semplice, ed assomiglia a quella che si usa in Matematica per le matrici: il secondo alunno è
l'elemento di indice 1 nella classe, e il suo voto in Italiano è l'elemento di indice 2 dell'alunno. Quindi
dovremo scrivere classe[1][2]
(cioè l'elemento 2 dell'elemento 1 della classe). Se vi sembra che
tutto questo sia troppo complicato provate a fare un po' di pratica con IDLE
classe = [ ["Abbotti", "Cesare", 5, 6, "r"], ["Bombetti", "Camilla", 9, 10, "p"],
["Casini", "Piergiorgio", 4, 3, "b"], ["De Rompis", "Anna", 7, 5, "r"] ]
Notate l'uso delle parentesi quadre una dentro l'altra per indicare che gli elementi della lista classe
sono a loro volta delle liste. Ora provate a dare i seguenti comandi (ho omesso l'output) cercando di indovinare il
risultato prima di premere Invio:
>>> classe[0][0]
>>> classe[0][1]
>>> classe[1][0]
>>> classe[1][1]
>>> classe[2][2]
>>> classe[2][3]
>>> classe[4][4]
Ecco adesso un programma che ci permette di inserire nome, cognome e voti di ogni alunno; volutamente, per il momento, trascuriamo l'esito dello scrutinio (lo farete voi come esercizio)
materie = ["Italiano", "Matematica"]
classe = [] # la nostra classe, inizialmente vuota
risp = "s"
while risp == "s": # ciclo per inserire gli alunni
alunno = []
print("Alunno", len(classe) + 1) # stampa il numero dell'alunno corrente
alunno.append(input("Inserisci il cognome ")) # piu' compatto, senza usare variabili intermedie
alunno.append(input("Inserisci il nome "))
for i in range(len(materie)): # al solito, non scriviamo range(2)
alunno.append(int(input("Inserisci il voto in " + materie[i] + " ")))
classe.append(alunno) # aggiungiamo l'alunno alla classe
risp = input("Vuoi inserire un altro alunno?(s/n) ")
i
che iteri sugli indici della lista classe
per ottenere un output
simile a questo:1 Abbotti Cesare Italiano 6 Matematica 5
2 Bombetti Camilla Italiano 9 Matematica 10
...
ATTENZIONE: Ottenere quest'output non è facile: dovete usare i doppi indici per riferirvi ai dati dei
singoli alunni ed il carattere di tabulazione
"\t" per allineare le varie parole. Purtroppo se introducete
nomi lunghi e nomi corti (ad es. "Ugo" e "Alessandro") la tabulazione non si allineerà correttamente dandovi
un risultato esteticamente brutto. Come ho detto, ci sono delle istruzioni Python per formattare le stringhe
ma per il momento non ne parleremo. Può anche essere utile, anzichè scrivere una complicatissima
print()
, usare una variabile intermedia s
da costruire man mano aggiungendo i
vari dati.
Infine vediamo come risolvere il problema dell'esito dello scrutinio: questo dato non va immesso dall'utente ma deve essere ricavato dal programma stesso
alunno
la stringa appropriata. Modificate
anche la parte del programma che stampa i risultati per includere questa nuova stringa.promossi, rimandati, bocciati
inizialmente vuote, ed inserirvi gli
alunni ...
Fine della lezione