# Lezione 17 - Lavorare con i file

# A livello puramente informatico e teorico, un file è una sequenza di byte
# memorizzata su un dispositivo di memorizzazione permanente, come un disco
# rigido. I file possono contenere qualsiasi tipo di informazione, come testo,
# immagini, audio, video, ecc.

# Essendo i file una sequenza di byte, possono essere di due tipi:
# - file di testo: contengono testo in formato ASCII o Unicode.
# - file binari: contengono informazioni non testuali, come immagini, audio,
#   video, ecc.

# I file sono fondamentali per la memorizzazione e il recupero di dati. Ad
# esempio, quando si scrive un programma, è possibile memorizzare i dati in un
# file per poterli recuperare in un secondo momento. Fino a questo momento,
# infatti, i dati che abbiamo utilizzato sono stati memorizzati solo in memoria
# RAM, che è volatile, cioè i dati vengono persi quando il computer viene
# spento o il programma termina. Attraverso i file, invece, è possibile
# memorizzare i dati in modo permanente.

# ---==== Aprire un file ====---

# Per aprire un file in Python, si utilizza la funzione open(). Questa funzione
# restituisce un oggetto file, che è un oggetto che rappresenta il file aperto.
# La sintassi della funzione open() è la seguente:

# open(nome_file, modalità)

# dove:
# - nome_file è il nome del file da aprire.
# - modalità è la modalità di apertura del file. Le modalità possibili sono:
#   - 'r': apre il file in modalità di sola lettura. Il file deve esistere.
#   - 'w': apre il file in modalità di sola scrittura. Se il file esiste, il
#          suo contenuto viene sovrascritto. Se il file non esiste, viene
#          creato.
#   - 'a': apre il file in modalità di scrittura in append. Se il file esiste,
#          il cursore viene posizionato alla fine del file. Se il file non
#          esiste, viene creato.
#   - 'r+': apre il file in modalità di lettura e scrittura. Il file deve
#           esistere.
#   - 'w+': apre il file in modalità di lettura e scrittura. Se il file esiste,
#           il suo contenuto viene sovrascritto. Se il file non esiste, viene
#           creato.
#   - 'a+': apre il file in modalità di lettura e scrittura in append. Se il
#           file esiste, il cursore viene posizionato alla fine del file. Se il
#           file non esiste, viene creato.
#   - 'x': apre il file in modalità di sola scrittura. Se il file esiste, viene
#          sollevata un'eccezione di tipo FileExistsError. Se il file non esiste,
#          viene creato.
#   - 'b': apre il file in modalità binaria. Questa modalità deve essere
#          utilizzata insieme a una delle modalità precedenti e serve per aprire
#          un file binario (ad esempio, un'immagine, un file audio, ecc.).
# Ce ne sono altre, ma queste sono le più comuni.

# ATTENZIONE: il nome_file deve essere una stringa e deve essere il percorso del
# file. Cosa si intende con percorso del file?
# Sostanzialmente, il nome di un file, dietro le quinte, è composto da due parti:
# - il nome vero e proprio del file.
# - il percorso del file, cioè la sequenza di cartelle che, a partire dalla cartella
#   radice del sistema operativo, porta al file.

# N.B.: la struttura del percorso cambia in base al sistema operativo. In generale:
# - su Windows, il percorso è composto da una sequenza di cartelle separate da
#   backslash (\).
# - su Unix e Unix-like (Linux, macOS), il percorso è composto da una sequenza di
#   cartelle separate da slash (/).

# N.B.2: generalmente, i file hanno un'estensione che indica il tipo di file. Ad
# esempio, i file di testo hanno estensione .txt, i file Python hanno estensione
# .py, i file immagine hanno estensione .jpg o .png, i file audio hanno estensione
# .mp3 o .wav, i file Excel hanno estensione .xls o .xlsx, i file Word hanno
# estensione .doc o .docx, i file PDF hanno estensione .pdf, i file PowerPoint
# hanno estensione .ppt o .pptx, i file ZIP hanno estensione .zip, ecc...
# Tuttavia, ci possono essere anche file senza estensione o con estensioni
# personalizzate. L'estensione di un file non è altro che una convenzione per
# indicare il tipo di file, ma non è obbligatoria. Infatti, è possibile creare un
# file di testo con estensione .jpg e un'immagine con estensione .txt, ma questo
# potrebbe creare confusione e non funzionare correttamente con alcuni programmi.

