7. Input e Output

Ci sono diversi modi per presentare l’output di un programma; i dati possono essere stampati in una forma leggibile dall’uomo o scritti in un file per un uso futuro. Questo capitolo discuterà alcune delle possibilità.

7.1. Formattazione Avanzata dell’Output

Finora abbiamo incontrato due modi di scrivere valori: espressioni e la funzione print(). (Un terzo modo è utilizzare il metodo write() degli oggetti file; il file di output standard può essere referenziato come sys.stdout. Vedi la Libreria di Riferimento per ulteriori informazioni a riguardo.)

Spesso desidererai avere più controllo sulla formattazione del tuo output rispetto al semplice stampare valori separati da spazi. Ci sono diversi modi per formattare l’output.

  • Per usare formatted string literals, inizia una stringa con f o F prima del segno di apertura della virgoletta o della tripla virgoletta. Dentro questa stringa, puoi scrivere un’espressione Python tra i caratteri { e }, che possono riferirsi a variabili o valori letterali.

    >>> year = 2016
    >>> event = 'Referendum'
    >>> f'Results of the {year} {event}'
    'Results of the 2016 Referendum'
    
  • Il metodo str.format() delle stringhe richiede più sforzo manuale. Utilizzerai comunque { e } per indicare dove una variabile sarà sostituita e potrai fornire direttive di formattazione dettagliate, ma dovrai anche fornire le informazioni da formattare. Nel blocco di codice seguente ci sono due esempi di come formattare variabili:

    >>> yes_votes = 42_572_654
    >>> total_votes = 85_705_149
    >>> percentage = yes_votes / total_votes
    >>> '{:-9} YES votes  {:2.2%}'.format(yes_votes, percentage)
    ' 42572654 YES votes  49.67%'
    

    Nota come i yes_votes sono riempiti con spazi e con un segno negativo solo per i numeri negativi. L’esempio stampa anche percentage moltiplicato per 100, con 2 cifre decimali e seguito da un segno di percentuale (vedi Format Specification Mini-Language per i dettagli).

  • Infine, puoi gestire tutte le stringhe da solo usando operazioni di slicing e concatenazione per creare qualsiasi layout tu possa immaginare. Il tipo stringa ha alcuni metodi che eseguono operazioni utili per riempire le stringhe a una determinata larghezza di colonna.

Quando non hai bisogno di un output sofisticato ma vuoi solo una rapida visualizzazione di alcune variabili per scopi di debug, puoi convertire qualsiasi valore in una stringa con le funzioni repr() o str().

La funzione str() è pensata per restituire rappresentazioni di valori che siano abbastanza leggibili, mentre repr() è pensata per generare rappresentazioni che possono essere lette dall’interprete (o genererà una SyntaxError se non esiste una sintassi equivalente). Per oggetti che non hanno una particolare rappresentazione per il consumo umano, str() restituirà lo stesso valore di repr(). Molti valori, come i numeri o strutture come liste e dizionari, hanno la stessa rappresentazione usando entrambe le funzioni. Le stringhe, in particolare, hanno due rappresentazioni distinte.

Alcuni esempi:

>>> s = 'Hello, world.'
>>> str(s)
'Hello, world.'
>>> repr(s)
"'Hello, world.'"
>>> str(1/7)
'0.14285714285714285'
>>> x = 10 * 3.25
>>> y = 200 * 200
>>> s = 'The value of x is ' + repr(x) + ', and y is ' + repr(y) + '...'
>>> print(s)
The value of x is 32.5, and y is 40000...
>>> # The repr() of a string adds string quotes and backslashes:
... hello = 'hello, world\n'
>>> hellos = repr(hello)
>>> print(hellos)
'hello, world\n'
>>> # The argument to repr() may be any Python object:
... repr((x, y, ('spam', 'eggs')))
"(32.5, 40000, ('spam', 'eggs'))"

Il modulo string contiene una classe Template che offre un altro modo per sostituire valori nelle stringhe, utilizzando segnaposti come $x e sostituendoli con valori da un dizionario, ma offre un controllo molto minore sulla formattazione.

