Previous Up Next

Buy this book at Amazon.com

Chapter 14  File

Questo capitolo spiega il concetto di programma “persistente”, che mantiene i propri dati in archivi permanenti, e mostra come usare diversi tipi di archivi, come file e database.

14.1  Persistenza

La maggior parte dei programmi che abbiamo visto finora sono transitori, nel senso che vengono eseguiti per breve tempo e producono un risultato, ma quando vengono chiusi i loro dati svaniscono. Se rieseguite il programma, questo ricomincia da zero.

Altri programmi sono persistenti: sono eseguiti per un lungo tempo (o di continuo); mantengono almeno una parte dei loro dati archiviati in modo permanente, come su un disco fisso; e se vengono arrestati e riavviati, riprendono il loro lavoro da dove lo avevano lasciato.

Esempi di programmi persistenti sono i sistemi operativi, eseguiti praticamente ogni volta che un computer viene acceso, e i web server, che lavorano di continuo in attesa di richieste provenienti dalla rete.

Per i programmi, uno dei modi più semplici di mantenere i loro dati è di leggerli e scriverli su file di testo. Abbiamo già visto qualche programma che legge dei file di testo; in questo capitolo ne vedremo alcuni che li scrivono.

Un’alternativa è conservare la situazione del programma in un database. In questo capitolo mostrerò un semplice database e un modulo, pickle, che rende agevole l’archiviazione dei dati.

14.2  Lettura e scrittura

Un file di testo è un una sequenza di caratteri salvata su un dispositivo permanente come un disco fisso, una memoria flash o un CD-ROM. Abbiamo già visto come aprire e leggere un file nel Paragrafo 9.1.

Per scrivere un file, lo dovete aprire indicando la modalità 'w' come secondo parametro:

>>> fout = open('output.txt', 'w')

Se il file esiste già, l’apertura in modalità scrittura lo ripulisce dai vecchi dati e riparte da zero, quindi fate attenzione! Se non esiste, ne viene creato uno nuovo.

open restituisce un oggetto file che fornisce i metodi per lavorare con il file.

Il metodo write inserisce i dati nel file.

>>> riga1 = "E questa qui è l'acacia,\n"
>>> fout.write(riga1)
25

Il valore di ritorno è il numero di caratteri che sono stati scritti. L’oggetto file tiene traccia di dove si trova, e se invocate ancora il metodo write, aggiunge i nuovi dati in coda al file.

>>> riga2 = "l'emblema della nostra terra.\n"
>>> fout.write(riga2)
30

Quando avete finito di scrivere, è opportuno chiudere il file.

>>> fout.close()

Se non chiudete il file, viene comunque chiuso automaticamente al termine del programma.

14.3  L’operatore di formato

L’argomento di write deve essere una stringa, e se volessimo inserire valori di tipo diverso in un file dovremmo prima convertirli in stringhe. Il metodo più semplice per farlo è usare str:

>>> x = 52
>>> fout.write(str(x))

Un’alternativa è utilizzare l’operatore di formato, %. Quando viene applicato agli interi, % rappresenta l’operatore modulo. Ma se il primo operando è una stringa, % diventa l’operatore di formato.

Il primo operando è detto stringa di formato, che contiene una o più sequenze di formato, che specificano il formato del secondo operando. Il risultato è una stringa.

Per esempio, la sequenza di formato '%d' significa che il secondo operando dovrebbe essere nel formato di numero intero in base decimale:

>>> cammelli = 42
>>> '%d' % cammelli
'42'

Il risultato è la stringa '42', che non va confusa con il valore intero 42.

Una sequenza di formato può comparire dovunque all’interno di una stringa, e così possiamo incorporare un valore in una frase:

>>> 'Ho contato %d cammelli.' % cammelli
'Ho contato 42 cammelli.'

Se nella stringa c’è più di una sequenza di formato, il secondo operando deve essere una tupla. Ciascuna sequenza di formato corrisponde a un elemento della tupla, nell’ordine.

L’esempio che segue usa '%d' per formattare un intero, '%g' per formattare un decimale a virgola mobile (floating-point), e '%s' per formattare una stringa:

>>> 'In %d anni ho contato %g %s.' % (3, 0.1, 'cammelli')
'In 3 anni ho contato 0.1 cammelli.'

Naturalmente, il numero degli elementi nella tupla deve essere pari a quello delle sequenze di formato nella stringa, ed i tipi degli elementi devono corrispondere a quelli delle sequenze di formato:

>>> '%d %d %d' % (1, 2)
TypeError: not enough arguments for format string
>>> '%d' % 'dollari'
TypeError: %d format: a number is required, not str

Nel primo esempio, non ci sono abbastanza elementi; nel secondo, l’elemento è del tipo sbagliato.

Per saperne di più sull’operatore di formato: https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting. Un’alternativa più potente è il metodo di formato delle stringhe, potete leggerne la documentazione sul sito https://docs.python.org/3/library/stdtypes.html#str.format.

14.4  Nomi di file e percorsi

Il file sono organizzati in directory (chiamate anche “cartelle”). Ogni programma in esecuzione ha una “directory corrente”, che è la directory predefinita per la maggior parte delle operazioni che compie. Ad esempio, quando aprite un file in lettura, Python lo cerca nella sua directory corrente.

Il modulo os fornisce delle funzioni per lavorare con file e directory (“os” sta per “sistema operativo”). os.getcwd restituisce il nome della directory corrente:

>>> import os
>>> cwd = os.getcwd()
>>> cwd
'/home/dinsdale'

cwd sta per “current working directory” (directory di lavoro corrente). Il risultato di questo esempio è /home/dinsdale, che è la directory home di un utente di nome dinsdale.

Una stringa come '/home/dinsdale', che individua la collocazione di un file o una directory, è chiamata percorso.

Un semplice nome di file, come memo.txt è pure considerato un percorso, ma è un percorso relativo perché si riferisce alla directory corrente. Se la directory corrente è /home/dinsdale, il nome di file memo.txt starebbe per /home/dinsdale/memo.txt.

Un percorso che comincia per / non dipende dalla directory corrente; viene chiamato percorso assoluto. Per trovare il percorso assoluto del file, si può usare os.path.abspath:

I percorsi visti finora sono semplici nomi di file, quindi sono percorsi relativi alla directory corrente. Per avere invece il percorso assoluto, potete usare os.path.abspath:

>>> os.path.abspath('memo.txt')
'/home/dinsdale/memo.txt'

os.path fornisce altre funzioni per lavorare con nomi di file e percorsi. Per esempio, os.path.exists controlla se un file o una cartella esistono:

>>> os.path.exists('memo.txt')
True

Se esiste, os.path.isdir controlla se è una directory:

>>> os.path.isdir('memo.txt')
False
>>> os.path.isdir('/home/dinsdale')
True

Similmente, os.path.isfile controlla se è un file.

os.listdir restituisce una lista dei file e delle altre directory nella cartella indicata:

>>> os.listdir(cwd)
['musica', 'immagini', 'memo.txt']

Per dimostrare l’uso di queste funzioni, l’esempio seguente “esplora” il contenuto di una directory, stampa il nome di tutti i file e si chiama ricorsivamente su tutte le sottodirectory.

def esplora(dirnome):
    for nome in os.listdir(dirnome):
        percorso = os.path.join(dirnome, nome)

        if os.path.isfile(percorso):
            print(percorso)
        else:
            esplora(percorso)

os.path.join prende il nome di una directory e il nome di un file e li unisce a formare un percorso completo.

Il modulo os contiene una funzione di nome walk che è simile a questa ma più versatile. Come esercizio, leggetene la documentazione e usatela per stampare i nomi dei file di una data directory e delle sue sottodirectory. Soluzione: http://thinkpython2.com/code/walk.py.

14.5  Gestire le eccezioni

Parecchie cose possono andare storte quando si cerca di leggere e scrivere file. Se tentate di aprire un file che non esiste, si verifica un IOError:

>>> fin = open('file_corrotto')
IOError: [Errno 2] No such file or directory: 'file_corrotto'

Se non avete il permesso di accedere al file:

>>> fout = open('/etc/passwd', 'w')
PermissionError: [Errno 13] Permission denied: '/etc/passwd'

E se cercate di aprire una directory in lettura, ottenete:

>>> fin = open('/home')
IsADirectoryError: [Errno 21] Is a directory: '/home'

Per evitare questi errori, potete usare funzioni come os.path.exists e os.path.isfile, ma ci vorrebbe molto tempo e molto codice per controllare tutte le possibilità (se “Errno 21” significa qualcosa, ci sono almeno 21 cose che possono andare male).

È meglio allora andare avanti e provare—e affrontare i problemi quando si presentano—che è proprio quello che fa l’istruzione try. La sintassi è simile a un’istruzione if...else:

try:    
    fin = open('file_corrotto')