# Per quanto riguarda le estensioni personalizzate, è possibile, per esempio, creare
# un file PDF con estensione .pdf.old, se magari si vuole mantenere una
# vecchia versione di un file PDF. In questo caso, però, il file non sarà riconosciuto
# come un file PDF da un programma che gestisce i file PDF.

# Esempi di percorsi di file:
# Supponiamo di avere un file chiamato lista_spesa.txt, che si trova nella cartella
# Desktop del nostro utente (che chiameremo biero). I percorsi del file sono:
# - su Windows: C:\Users\biero\Desktop\lista_spesa.txt
# - su Unix e Unix-like: /home/biero/Desktop/lista_spesa.txt

# Perché? Perché:
# - su Windows, la cartella radice è C:\, mentre su Unix e Unix-like è /.
# - su Windows, le cartelle sono separate da backslash (\), mentre su Unix e
#   Unix-like sono separate da slash (/).
# - su Windows, il nome dell'utente è preceduto da Users\, mentre su Unix e
#   Unix-like è preceduto da home/.

# Quindi, considerando Windows, il file lista_spesa.txt si trova nella cartella
# Desktop, che a sua volta si trova nella cartella biero, che a sua volta si trova
# nella cartella Users, che a sua volta si trova nella cartella C:\. Quindi, il
# percorso del file è C:\Users\biero\Desktop\lista_spesa.txt.

# Quindi, ricapitolando, il nome del file, per Python, è il suo percorso.

# Inoltre, questo percorso, può anche essere relativo. Cosa si intende con percorso
# relativo? Sostanzialmente, un percorso relativo è un percorso costruito attraverso
# due cartelle speciali:
# - la cartella punto (.) rappresenta la cartella corrente.
# - la cartella punto-punto (..) rappresenta la cartella genitore.

# Ad esempio, supponiamo di avere un file chiamato programma.py, che si trova nella
# cartella pythonata, che a sua volta si trova nella cartella Documenti, che a sua
# volta si trova nella cartella biero, che a sua volta si trova nella cartella
# Utenti, che a sua volta si trova nella cartella C:\. Quindi, il percorso ASSOLUTO
# del file # è C:\Utenti\biero\Documenti\pythonata\programma.py.
# Ma supponiamo che il file programma.py voglia aprire un file chiamato dati.txt,
# che si trova nella stessa cartella di programma.py. In questo caso, il percorso
# RELATIVO del file dati.txt è .\dati.txt. Il punto (.) rappresenta la cartella
# corrente, mentre il backslash (\) separa le cartelle.
# Supponiamo, invece, che il file dati.txt si trovi nella cartella prova, che si
# trova nella cartella pythonata. Allora, il percorso RELATIVO del file dati.txt
# è ..\prova\dati.txt. Il punto-punto (..) rappresenta la cartella genitore.

# Esempi di apertura di un file:
f = open('C:\\Users\\biero\\Desktop\\lista_spesa.txt', 'r')
f = open('/home/biero/Desktop/lista_spesa.txt', 'r')
f = open('lista_spesa.txt', 'r') # Questo è un percorso relativo e apre il file
                                 # nella cartella corrente.

# N.B.: quando si apre un file, è importante chiuderlo quando non serve più. Per
# chiudere un file, si utilizza il metodo close() dell'oggetto file.

# Esempio:
f = open('lista_spesa.txt', 'r')
f.close()

# ---==== Leggere il contenuto di un file ====---

# Per leggere il contenuto di un file, si utilizza il metodo read() dell'oggetto
# file. Questo metodo restituisce una stringa contenente tutto il contenuto del
# file.

# Esempio:
f = open('lista_spesa.txt', 'r')
contenuto = f.read()
print(contenuto)
f.close()

# N.B.: il metodo read() legge tutto il contenuto del file. Se il file è molto
# grande, potrebbe occupare troppa memoria. In questo caso, è possibile leggere
# il file riga per riga, utilizzando il metodo readline() dell'oggetto file.

