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'))"

The "string" module contains support for a simple templating approach
based upon regular expressions, via "string.Template". This offers yet
another way to substitute values into strings, using placeholders like
"$x" and replacing them with values from a dictionary. This syntax is
easy to use, although it offers much less control for formatting.


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