7.1.1. Stringhe Formattate Letterali

Formatted string literals (anche dette f-strings per brevità) ti permettono di includere il valore di espressioni Python all’interno di una stringa, prefissando la stringa con f o F e scrivendo espressioni come {expression}.

Uno specificatore di formato opzionale può seguire l’espressione. Questo permette un maggiore controllo su come il valore viene formattato. Il seguente esempio arrotonda pi a tre cifre decimali:

>>> import math
>>> print(f'The value of pi is approximately {math.pi:.3f}.')
The value of pi is approximately 3.142.

Passare un intero dopo ':' farà sì che quel campo abbia una larghezza minima di un certo numero di caratteri. Questo è utile per allineare le colonne.

>>> table = {'Sjoerd': 4127, 'Jack': 4098, 'Dcab': 7678}
>>> for name, phone in table.items():
...     print(f'{name:10} ==> {phone:10d}')
...
Sjoerd     ==>       4127
Jack       ==>       4098
Dcab       ==>       7678

Possono essere usati altri modificatori per convertire il valore prima che venga formattato. '!a' applica ascii(), '!s' applica str(), e '!r' applica repr():

>>> animals = 'eels'
>>> print(f'My hovercraft is full of {animals}.')
My hovercraft is full of eels.
>>> print(f'My hovercraft is full of {animals!r}.')
My hovercraft is full of 'eels'.

Lo specificatore = può essere usato per espandere un’espressione al testo dell’espressione, un segno di uguale e poi la rappresentazione dell’espressione valutata:

>>> bugs = 'roaches'
>>> count = 13
>>> area = 'living room'
>>> print(f'Debugging {bugs=} {count=} {area=}')
Debugging bugs='roaches' count=13 area='living room'

Vedi self-documenting expressions per maggiori informazioni sullo specificatore =. Per un riferimento su queste specifiche di formato, consulta la guida di riferimento per Format Specification Mini-Language.

7.1.2. Il Metodo String format()

L’uso di base del metodo str.format() sembra così:

>>> print('We are the {} who say "{}!"'.format('knights', 'Ni'))
We are the knights who say "Ni!"

Le parentesi e i caratteri al loro interno (chiamati campi di formato) sono sostituiti dagli oggetti passati nel metodo str.format(). Un numero nelle parentesi può essere usato per riferirsi alla posizione dell’oggetto passato nel metodo str.format().

>>> print('{0} and {1}'.format('spam', 'eggs'))
spam and eggs
>>> print('{1} and {0}'.format('spam', 'eggs'))
eggs and spam

Se vengono utilizzati argomenti keyword nel metodo str.format(), i loro valori vengono referenziati usando il nome dell’argomento.

>>> print('This {food} is {adjective}.'.format(
...       food='spam', adjective='absolutely horrible'))
This spam is absolutely horrible.

Gli argomenti posizionali e keyword possono essere combinati arbitrariamente:

>>> print('The story of {0}, {1}, and {other}.'.format('Bill', 'Manfred',
...                                                    other='Georg'))
The story of Bill, Manfred, and Georg.

Se hai una stringa di formato molto lunga che non vuoi dividere, sarebbe utile poter riferire le variabili da formattare per nome invece che per posizione. Questo può essere fatto semplicemente passando il dizionario e usando le parentesi quadre '[]' per accedere alle chiavi.

>>> table = {'Sjoerd': 4127, 'Jack': 4098, 'Dcab': 8637678}
>>> print('Jack: {0[Jack]:d}; Sjoerd: {0[Sjoerd]:d}; '
...       'Dcab: {0[Dcab]:d}'.format(table))
Jack: 4098; Sjoerd: 4127; Dcab: 8637678

Questo può essere fatto anche passando il dizionario table come argomenti keyword usando la notazione **.

>>> table = {'Sjoerd': 4127, 'Jack': 4098, 'Dcab': 8637678}
>>> print('Jack: {Jack:d}; Sjoerd: {Sjoerd:d}; Dcab: {Dcab:d}'.format(**table))
Jack: 4098; Sjoerd: 4127; Dcab: 8637678

