pathlib - Percorsi di Filesystem come Oggetti

Scopo: Elabora, costruisce, verifica oppure lavora con nomi di file e percorsi usando una API orientata agli oggetti invece che operazioni di stringa a basso livello.

Rappresentazione di Percorsi

Il modulo pathlib include classi per gestire percorsi di filesystem formattati usando sia la sintassi POSIX standard che quella Microsoft Windows. Comprende le cosiddette classi "pure", che operano sulle stringhe ma non interagiscono con il filesystem reale, e quelle "concrete", le quali estendono l'API per includere operazioni che riflettono o modificano dati sul filesystem locale.

Le classi pure PurePosixPath e PureWindowsPath possono essere istanziate e usate su ogni sistema operativo, visto che funzionano solo con i nomi. Per istanziare la classe corretta per lavorare con un filesystem reale, si usi PosixPath o WindowsPath a seconda della piattaforma.

Costruire Percorsi

Per istanziare un nuovo percorso, si passa una stringa come primo argomento. La rappresentazione stringa dell'oggetto percorso è quel valore. Per creare un nuovo percorso che faccia riferimento a un valore relativo a un percorso esistente, si usi l'operatore / per estendere il percorso. L'argomento per l'operatore può essere sia una stringa che un altro oggetto percorso.

# pathlib_operator.py

import pathlib

usr = pathlib.PurePosixPath('/usr')
print(usr)

usr_local = usr / 'local'
print(usr_local)

usr_share = usr / pathlib.PurePosixPath('share')
print(usr_share)

root = usr / '..'
print(root)

etc = root / '/etc/'
print(etc)

Come mostra il valore di root nel risultato dell'esempio, l'operatore combina i valori di percorso così come sono passati, e non normalizza il risultato quando contiene il riferimento alla directory genitore "..". Tuttavia, se un segmento inizia con il separatore di percorso, viene interpretato come un nuovo riferimento a "root" allo stesso modo della funzione os.path.join(). Separatori di percorso supplementari sono rimessi dal mezzo del valore del percorso, come in etc in questo esempio.

$ python3 pathlib_operator.py

/usr
/usr/local
/usr/share
/usr/..
/etc

Le classi di percorso concrete includono il metodo resolve() per normalizzare un percorso cercando nel filesystem directory e collegamenti simbolici per produrre il percorso assoluto riferito da un nome.

# pathlib_resolve.py

import pathlib

usr_local = pathlib.Path('/usr/local')
share = usr_local / '..' / 'share'
print(share.resolve())

Qui il percorso relativo è convertito in un percorso assoluto in /usr/share. Se il percorso in input comprende collegamenti simbolici, sono anch'essi espansi per consentire al percorso di far direttamente riferimento alla destinazione.

$ python3 pathlib_resolve.py

/usr/share

Per costruire percorsi quando non si conoscono in anticipo i segmenti, si usi joinpath(), passando ogni segmento del percorso come argomento separato.

# pathlib_joinpath.py

import pathlib

root = pathlib.PurePosixPath('/')
subdirs = ['usr', 'local']
usr_local = root.joinpath(*subdirs)
print(usr_local)

Come con l'operatore / la chiamata di joinpath() crea una nuova istanza.

$ python3 pathlib_joinpath.py

/usr/local

Dato un oggetto percorso esistente, è facile costruirne uno nuovo con piccole differenze tipo il fare riferimento a un file diverso nella stessa directory. Si usi with_name() per creare un nuovo percorso che sostituisce la porzione del nome con un nome di file differente. Si usi with_suffix() per creare un nuovo percorso che sostituisca l'estensione del nome del file con un diverso valore.

# pathlib_from_existing.py

import pathlib

ind = pathlib.PurePosixPath('source/pathlib/index.rst')
print(ind)

py = ind.with_name('pathlib_from_existing.py')
print(py)

pyc = py.with_suffix('.pyc')
print(pyc)

Entrambi i metodi ritornano nuovi oggetti, e gli originali rimangono invariati.

$ python3 pathlib_from_existing.py

source/pathlib/index.rst
source/pathlib/pathlib_from_existing.py
source/pathlib/pathlib_from_existing.pyc

Analizzare percorsi

Gli oggetti di percorso hanno metodi e proprietà per estrarre valori parziali dal nome. Ad esempio la proprietà parts produce una sequenza di segmenti di percorso analizzati in base al valore del separatore di percorso.

