Alberi e Ricorsione
In questa lezione vedremo come esplorare in modo gerarchico le cartelle sul computer. Per fare questo in modo efficiente, costruiremo una struttura ad albero in memoria che rappresenta l'organizzazione dei file. Per usare questa struttura introdurremo funzioni ricorsive che permettono di navigare la struttura per fare operazioni sulla stessa. Per rendere gli esempi piu' uniformi, distribuiamo un pacchetto.
File System
Il file system, cioe' la collezione di file file su disco, e' organizzato in una struttura ad albero. Le cartelle raggruppano file ed altre cartelle, le quali ultime possono raggruppare altri file e cartelle. Da ora avanti useremo il nome directory per indicare le cartelle. L'albero che esploreremo in questo esempio sara' il seguente.
Esplorare il File System con la Ricorsione
Possiamo scrivere una funzione semplice che stampa il contenuto di una directory. In python, il modulo os
contiene funzioni tipiche di quelle fornite dai "sistemi operative", come manipolazione di file e processi. La funzione os.listdir(name)
ritorna una sequenza di nomi di file o directory contenute nella directory name
. Ad esempio, usando la direcotry nel pacchetto, la funzione
import os
# print the files and directory in a specific parent directory
def print_dir(path):
for name in os.listdir(path):
print path+'/'+name
print_dir('dirBase')
stampa il contenuto della directory dirBase
che risulta
dirBase/dirA
dirBase/dirB
Questa funzioni pero' non stampa il conenuto di dirA
o di dirB
. Quello che vorremmo e' la possibilita' di stampare il contenuto di tutto l'albero di file sotto dirBase
. Per fare questo, possiamo semplicemente chiamare ancora la funzione print_dir
con il nome della direttori dirA
e dirB
. Dalla lezione precedente sappiamo che i nomi sono anch'essi composti e devono comprendere il nome di base, cioe' dirBase/dirA
etc. Infine, vogliamo chiamare la funzione solo per le directory e non i files. Possiamo controllare la cosa con os.path.isdir(name)
.
# print the files and directory in a specific tree
def print_tree(path):
for name in os.listdir(path):
fullname = path+'/'+name
print fullname
if os.path.isdir(fullname):
print_tree(fullname)
Con print_tree('dirBase')
otteniamo
dirBase/dirA
dirBase/dirA/dirC
dirBase/dirA/dirC/fileD.txt
dirBase/dirA/dirC/fileE.txt
dirBase/dirA/dirD
dirBase/dirA/dirD/dirF
dirBase/dirA/dirD/dirF/fileK.txt
dirBase/dirA/dirD/fileF.txt
dirBase/dirA/dirD/fileG.txt
dirBase/dirA/dirD/fileH.txt
dirBase/dirA/dirE
dirBase/dirA/dirE/fileI.txt
dirBase/dirA/dirE/fileJ.txt
dirBase/dirA/fileA.txt
dirBase/dirB
dirBase/dirB/fileB.txt
dirBase/dirB/fileC.txt
Nell'esempio precedente abbiamo illustrato un'idea fondamentale della programmazione detta ricorsione. L'idea e' che una funzione puo' chiamare se stessa con argomenti diversi. Per poter vsualizzare cosa succede dobbiamo salvare i dati sul file system in una struttura che ci permette di lavorare senza accedere completamente ai files.
Alberi di Dizionari
Vorremmo adesso introdurre una struttura dati che permette di memorizzare i nomi di file e directory e la loro struttura. Potremmo ad esempio memorizzare la lista generata in precedenza e poi ottenere nomi locali usando split('/)
. Ma questo e' un trucco non utilissimo perche' usare la struttura diventa molto laborioso. Una struttura piu' adatta per rappresentare questo tipo di dati e' una struttura ad albero. Gil alberi sono strutture in cui i singoli elementi, detti nodi, memorizzano il loro contenuto che e' una numero di variabili arbitrarie che includono altri nodi. Questo e' simile alle directory che hanno directory incluse in se stesse. Per farlo possiamo usare un dizionario.
# get the files and directory in a specific tree
def get_tree_dict(path):
d = {}
d['name'] = path.split('/')[-1]
d['path'] = path
d['content'] = []
d['isdir'] = os.path.isdir(path)
if not d['isdir']: return d
for name in os.listdir(path):
fullname = path+'/'+name
d['content'] += [ get_tree_dict(fullname) ]
return d
Con get_tree_dict('dirBase')
otteniamo
{'content': [{'content': [{'content': [{'content': [], 'path': 'dirBase/dirA/dirC/fileD.txt', 'name': 'fileD.txt', 'isdir': False}, {'content': [], 'path': 'dirBase/dirA/dirC/fileE.txt', 'name': 'fileE.txt', 'isdir': False}], 'path': 'dirBase/dirA/dirC', 'name': 'dirC', 'isdir': True}, {'content': [{'content': [{'content': [], 'path': 'dirBase/dirA/dirD/dirF/fileK.txt', 'name': 'fileK.txt', 'isdir': False}], 'path': 'dirBase/dirA/dirD/dirF', 'name': 'dirF', 'isdir': True}, {'content': [], 'path': 'dirBase/dirA/dirD/fileF.txt', 'name': 'fileF.txt', 'isdir': False}, {'content': [], 'path': 'dirBase/dirA/dirD/fileG.txt', 'name': 'fileG.txt', 'isdir': False}, {'content': [], 'path': 'dirBase/dirA/dirD/fileH.txt', 'name': 'fileH.txt', 'isdir': False}], 'path': 'dirBase/dirA/dirD', 'name': 'dirD', 'isdir': True}, {'content': [{'content': [], 'path': 'dirBase/dirA/dirE/fileI.txt', 'name': 'fileI.txt', 'isdir': False}, {'content': [], 'path': 'dirBase/dirA/dirE/fileJ.txt', 'name': 'fileJ.txt', 'isdir': False}], 'path': 'dirBase/dirA/dirE', 'name': 'dirE', 'isdir': True}, {'content': [], 'path': 'dirBase/dirA/fileA.txt', 'name': 'fileA.txt', 'isdir': False}], 'path': 'dirBase/dirA', 'name': 'dirA', 'isdir': True}, {'content': [{'content': [], 'path': 'dirBase/dirB/fileB.txt', 'name': 'fileB.txt', 'isdir': False}, {'content': [], 'path': 'dirBase/dirB/fileC.txt', 'name': 'fileC.txt', 'isdir': False}], 'path': 'dirBase/dirB', 'name': 'dirB', 'isdir': True}], 'path': 'dirBase', 'name': 'dirBase', 'isdir': True}
Questo e' proprio poco leggibile. Scriviamo quindi una funzione che stampa il contenuto di questo dizionario in modo piu' sensato. Per stamparlo in modo piu' leggibile, aggiungeremo un parametro che indica di quanto dobbiamo indentare il testo. Mentre scnediamo nell'albero incrementeremo questo parametro.
# print the files and directory in a specific tree
def print_tree_dict(d, indent=''):
print indent+d['name']
for f in d['content']: print_tree_dict(f,indent+' ')
Con output
dirBase
dirA
dirC
fileD.txt
fileE.txt
dirD
dirF
fileK.txt
fileF.txt
fileG.txt
fileH.txt
dirE
fileI.txt
fileJ.txt
fileA.txt
dirB
fileB.txt
fileC.txt
Alberi di Oggetti
Come abbiamo visto nelle lezioni precedenti, e' utile usare le classi per avere una sintassi piu' facile da leggere quando di creano strutture di dizionari ripetuti. Ecco l'equivalente programma scritto in precedenza.
# dir object
class DirNode(object):
def __init__(self,name,path,isdir,content):
self.name = name
self.path = path
self.isdir = isdir
self.content = content
def print_tree(self,indent=''):
print indent+self.name
for n in self.content: n.print_tree(indent+' ')
def find_by_name(self,name):
ret = []
if self.name == name: ret += [self]
for n in self.content: ret += n.find_by_name(name)
return ret
# get directory tree as object
def get_tree_node(path):
d = DirNode(path.split('/')[-1],path,os.path.isdir(path),[])
if not d.isdir: return d
for name in os.listdir(path):
fullname = path+'/'+name
d.content += [ get_tree_node(fullname) ]
return d
Nel codice precedente abbiamo incluso anche un metodo per la ricerca di un valore in un albero dato il nome, come ulteriore esempio. Seguono due esempi di utilizzo.
>>> d = get_tree_node('dirBase')
>>> d.print_tree()
dirBase
dirA
dirC
fileD.txt
fileE.txt
dirD
dirF
fileK.txt
fileF.txt
fileG.txt
fileH.txt
dirE
fileI.txt
fileJ.txt
fileA.txt
dirB
fileB.txt
fileC.txt
>>> found = d.find_by_name('fileI.txt')
>>> for n in found: print n.path
dirBase/dirA/dirE/fileI.txt