Questo è particolarmente utile in combinazione con la funzione built-in vars(), che restituisce un dizionario contenente tutte le variabili locali:

>>> table = {k: str(v) for k, v in vars().items()}
>>> message = " ".join([f'{k}: ' + '{' + k +'};' for k in table.keys()])
>>> print(message.format(**table))
__name__: __main__; __doc__: None; __package__: None; __loader__: ...

Ad esempio, le seguenti righe producono un insieme di colonne ordinatamente allineate che danno interi e loro quadrati e cubi:

>>> for x in range(1, 11):
...     print('{0:2d} {1:3d} {2:4d}'.format(x, x*x, x*x*x))
...
 1   1    1
 2   4    8
 3   9   27
 4  16   64
 5  25  125
 6  36  216
 7  49  343
 8  64  512
 9  81  729
10 100 1000

Per una panoramica completa della formattazione delle stringhe con str.format(), vedi Format String Syntax.

7.1.3. Formattazione Manuale delle Stringhe

Ecco la stessa tabella di quadrati e cubi, formattata manualmente:

>>> for x in range(1, 11):
...     print(repr(x).rjust(2), repr(x*x).rjust(3), end=' ')
...     # Note use of 'end' on previous line
...     print(repr(x*x*x).rjust(4))
...
 1   1    1
 2   4    8
 3   9   27
 4  16   64
 5  25  125
 6  36  216
 7  49  343
 8  64  512
 9  81  729
10 100 1000

(Nota che l’unico spazio tra ogni colonna è stato aggiunto dal modo in cui funziona print(): aggiunge sempre spazi tra i suoi argomenti.)

Il metodo str.rjust() degli oggetti stringa giustifica a destra una stringa in un campo di una data larghezza riempiendola con spazi sulla sinistra. Esistono metodi simili str.ljust() e str.center(). Questi metodi non scrivono nulla, restituiranno solo una nuova stringa. Se la stringa di input è troppo lunga, non viene troncata, ma restituita invariata; questo comprometterà l’allineamento delle colonne ma di solito è meglio dell’alternativa, che sarebbe mentire su un valore. (Se desideri davvero la troncatura, puoi sempre aggiungere un’operazione di slicing, come in x.ljust(n)[:n].)

C’è un altro metodo, str.zfill(), che riempie una stringa numerica a sinistra con zeri. Comprende i segni più e meno:

>>> '12'.zfill(5)
'00012'
>>> '-3.14'.zfill(7)
'-003.14'
>>> '3.14159265359'.zfill(5)
'3.14159265359'

7.1.4. Vecchia formattazione delle stringhe

The % operator (modulo) can also be used for string formatting. Given format % values (where format is a string), % conversion specifications in format are replaced with zero or more elements of values. This operation is commonly known as string interpolation. For example:

>>> import math
>>> print('The value of pi is approximately %5.3f.' % math.pi)
The value of pi is approximately 3.142.

Maggiori informazioni possono essere trovate nella sezione Formattazione delle stringhe in stile printf.

7.2. Lettura e Scrittura di File

open() restituisce un file object, ed è più comunemente usato con due argomenti posizionali e uno keyword: open(filename, mode, encoding=None)

>>> f = open('workfile', 'w', encoding="utf-8")

Il primo argomento è una stringa che contiene il nome del file. Il secondo argomento è un’altra stringa che contiene alcuni caratteri che descrivono il modo in cui il file verrà utilizzato. mode può essere 'r' quando il file verrà solo letto, 'w' per solo scrivere (un file esistente con lo stesso nome verrà cancellato), e 'a' apre il file per l’aggiunta dati; qualsiasi dato scritto nel file viene automaticamente aggiunto alla fine. 'r+' apre il file per sia lettura che scrittura. L’argomento mode è opzionale; 'r' sarà assunto se omesso.