# pathlib_parts.py

import pathlib

p = pathlib.PurePosixPath('/usr/local')
print(p.parts)

La sequenza è una tupla, che riflette l'immutabilità dell'istanza di percorso.

$ python3 pathlib_parts.py

('/', 'usr', 'local')

Ci sono due modi per risalire la gerarchia del filesystem dato un oggetto percorso. La proprietà parent fa riferimento da una nuova istanza di percorso per la directory che contiene il percorso, il valore ritornato da os.path.dirname(). La proprietà parents è un iterabile che produce riferimenti alla directory genitore, risalendo continuamente la gerarchia del percorso fino a raggiungere la radice.

# pathlib_parents.py

import pathlib

p = pathlib.PurePosixPath('/usr/local/lib')

print('genitore: {}'.format(p.parent))

print('\ngerarchia:')
for up in p.parents:
    print(up)

L'esempio itera sulla proprietà parents e stampa i valori dei membri.

$ python3 pathlib_parents.py

genitore: /usr/local

gerarchia:
/usr/local
/usr
/

Altre parti del percorso possono essere raggiunte tramite le proprietà dell'oggetto di percorso. La proprietà name mantiene la parte finale del percorso, dopo l'ultimo separatore di percorso (lo stesso valore prodotto da os.path.basename()). La proprietà suffix mantiene il valore che segue il separatore di estensione e la proprietà stem mantiene la porzione del nome prima del suffisso.

# pathlib_name.py

import pathlib

p = pathlib.PurePosixPath('./source/pathlib/pathlib_name.py')
print('percorso   : {}'.format(p))
print('nome       : {}'.format(p.name))
print('suffisso   : {}'.format(p.suffix))
print('radice nome: {}'.format(p.stem))

Sebbene i valori di suffix e stem siano simili a quelli prodotti da os.path.splitext(), i valori sono basati solo sul valore di name e non del percorso completo.

$ python3 pathlib_name.py

percorso   : source/pathlib/pathlib_name.py
nome       : pathlib_name.py
suffisso   : .py
radice nome: pathlib_name

Creare Percorsi Concreti

Le istanze della classe concreta Path possono essere create da argomenti stringa in riferimento al nome (o nome potenziale) di un file, directory o collegamento simbolico sul filesystem. La classe fornisce anche parecchi metodi di convenienza per costruire istanze utilizzando locazioni comunemente usate che cambiano, tipo la directory di lavoro corrente e la directory home dell'utente.

# pathlib_convenience.py

import pathlib

home = pathlib.Path.home()
print('home         : ', home)

cwd = pathlib.Path.cwd()
print('dir. corrente: ', cwd)

Entrambi i metodi creano istanze di Path prepopolate con un riferimento assoluto al filesystem.

$ python3 pathlib_convenience.py
home         :  /home/robby
dir. corrente:  ...

Contenuto delle Directory

Ci sono tre metodi per accedere al contenuto di una directory per trovare i nomi di file disponibili sul filesystem. iterdir() è un generatore, che trattiene una nuova istanza di Path per ciascun elemento nella directory interessata.

# pathlib_iterdir.py

import pathlib

p = pathlib.Path('.')

for f in p.iterdir():
    print(f)

Se Path non fa riferimento a una directory, iterdir() solleva una eccezione NotADirectoryError.

$ python3 pathlib_iterdir.py

pathlib_operator.py
pathlib_parents.py
pathlib_iterdir.py
pathlib_resolve.py
pathlib_parts.py
pathlib_name.py
pathlib_from_existing.py
pathlib_convenience.py

Si usi glob() per trovare solo i file che corrispondono a un modello.

# pathlib_glob.py

import pathlib

p = pathlib.Path('..')

for f in p.glob('*.rst'):
    print(f)

Questo esempio mostra tutti i file di tipo reStructuredText nella directory genitore rispetto allo script.

$ python3 pathlib_glob.py

../about.rst
../algorithm_tools.rst
../book.rst
../compression.rst
../concurrency.rst
../cryptographic.rst
../data_structures.rst
../dates.rst
../dev_tools.rst
../email.rst
../file_access.rst
../frameworks.rst
../i18n.rst
../importing.rst
../index.rst
../internet_protocols.rst
../language.rst
../networking.rst
../numeric.rst
../persistence.rst
../porting_notes.rst
../runtime_services.rst
../text.rst
../third_party.rst
../unix.rst