# Esempio:
f = open('lista_spesa.txt', 'r')
riga = f.readline()
while riga != '': # Quando il metodo readline() raggiunge la fine del file,
                  # restituisce una stringa vuota. Quindi, il ciclo while
                  # continua finché la riga non è vuota, ovvero finché non
                  # raggiunge la fine del file.
    print(riga)
    riga = f.readline()
f.close()

# ---==== Scrivere su un file ====---

# Per scrivere su un file, si utilizza il metodo write() dell'oggetto file. Questo
# metodo scrive una stringa nel file. Se il file è stato aperto in modalità di
# sola scrittura ('w'), il contenuto del file viene sovrascritto. Se il file è
# stato aperto in modalità di scrittura in append ('a'), il contenuto viene
# aggiunto alla fine del file.

# Esempio:
f = open('lista_spesa.txt', 'w')
f.write('Latte\n')
f.write('Pane\n')
f.write('Burro\n')
f.close()

# N.B.: il metodo write() scrive solo stringhe. Se si vuole scrivere un numero
# intero o un numero decimale, è necessario convertirlo in stringa utilizzando
# la funzione str(), ovvero effettuando il type casting.

# Esempio:
f = open('lista_spesa.txt', 'w')
f.write(str(1) + '\n')
f.write(str(2) + '\n')
f.write(str(3) + '\n')
f.close()

# ---==== Dettagli aggiuntivi ====---

# 1
# La cosa comoda dei file è che questi possono sostituire l'input messo da 
# terminale e l'output printato a terminale. Infatti, è possibile leggere da un
# file e scrivere su un file, senza dover utilizzare input() e print().

# 2
# Attenzione: se il file contiene caratteri speciali, come accenti, apostrofi,
# ecc., è necessario specificare l'encoding del file. L'encoding è la codifica
# dei caratteri, ovvero il modo in cui i caratteri sono rappresentati in byte.
# I due encoding più comuni sono:
# - ASCII: codifica a 7 bit che rappresenta 128 caratteri (senza accenti, ecc.).
# - UTF-8 (Unicode Transformation Format 8-bit): codifica a 8 bit che rappresenta
#   tutti i caratteri Unicode (compresi accenti, apostrofi, emoji, ecc.).

# Per specificare l'encoding di un file, si utilizza il parametro encoding della
# funzione open().

# Esempio:
f = open('lista_spesa.txt', 'r', encoding='utf-8')
contenuto = f.read()
print(contenuto)
f.close()

# N.B.: l'encoding di un file deve essere specificato sia in lettura che in
# scrittura. Se si apre un file in lettura con un certo encoding, è necessario
# aprire lo stesso file in scrittura con lo stesso encoding. Altrimenti, si
# potrebbero avere problemi di codifica.

# 3
# La funzione read() può prendere un argomento, che è il numero di caratteri (byte)
# da leggere. Questo può essere utile se si vuole leggere solo una parte del file.
# N.B.: anche l'andare a capo (\n) è considerato un carattere.

# 4
# Così come quando inserivamo gli input del nostro programma da terminale attraverso
# la funzione input(), anche la lettura dai file restituisce solo ed esclusivamente
# stringhe. Se si vuole leggere un numero intero o un numero decimale, è necessario
# convertire la stringa in intero o decimale. Per fare ciò, si usano le stesse funzioni
# viste precedentemente: int() e float().

# Esempio:
f = open('lista_spesa.txt', 'r')
numero = int(f.readline())
print(numero)
f.close()

# 5
# Dettaglio per evitare di avere problemi: è sempre una buona pratica non chiamare
# i file con nomi che possono creare problemi. Ad esempio, non chiamare un file
# con il nome di una funzione built-in di Python, come open, print, ecc.
# Oppure non chiamare un file con il nome di una variabile già definita nel programma.
# Oppure non chiamare un file con uno spazio nel nome. In generale, è sempre meglio
# usare l'underscore (_) al posto dello spazio.
# Per buona pratica è anche spesso consigliato usare solo lettere minuscole, salvo
# casi particolari (ad esempio per nomi di classi).

# 6
# Risolutore di percorsi per script lanciati su piattaforme diverse:
from pathlib import Path

file_relativo = Path('./lista_spesa.txt')

print(file_relativo.resolve())