Normalmente, i file vengono aperti in text mode, ciò significa che leggi e scrivi stringhe dal e nel file, che sono codificate in una specifica encoding. Se encoding non è specificato, il valore predefinito dipende dal sistema (vedi open()). Poiché UTF-8 è lo standard de facto moderno, encoding="utf-8" è raccomandato a meno che non si sappia che è necessario utilizzare una diversa codifica. Aggiungendo una 'b' al mode, si apre il file in binary mode. I dati in modalità binaria sono letti e scritti come oggetti bytes. Non puoi specificare encoding quando apri file in modalità binaria.

In modalità testo, il valore predefinito durante la lettura è convertire i terminatori di riga specifici della piattaforma (\n su Unix, \r\n su Windows) in solo \n. Quando si scrive in modalità testo, il valore predefinito è convertire le occorrenze di \n nei terminatori specifici della piattaforma. Questa modifica dietro le quinte ai dati del file va bene per i file di testo, ma corromperà i dati binari come quelli nei file JPEG o EXE. Fai molta attenzione a usare modalità binaria quando leggi e scrivi tali file.

È buona pratica usare la keyword with quando si lavora con oggetti file. Il vantaggio è che il file viene chiuso correttamente dopo che la sua suite finisce, anche se viene generata un’eccezione a un certo punto. Usare with è anche molto più breve che scrivere blocchi equivalenti try-finally:

>>> with open('workfile', encoding="utf-8") as f:
...     read_data = f.read()

>>> # We can check that the file has been automatically closed.
>>> f.closed
True

Se non stai usando la keyword with, allora dovresti chiamare f.close() per chiudere il file e liberare immediatamente qualsiasi risorsa di sistema utilizzata da esso.

Avvertimento

Chiamare f.write() senza usare la keyword with o chiamare f.close() potrebbe risultare nel fatto che gli argomenti di f.write() non vengano completamente scritti sul disco, anche se il programma termina con successo.

Dopo che un oggetto file è stato chiuso, sia tramite un’istruzione with, sia chiamando f.close(), i tentativi di utilizzare l’oggetto file falliranno automaticamente.

>>> f.close()
>>> f.read()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: I/O operation on closed file.

7.2.1. Metodi degli Oggetti File

Gli altri esempi in questa sezione assumeranno che un oggetto file chiamato f sia già stato creato.

Per leggere i contenuti di un file, chiama f.read(size), che legge una certa quantità di dati e li restituisce come una stringa (in modalità testo) o un oggetto bytes (in modalità binaria). size è un argomento numerico opzionale. Quando size è omesso o negativo, verranno letti e restituiti tutti i contenuti del file; è un tuo problema se il file è due volte più grande della memoria del tuo computer. Altrimenti, al massimo size caratteri (in modalità testo) o size byte (in modalità binaria) vengono letti e restituiti. Se la fine del file è stata raggiunta, f.read() restituirà una stringa vuota ('').

>>> f.read()
'This is the entire file.\n'
>>> f.read()
''

f.readline() legge una singola riga dal file; un carattere di newline (\n) viene lasciato alla fine della stringa e viene omesso solo sull’ultima riga del file se il file non termina con un newline. Questo rende il valore di ritorno non ambiguo; se f.readline() restituisce una stringa vuota, la fine del file è stata raggiunta, mentre una linea vuota è rappresentata da '\n', una stringa contenente solo un newline.

>>> f.readline()
'This is the first line of the file.\n'
>>> f.readline()
'Second line of the file\n'
>>> f.readline()
''

Per leggere le righe da un file, puoi iterare sull’oggetto file. Questo è efficiente in termini di memoria, veloce e porta a un codice semplice:

>>> for line in f:
...     print(line, end='')
...
This is the first line of the file.
Second line of the file

Se vuoi leggere tutte le righe di un file in una lista puoi anche usare list(f) o f.readlines().

f.write(string) scrive i contenuti di string nel file, restituendo il numero di caratteri scritti.

>>> f.write('This is a test\n')
15

Altri tipi di oggetti devono essere convertiti – o in una stringa (in modalità testo) o in un oggetto bytes (in modalità binaria) – prima di scriverli:

>>> value = ('the answer', 42)
>>> s = str(value)  # convert the tuple to string
>>> f.write(s)
18