Il processore di glob supporta la scansione ricorsiva usando il modello con prefisso ** oppure chiamando rglob() in luogo di glob().

# pathlib_rglob.py

import pathlib

p = pathlib.Path('..')

for f in p.rglob('pathlib_*.py'):
    print(f)

Visto che questo esempio viene eseguito dalla directory genitore, è richiesta una ricerca ricorsiva per trovare i file di esempio che corrispondono al modello pathlib_*.py.

$ python3 dumpscripts/pathlib_rglob.py

../pymotw-it3.0/dumpscripts/pathlib_operator.py
../pymotw-it3.0/dumpscripts/pathlib_parents.py
../pymotw-it3.0/dumpscripts/pathlib_iterdir.py
../pymotw-it3.0/dumpscripts/pathlib_resolve.py
../pymotw-it3.0/dumpscripts/pathlib_parts.py
../pymotw-it3.0/dumpscripts/pathlib_name.py
../pymotw-it3.0/dumpscripts/pathlib_rglob.py
../pymotw-it3.0/dumpscripts/pathlib_from_existing.py
../pymotw-it3.0/dumpscripts/pathlib_convenience.py
../pymotw-it3.0/dumpscripts/pathlib_glob.py

Leggere e Scrivere File

Ogni istanza di Path include metodi per lavorare con il contenuto del file al quale fa riferimento. Per ottenere immediatamente il contenuto, si usi read_bytes() oppure read_text(). Per scrivere sul file si usi write_bytes() oppure write_text(). Si usi il metodo open() per aprire il file e trattenerne l'handle. invece che passare il nome alla funzione built-in open().

# pathlib_read_write.py

import pathlib

f = pathlib.Path('esempio.txt')

f.write_bytes('Questo è il contenuto'.encode('utf-8'))

with f.open('r', encoding='utf-8') as handle:
    print('lettura da open(): {!r}'.format(handle.read()))

print('read_text(): {!r}'.format(f.read_text('utf-8')))

I metodi di convenienza eseguono qualche controllo di tipo prima aprire il file e scriverci sopra, ma per il resto equivale a eseguire l'operazione direttamente.

$ python3 pathlib_read_write.py

lettura da open(): 'Questo è il contenuto'
read_text(): 'Questo è il contenuto'

Manipolare Directory e Collegamenti Simbolici

I percorsi che rappresentano directory o collegamenti simbolici che non esistono possono essere usati per creare i relativi elementi nel filesystem.

# pathlib_mkdir.py

import pathlib

p = pathlib.Path('dir_esempio')

print('Creazione di {}'.format(p))
p.mkdir()

Se il percorso esiste già, mkdir() solleva un FileExistsError.

$ python3 pathlib_mkdir.py

Creazione di dir_esempio

$ python3 pathlib_mkdir.py

Creazione di dir_esempio
Traceback (most recent call last):
  File "pathlib_mkdir.py", line 8, in <module>
    p.mkdir()
  File "/usr/lib/python3.5/pathlib.py", line 1214, in mkdir
    self._accessor.mkdir(self, mode)
  File "/usr/lib/python3.5/pathlib.py", line 371, in wrapped
    return strfunc(str(pathobj), *args)
FileExistsError: [Errno 17] File exists: 'dir_esempio'

Si usi symlink_to() per creare un collegamento simbolico. Questo collegamento assumerà un nome in base al valore del percorso e farà riferimento al nome dato come argomento a symlink_to().

# pathlib_symlink_to.py

import pathlib

p = pathlib.Path('coll_di_esempio')

p.symlink_to('pathlib_symlink_to.py')

print(p)
print(p.resolve().name)

Questo esempio crea un collegamento simbolico, quindi usa resolve() per leggere il collegamento per trovare a cosa punta e ne stampa il nome.

$ python3 pathlib_symlink_to.py

coll_di_esempio
pathlib_symlink_to.py

Tipi di File

Una istanza di Path include parecchi metodi per verificare il tipo di un file a cui fa riferimento. Questo esempio crea parecchi file di diversi tipi e li verifica assieme ad altri file specifici per il dispositivo disponibili nel sistema operativo locale.

