Fondamenti di Programmazione

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