f.tell() restituisce un intero che indica la posizione corrente dell’oggetto file nel file, rappresentata come numero di byte dall’inizio del file quando in modalità binaria e un numero opaco quando in modalità testo.

Per cambiare la posizione dell’oggetto file, usa f.seek(offset, whence). La posizione è calcolata sommando offset a un punto di riferimento; il punto di riferimento è selezionato dall’argomento whence. Un valore whence di 0 misura dall’inizio del file, 1 usa la posizione corrente del file, e 2 usa la fine del file come punto di riferimento. whence può essere omesso e predefinito a 0, utilizzando l’inizio del file come punto di riferimento.

>>> f = open('workfile', 'rb+')
>>> f.write(b'0123456789abcdef')
16
>>> f.seek(5)      # Go to the 6th byte in the file
5
>>> f.read(1)
b'5'
>>> f.seek(-3, 2)  # Go to the 3rd byte before the end
13
>>> f.read(1)
b'd'

Nei file di testo (quelli aperti senza una b nella stringa di modalità), sono consentiti solo i seek relativi all’inizio del file (con l’eccezione del seek alla fine del file con seek(0, 2)) e gli unici valori offset validi sono quelli restituiti da f.tell(), o zero. Qualsiasi altro valore offset produce un comportamento non definito.

Gli oggetti file hanno alcuni metodi aggiuntivi, come isatty() e truncate() che sono meno frequentemente usati; consulta la Libreria di Riferimento per una guida completa agli oggetti file.

7.2.2. Salvare dati strutturati con json

Le stringhe possono essere facilmente scritte su e lette da un file. I numeri richiedono un po” più di sforzo, poiché il metodo read() restituisce solo stringhe, che dovranno essere passate a una funzione come int(), che prende una stringa come '123' e restituisce il suo valore numerico 123. Quando vuoi salvare tipi di dati più complessi come liste annidate e dizionari, il parsing e la serializzazione manuale diventano complicati.

Piuttosto che avere utenti che scrivono e fanno debug costantemente di codice per salvare tipi di dati complicati nei file, Python ti permette di usare il popolare formato di interscambio dati chiamato JSON (JavaScript Object Notation). Il modulo standard chiamato json può prendere gerarchie di dati Python e convertirle in rappresentazioni di stringhe; questo processo è chiamato serializzazione. Ricostruire i dati dalla rappresentazione di stringa è chiamato deserializzazione. Tra la serializzazione e la deserializzazione, la stringa che rappresenta l’oggetto può essere stata memorizzata in un file o dato, o inviata su una connessione di rete a una macchina lontana.

Nota

Il formato JSON è comunemente usato dalle applicazioni moderne per permettere lo scambio di dati. Molti programmatori sono già familiari con esso, il che lo rende una buona scelta per l’interoperabilità.

Se hai un oggetto x, puoi visualizzare la sua rappresentazione JSON della stringa con una semplice riga di codice:

>>> import json
>>> x = [1, 'simple', 'list']
>>> json.dumps(x)
'[1, "simple", "list"]'

Un’altra variante della funzione dumps(), chiamata dump(), semplicemente serializza l’oggetto in un oggetto text file. Quindi se f è un oggetto text file aperto per la scrittura, possiamo fare questo:

json.dump(x, f)

Per decodificare l’oggetto di nuovo, se f è un oggetto binary file o text file che è stato aperto per la lettura:

x = json.load(f)

Nota

I file JSON devono essere codificati in UTF-8. Usa encoding="utf-8" quando apri il file JSON come text file sia per la lettura che per la scrittura.

Questa semplice tecnica di serializzazione può gestire liste e dizionari, ma serializzare istanze di classi arbitrari in JSON richiede un po” di sforzo extra. Il riferimento per il modulo json contiene una spiegazione di questo.

Vedi anche

pickle - il modulo pickle

Contrariamente a JSON, pickle è un protocollo che permette la serializzazione di oggetti Python arbitrariamente complessi. Come tale, è specifico per Python e non può essere usato