# pathlib_types.py

import itertools
import os
import pathlib

root = pathlib.Path('file_di_prova')

# Pulisce quanto elaborato nelle precedenti esecuzioni
if root.exists():
    for f in root.iterdir():
        f.unlink()
else:
    root.mkdir()

# Crea i file di prova
(root / 'file').write_text(
    'Questo è un file normale', encoding='utf-8')
(root / 'symlink').symlink_to('file')
os.mkfifo(str(root / 'fifo'))

# Verifica il tipo di file
to_scan = itertools.chain(
    root.iterdir(),
    [pathlib.Path('/dev/disk0'),
     pathlib.Path('/dev/console')],
)
hfmt = '{:18s}' + ('  {:>5}' * 6)
print(hfmt.format('Nome', 'File', 'Dir', 'Link', 'FIFO', 'Blocco',
                  'Carattere'))
print()

fmt = '{:20s}  ' + ('{!r:>5}  ' * 6)
for f in to_scan:
    print(fmt.format(
        str(f),
        f.is_file(),
        f.is_dir(),
        f.is_symlink(),
        f.is_fifo(),
        f.is_block_device(),
        f.is_char_device(),
    ))

Ognuno dei metodi is_dir(), is_file(), is_symlink(), is_socket(), is_fifo(), is_block_device() e is_char_device() non richiede argomenti.

$ python3 pathlib_types.py

Nome                 File    Dir   Link   FIFO  Blocco  Carattere

file_di_prova/fifo    False  False  False   True  False  False
file_di_prova/symlink   True  False   True  False  False  False
file_di_prova/file     True  False  False  False  False  False
/dev/disk0            False  False  False  False  False  False
/dev/console          False  False  False  False  False   True

Proprietà dei File

Informazioni dettagliate per un file possono essere recuperate usando i metodi stat() oppure lstat() (per verificare lo stato di qualcosa che possa assomigliare a un collegamento simbolico). Questi metodi producono gli stessi risultati di os.stat() ed os.lstat().

# pathlib_stat.py

import pathlib
import sys
import time

if len(sys.argv) == 1:
    filename = __file__
else:
    filename = sys.argv[1]

p = pathlib.Path(filename)
stat_info = p.stat()

print('{}:'.format(filename))
print('  Dimnsione      :', stat_info.st_size)
print('  Permessi       :', oct(stat_info.st_mode))
print('  Proprietario   :', stat_info.st_uid)
print('  Dispositivo    :', stat_info.st_dev)
print('  Creato         :', time.ctime(stat_info.st_ctime))
print('  Ultima modifica:', time.ctime(stat_info.st_mtime))
print('  Ultimo accesso :', time.ctime(stat_info.st_atime))

L'output potrà variare in base a dove il codice di esempio viene eseguito. Si provi a passare nomi di file diversi da riga di comando a pathlib_stat.py.

$ python3 pathlib_stat.py

pathlib_stat.py:
  Dimnsione      : 589
  Permessi       : 0o100644
  Proprietario   : 1000
  Dispositivo    : 2053
  Creato         : Sat Aug 19 16:50:42 2017
  Ultima modifica: Sat Aug 19 16:50:42 2017
  Ultimo accesso : Sat Aug 19 16:50:42 2017

$ python3 pathlib_stat.py ../operator.xml
../operator.xml:
  Dimnsione      : 3711
  Permessi       : 0o100664
  Proprietario   : 1000
  Dispositivo    : 2053
  Creato         : Wed Nov  2 22:43:09 2016
  Ultima modifica: Thu Sep 29 18:48:13 2016
  Ultimo accesso : Wed Aug 16 20:44:53 2017

Per un accesso semplificato a informazioni circa il proprietario di un file si usi owner() e group().

# pathlib_ownership.py

import pathlib

p = pathlib.Path(__file__)

print('Il proprietario di {} è {}/{}'.format(p, p.owner(), p.group()))

Mentre stat() ritorna valori di ID di sistema numerici, questi metodi cercano il nome associato all'identificativo.

$ python3 pathlib_ownership.py

Il proprietario di pathlib_ownership.py è robby/robby

Il metodo touch() funziona come il comando Unix touch per creare un file o aggiornare la data di modifica del file e i permessi.

# pathlib_touch.py