except:
    print('Qualcosa non funziona.')

Python comincia con l’eseguire la clausola try. Se tutto va bene, tralascia la clausola except e procede. Se si verifica un’eccezione, salta fuori dalla clausola try e va ad eseguire la clausola except.

Utilizzare in questo modo l’istruzione try viene detto gestire un’eccezione. Nell’esempio precedente, la clausola except stampa un messaggio di errore che non è di grande aiuto. In genere, gestire un’eccezione vi dà la possibilità di sistemare il problema, o riprovare, o per lo meno arrestare il programma in maniera morbida.

14.6  Database

Un database è un file che è progettato per archiviare dati. Molti database sono organizzati come un dizionario, nel senso che fanno una mappatura da chiavi in valori. La grande differenza tra database e dizionari è che i primi risiedono su disco (o altro dispositivo permanente), e persistono quando il programma viene chiuso.

Il modulo dbm fornisce un’interfaccia per creare e aggiornare file di database. Come esempio, creerò un database che contiene le didascalie di alcuni file di immagini.

Un database si apre in modo simile agli altri file:

>>> import dbm
>>> db = dbm.open('didascalie', 'c')

La modalità 'c' significa che il database deve essere creato se non esiste già. Il risultato è un oggetto database che può essere utilizzato (per la maggior parte delle operazioni) come un dizionario.

Se create un nuovo elemento, dbm aggiorna il file di database.

>>> db['cleese.png'] = 'Foto di John Cleese.'

Quando accedete a uno degli elementi, dbm legge il file:

>>> db['cleese.png']
b'Foto di John Cleese.'

Il risultato è un oggetto bytes, ed è per questo che comincia per b. Un oggetto bytes è per molti aspetti simile ad una stringa. Quando approfondirete Python la differenza diverrà importante, ma per ora possiamo soprassedere.

Se fate una nuova assegnazione a una chiave esistente, dbm sostituisce il vecchio valore:

>>> db['cleese.png'] = 'Foto di John Cleese che cammina in modo ridicolo.'
>>> db['cleese.png']
b'Foto di John Cleese che cammina in modo ridicolo.'

Certi metodi dei dizionari, come keys e items, non funzionano con gli oggetti database, ma funziona l’iterazione con un ciclo for.

for chiave in db:
    print(chiave, db[chiave])

Come con gli altri file, dovete chiudere il database quando avete finito:

>>> db.close()

14.7  Pickling

Un limite di dbm è che le chiavi e i valori devono essere delle stringhe, oppure bytes. Se cercate di utilizzare qualsiasi altro tipo, si verifica un errore.

Il modulo pickle può essere di aiuto: trasforma quasi ogni tipo di oggetto in una stringa, adatta per essere inserita in un database, e quindi ritrasforma la stringa in oggetto.

pickle.dumps accetta un oggetto come parametro e ne restituisce una serializzazione, ovvero un rappresentazione sotto forma di una stringa (dumps è l’abbreviazione di “dump string”, scarica stringa):

>>> import pickle
>>> t = [1, 2, 3]
>>> pickle.dumps(t)
b'\x80\x03]q\x00(K\x01K\x02K\x03e.'

Il formato non è immediatamente leggibile: è progettato per essere facile da interpretare da parte di pickle. In seguito, pickle.loads (“carica stringa”) ricostruisce l’oggetto:

>>> t1 = [1, 2, 3]
>>> s = pickle.dumps(t1)
>>> t2 = pickle.loads(s)
>>> t2
[1, 2, 3]

Sebbene il nuovo oggetto abbia lo stesso valore di quello vecchio, non è in genere lo stesso oggetto:

>>> t1 == t2
True
>>> t1 is t2
False

In altre parole, fare una serializzazione con pickle e poi l’operazione inversa, ha lo stesso effetto di copiare l’oggetto.

Potete usare pickle per archiviare in un database tutto ciò che non è una stringa. In effetti, questa combinazione è tanto frequente da essere stata incapsulata in un modulo chiamato shelve.

14.8  Pipe

Molti sistemi operativi forniscono un’interfaccia a riga di comando, nota anche come shell. Le shell sono dotate di comandi per spostarsi nel file system e per lanciare le applicazioni. Per esempio, in UNIX potete cambiare directory con il comando cd, visualizzarne il contenuto con ls, e lanciare un web browser scrivendone il nome, per esempio firefox.

Qualsiasi programma lanciabile dalla shell può essere lanciato anche da Python usando un oggetto pipe, che rappresenta un programma in esecuzione.