import pathlib
import time

p = pathlib.Path('toccato')
if p.exists():
    print('esiste già')
else:
    print('creato nuovo')

p.touch()
start = p.stat()

time.sleep(1)

p.touch()
end = p.stat()

print('Inizio:', time.ctime(start.st_mtime))
print('Fine  :', time.ctime(end.st_mtime))

L'esecuzione di più di una volta di questo esempio aggiornerà il file esistente dopo la prima esecuzione.

$ python3 pathlib_touch.py

creato nuovo
Inizio: Sat Aug 19 16:59:08 2017
Fine  : Sat Aug 19 16:59:09 2017

$ python3 pathlib_touch.py

esiste già
Inizio: Sat Aug 19 16:59:43 2017
Fine  : Sat Aug 19 16:59:44 2017

Permessi

Sui sistemi tipo Unix, i permessi dei file possono essere cambiati usando chmod(), passando il modo come intero. I valori possono essere costruiti usando le costanti definite nel modulo stat. Questo esempio agisce sul bit del permesso di esecuzione per l'utente alternandone il valore.

# pathlib_chmod.py

import os
import pathlib
import stat

# Crea un nuovo file di testo
f = pathlib.Path('pathlib_chmod_esempio.txt')
if f.exists():
    f.unlink()
f.write_text('contenuto')

# Determa che permessi sono già impostati usando stat.
existing_permissions = stat.S_IMODE(f.stat().st_mode)
print('Prima: {:o}'.format(existing_permissions))

# Decide in che modo alternarli.
if not (existing_permissions & os.X_OK):
    print('Aggiunto permesso di esecuzione')
    new_permissions = existing_permissions | stat.S_IXUSR
else:
    print('Rimosso permesso di esecuzione')
    # usa xor per rimuovere il permesso di esecuzione utente
    new_permissions = existing_permissions ^ stat.S_IXUSR

# Esegue la modifica e mostra il nuovo valore.
f.chmod(new_permissions)
after_permissions = stat.S_IMODE(f.stat().st_mode)
print('Dopo: {:o}'.format(after_permissions))

Questo script assume che sia abbiano i privilegi necessari per modificare il modo del file quando viene eseguito.

$ python3 pathlib_chmod.py

Prima: 644
Aggiunto permesso di esecuzione
Dopo: 744

Eliminare

Ci sono due metodi per rimuovere elementi dal filesystem, in base al tipo. Per eliminare una directory vuota si usi rmdir().

# pathlib_rmdir.py

import pathlib

p = pathlib.Path('dir_esempio')

print('Rimozione di {}'.format(p))
p.rmdir()

Viene sollevata una eccezione FileNotFoundError se le condizioni di arrivo sono già presenti e la directory non esiste. Si avrà un errore anche se si tenta di rimuovere una directory non vuota.

$ python3 pathlib_rmdir.py

Rimozione di dir_esempio

$ python3 pathlib_rmdir.py
Rimozione di dir_esempio
Traceback (most recent call last):
  File "pathlib_rmdir.py", line 8, in ≶module>
    p.rmdir()
  File "/usr/lib/python3.5/pathlib.py", line 1262, in rmdir
    self._accessor.rmdir(self)
  File "/usr/lib/python3.5/pathlib.py", line 371, in wrapped
    return strfunc(str(pathobj), *args)
FileNotFoundError: [Errno 2] No such file or directory: 'dir_esempio'

Per file, collegamenti simbolici e la maggior parte degli altri tipi di percorso si usi unlink().

# pathlib_unlink.py

import pathlib

p = pathlib.Path('toccato')

p.touch()

print('esiste prima della rimozione:', p.exists())

p.unlink()

print('esiste dopo la rimozione:', p.exists())

L'utente deve avere i privilegi per rimuovere il file, il collegamento simbolico, il socket o altro oggetto di filesystem.

$ python3 pathlib_unlink.py

esiste prima della rimozione: True
esiste dopo la rimozione: False

Vedere anche:

pathlib
La documentazione della libreria standard per questo modulo
os.path
Manipolazione di nomi di file indipendentemente dalla piattaforma
Gestire i Permessi del File System
Discussione su os.stat() ed os.lstat()
glob
Verifica di corrispondenza su modello per nomi di file in Unix
PEP 428
Il modulo pathlib.