Ad esempio, il comando Unix ls -l di norma mostra il contenuto della cartella attuale (in formato esteso). Potete lanciare ls anche con os.popen1:

>>> cmd = 'ls -l'
>>> fp = os.popen(cmd)

L’argomento è una stringa che contiene un comando shell. Il valore di ritorno è un oggetto che si comporta come un file aperto. Potete leggere l’output del processo ls una riga per volta con readline, oppure ottenere tutto in una volta con read:

>>> res = fp.read()

Quando avete finito, chiudete il pipe come se fosse un file:

>>> stat = fp.close()
>>> print(stat)
None

Il valore di ritorno è lo stato finale del processo ls; None significa che si è chiuso normalmente (senza errori).

Altro esempio, in molti sistemi Unix il comando md5sum legge il contenuto di un file e ne calcola una checksum . Per saperne di più: http://it.wikipedia.org/wiki/MD5. Questo comando è un mezzo efficiente per controllare se due file hanno lo stesso contenuto. La probabilità che due diversi contenuti diano la stessa checksum è piccolissima (per intenderci, è improbabile che succeda prima che l’universo collassi).

Potete allora usare un pipe per eseguire md5sum da Python e ottenere il risultato:

>>> nomefile = 'book.tex'
>>> cmd = 'md5sum ' + nomefile
>>> fp = os.popen(cmd)
>>> res = fp.read()
>>> stat = fp.close()
>>> print(res)
1e0033f0ed0656636de0d75144ba32e0  book.tex
>>> print(stat)
None

14.9  Scrivere moduli

Qualunque file che contenga codice Python può essere importato come modulo. Per esempio, supponiamo di avere un file di nome wc.py che contiene il codice che segue:

def contarighe(nomefile):
    conta = 0
    for riga in open(nomefile):
        conta += 1
    return conta

print(contarighe('wc.py'))

Se eseguite questo programma, legge se stesso e stampa il numero delle righe nel file, che è 7. Potete anche importare il file in questo modo:

>>> import wc
7

Ora avete un oggetto modulo wc:

>>> wc
<module 'wc' from 'wc.py'>

L’oggetto modulo fornisce contarighe:

>>> wc.contarighe('wc.py')
7

Ecco come scrivere moduli in Python.

L’unico difetto di questo esempio è che quando importate il modulo, esegue anche il codice di prova in fondo. Di solito, invece, un modulo definisce solo delle nuove funzioni ma non le esegue.

I programmi che verranno importati come moduli usano spesso questo costrutto:

if __name__ == '__main__':
    print(contarighe('wc.py'))

__name__ è una variabile predefinita che viene impostata all’avvio del programma. Se questo viene avviato come script, __name__ ha il valore '__main__'; in quel caso, il codice viene eseguito. Altrimenti, se viene importato come modulo, il codice di prova viene saltato.

Come esercizio, scrivete questo esempio in un file di nome wc.py ed eseguitelo come script. Poi avviate l’interprete e scrivete import wc. Che valore ha __name__ quando il modulo viene importato?

Attenzione: Se importate un modulo già importato, Python non fa nulla. Non rilegge il file, anche se è cambiato.

Se volete ricaricare un modulo potete usare la funzione reload, ma potrebbe dare delle noie, quindi la cosa più sicura è riavviare l’interprete e importare nuovamente il modulo.

14.10  Debug

Quando leggete e scrivete file, è possibile incontrare dei problemi con gli spaziatori. Questi errori sono difficili da correggere perché spazi, tabulazioni e ritorni a capo di solito non sono visibili.

>>> s = '1 2\t 3\n 4'
>>> print(s)
1 2  3
 4

La funzione predefinita repr può essere utile: riceve come argomento qualsiasi oggetto e restituisce una rappresentazione dell’oggetto in forma di stringa. Per le stringhe, essa rappresenta gli spaziatori con delle sequenze con barra inversa:

>>> print(repr(s))
'1 2\t 3\n 4'

Questa funzione può quindi aiutare nel debug.

Un altro problema in cui potreste imbattervi è che sistemi diversi usano caratteri diversi per indicare la fine della riga. Alcuni usano il carattere di ritorno a capo, rappresentato da \n. Altri usano quello di ritorno carrello, rappresentato da \r. Alcuni usano entrambi. Se spostate i file da un sistema all’altro, queste incongruenze possono causare errori.

Comunque, esistono per ogni sistema delle applicazioni che convertono da un formato a un altro. Potete trovarne (e leggere altro sull’argomento) sul sito http://it.wikipedia.org/wiki/Ritorno_a_capo. Oppure, naturalmente, potete scriverne una voi.

14.11  Glossario

persistente:
Di un programma eseguito per un tempo indefinito e che memorizza almeno parte dei suoi dati in dispositivi permanenti.
operatore di formato:
Operatore indicato da %, che a partire da una stringa di formato e una tupla produce una stringa che include gli elementi della tupla, ciascuno nel formato specificato dalla stringa di formato.
stringa di formato:
Stringa usata con l’operatore di formato e che contiene le sequenze di formato.
sequenza di formato:
Sequenza di caratteri in una stringa di formato, come %d, che specifica in quale formato deve essere un valore.
file di testo:
Sequenza di caratteri salvata in un dispositivo di archiviazione permanente come un disco fisso.
directory:
Raccolta di file; è dotata di un nome ed è chiamata anche cartella.
percorso:
Stringa che localizza un file.
percorso relativo:
Un percorso che parte dalla cartella di lavoro attuale.
percorso assoluto:
Un percorso che parte dalla cartella principale del file system.
gestire:
Prevenire l’arresto di un programma causato da un errore, mediante le istruzioni try e except.
database:
Un file i cui contenuti sono organizzati come un dizionario, con chiavi che corrispondono a valori.
oggetto bytes:
Un oggetto simile ad una stringa.
shell:
Un programma che permette all’utente di inserire comandi e di eseguirli, avviando altri programmi.
oggetto pipe:
Un oggetto che rappresenta un programma in esecuzione e che consente ad un programma Python di eseguire comandi e leggere i risultati.

14.12  Esercizi

Esercizio 1  

Scrivete una funzione di nome sed che richieda come argomenti una stringa modello, una stringa di sostituzione, e due nomi di file. La funzione deve leggere il primo file e scriverne il contenuto nel secondo file (creandolo se necessario). Se la stringa modello compare da qualche parte nel testo del file, la funzione deve sostituirla con la seconda stringa.

Se si verifica un errore in apertura, lettura, scrittura, chiusura del file, il vostro programma deve gestire l’eccezione, stampare un messaggio di errore e terminare. Soluzione: http://thinkpython2.com/code/sed.py.


Esercizio 2  

Se avete scaricato la mia soluzione dell’Esercizio 2 dal sito http://thinkpython2.com/code/anagram_sets.py, avrete visto che crea un dizionario che fa corrispondere una stringa ordinata di lettere alla lista di parole che possono essere scritte con quelle lettere. Per esempio, 'opst' corrisponde alla lista ['opts', 'post', 'pots', 'spot', 'stop', 'tops'].

Scrivete un modulo che importi anagram_sets e fornisca due nuove funzioni: arch_anagrammi deve archiviare il dizionario di anagrammi in uno “shelf”; leggi_anagrammi deve cercare una parola e restituire una lista dei suoi anagrammi. Soluzione: http://thinkpython2.com/code/anagram_db.py


Esercizio 3  

In una grande raccolta di file MP3 possono esserci più copie della stessa canzone, messe in cartelle diverse o con nomi di file differenti. Scopo di questo esercizio è di ricercare i duplicati.

  1. Scrivete un programma che cerchi in una cartella e, ricorsivamente, nelle sue sottocartelle, e restituisca un elenco dei percorsi completi di tutti i file con una stessa estensione (come .mp3). Suggerimento: os.path contiene alcune funzioni utili per trattare nomi di file e percorsi.
  2. Per riconoscere i duplicati, potete usare md5sum per calcolare la “checksum” di ogni file. Se due file hanno la stessa checksum, significa che con ogni probabilità hanno lo stesso contenuto.
  3. Per effettuare un doppio controllo, usate il comando Unix diff.

Soluzione: http://thinkpython2.com/code/find_duplicates.py.


1
popen ora è deprecato, cioè siamo invitati a smettere di usarlo e ad iniziare ad usare invece il modulo subprocess. Ma per i casi semplici, trovo che subprocess sia più complicato del necessario. Pertanto continuerò ad usare popen finché non verrà rimosso definitivamente.

Buy this book at Amazon.com

Contribute

If you would like to make a contribution to support my books, you can use the button below. Thank you!
Pay what you want:

Are you using one of our books in a class?

We'd like to know about it. Please consider filling out this short survey.



Previous Up Next