Joyitas en la stdlib: pathlib

Por Kiko Correoso

El otro día estuvimos hablando de la biblioteca collections, una joya dentro de la librería estándar. Hoy vamos a hablar de una nueva biblioteca que se incluyó en la versión 3.4 de CPython llamada pathlib.

  <p>
    <strong>Solo python 3, actualízate!!!</strong>
  </p>

  <p>
    Esta biblioteca nos da la posibilidad de usar clases para trabajar con las rutas del sistema de ficheros con una serie de métodos muy interesantes.
  </p>

  <h2 id="Algunas-utilidades-para-configurar-el-problema">
    Algunas utilidades para configurar el problema<a class="anchor-link" href="#Algunas-utilidades-para-configurar-el-problema"></a>
  </h2>

  <p>
    Vamos a crear un par de funciones que nos permiten crear y borrar un directorio de pruebas para poder reproducir el ejemplo de forma sencilla:
  </p>
</div>

import os
import glob
import shutil
from random import randint, choice, seed
from string import ascii_letters

# función que nos crea un directorio de prueba en
# el mismo directorio del notebook
def crea_directorio():
    seed(1)
    base = os.path.join(os.path.curdir,
                        'pybonacci_probando_pathlib')
    os.makedirs(base, exist_ok = True)

    for i in range(, randint(3, 5)):
        folder = ''.join([choice(ascii_letters) for _ in range(4)])
        path = os.path.join(base, folder)
        os.makedirs(path, exist_ok = True)
        for j in range(, randint(2, 5)):
            ext = choice(['.txt', '.py', '.html'])
            name = ''.join([choice(ascii_letters) for _ in range(randint(5, 10))])
            filename = name + ext
            path2 = os.path.join(path, filename)
            open(path2, 'w').close()

# Función que nos permite hacer limpieza            
def borra_directorio():
    base = os.path.join(os.path.curdir,
                        'pybonacci_probando_pathlib')
    shutil.rmtree(base + os.path.sep)

Si ahora ejecutamos la función crea_directorio:

crea_directorio()

Nos debería quedar una estructura parecida a lo siguiente:

  <pre><code>pybonacci_probando_pathlib/

├── KZWe │ ├── CrUZoLgubb.txt │ ├── IayRnBUbHo.txt │ ├── WCEPyYng.txt │ └── yBMWX.py ├── WCFJ │ ├── GBGQmtsLFG.html │ ├── PglOUshVv.py │ └── RoWDsb.py └── zLcE ├── AQlxJSXR.html ├── fCQGgXk.html └── xFUbEctT.html

  <h2 id="Ejemplo-usando-lo-disponible-hasta-hace-poco">
    Ejemplo usando lo disponible hasta hace poco<a class="anchor-link" href="#Ejemplo-usando-lo-disponible-hasta-hace-poco"></a>
  </h2>

  <p>
    Pensemos en un problema que consiste en identificar todos los ficheros <em>.py</em> disponibles en determinada ruta y dejarlos en una nueva carpeta, que llamaremos <em>python</em>, todos juntos eliminándolos de la carpeta original en la que se encuentren.
  </p>

  <p>
    De la forma antigua esto podría ser así:
  </p>
</div>

# Suponemos que ya has creado los directorios y ficheros
# de prueba usando crea_directorio()

# recolectamos todos los ficheros *.py con sus rutas
base = os.path.join(os.path.curdir,
                    'pybonacci_probando_pathlib')
ficheros_py = glob.glob(os.path.join(base, '**', '*.py'))

# creamos la carpeta 'python' 
# dentro de 'pybonacci_probando_pathlib'
os.makedirs(os.path.join(base, 'python'), exist_ok = True)

# y movemos los ficheros a la nueva carpeta 'python'
for f in ficheros_py:
    fich = f.split(os.path.sep)[-1]
    shutil.move(f, os.path.join(base, 'python'))

Nuestra nueva estructura de ficheros debería ser la siguiente:

  <pre><code>pybonacci_probando_pathlib/

├── KZWe │ ├── CrUZoLgubb.txt │ ├── IayRnBUbHo.txt │ └── WCEPyYng.txt ├── python │ ├── PglOUshVv.py │ ├── RoWDsb.py │ └── yBMWX.py ├── WCFJ │ └── GBGQmtsLFG.html └── zLcE ├── AQlxJSXR.html ├── fCQGgXk.html └── xFUbEctT.html

  <p>
    En el anterior ejemplo hemos tenido que usar las bibliotecas <code>glob</code>, <code>os</code> y <code>shutil</code> para poder realizar una operación relativamente sencilla. Esto no es del todo deseable porque he de conocer tres librerías diferentes y mi cabeza no da para tanto.
  </p>

  <h2 id="Limpieza">
    Limpieza<a class="anchor-link" href="#Limpieza"></a>
  </h2>

  <p>
    Me cargo la carpeta <em>pybonacci_probando_pathlib</em> para hacer un poco de limpieza:
  </p>
</div>

borra_directorio()

Y vuelvo a crear la estructura de ficheros inicial:

crea_directorio()

Después de la limpieza vamos a afrontar el problema usando pathlib.

  <h2 id="El-mismo-ejemplo-con-pathlib">
    El mismo ejemplo con <code>pathlib</code><a class="anchor-link" href="#El-mismo-ejemplo-con-pathlib"></a>
  </h2>

  <p>
    Primero importamos la librería y, como bonus, creamos una función que hace lo mismo que la función <code>borra_directorio</code> pero usando <code>pathlib</code>, que llamaremos <code>borra_directorio_pathlib</code>:
  </p>
</div>

from pathlib import Path

def borra_directorio_pathlib(path = None):
    if path is None:
        p = Path('.', 'pybonacci_probando_pathlib')
    else:
        p = path
    for i in p.iterdir():
        if i.is_dir():
            borra_directorio_pathlib(i)
        else:
            i.unlink()
    p.rmdir()

La anterior función con shutil es un poco más sencilla que con pathlib. Esto es lo único que hecho de menos en pathlib, algunas utilidades de shutil que vendrían muy bien de serie. Algo negativo tenía que tener.

  <p>
    En la anterior función, <code>borra_directorio_pathlib</code>, podemos ver ya algunas cositas de <code>pathlib</code>.
  </p>

  <p>
    <code>p = Path('.', 'pybonacci_probando_pathlib')</code> nos crea una ruta que ahora es un objeto en lugar de una cadena. Dentro del bucle usamos el método <a href="https://docs.python.org/3/library/pathlib.html#pathlib.Path.iterdir"><code>iterdir</code></a> que nos permite iterar sobre los directorios de la ruta definida en el objeto <code>p</code>. el iterador nos devuelve nuevos objetos que disponen de métodos como <a href="https://docs.python.org/3/library/pathlib.html#pathlib.Path.is_dir"><code>is_dir</code></a>, que nos permite saber si una ruta se refiere a un directorio, o <a href="https://docs.python.org/3/library/pathlib.html#pathlib.Path.unlink"><code>unlink</code></a>, que nos permite eliminar el fichero o enlace. Por último, una vez que no tenemos ficheros dentro del directorio definido en <code>p</code> podemos usar el método <a href="https://docs.python.org/3/library/pathlib.html#pathlib.Path.rmdir"><code>rmdir</code></a> para eliminar la carpeta.
  </p>

  <p>
    Ahora veamos cómo realizar lo mismo que antes usando <code>pathlib</code>, es decir, mover los ficheros <em>.py</em> a la carpeta <em>python</em> que hemos de crear.
  </p>
</div>

# recolectamos todos los ficheros *.py con sus rutas
p = Path('.', 'pybonacci_probando_pathlib')
ficheros_py = p.glob('**/*.py')

# creamos la carpeta 'python' dentro de 'pybonacci_probando_pathlib'
(p / 'python').mkdir(mode = 0o777, exist_ok = True)

# y copiamos los ficheros a la nueva carpeta 'python'
for f in ficheros_py:
    target = p / 'python' / f.name
    f.rename(target)

Nuevamente, nuestra estructura de ficheros debería ser la misma que antes:

  <pre><code>pybonacci_probando_pathlib/

├── KZWe │ ├── CrUZoLgubb.txt │ ├── IayRnBUbHo.txt │ └── WCEPyYng.txt ├── python │ ├── PglOUshVv.py │ ├── RoWDsb.py │ └── yBMWX.py ├── WCFJ │ └── GBGQmtsLFG.html └── zLcE ├── AQlxJSXR.html ├── fCQGgXk.html └── xFUbEctT.html

  <p>
    Repasemos el código anterior:<br /> Hemos creado un objeto ruta <code>p</code> tal como habíamos visto antes en la función <code>borra_directorio_pathlib</code>. Este objeto ahora dispone de un método <a href="https://docs.python.org/3/library/pathlib.html#pathlib.Path.glob"><code>glob</code></a> que nos devuelve un iterador con lo que le pidamos, en este caso, todos los ficheros con extensión <em>.py</em>. En la línea <code>(p / 'python').mkdir(mode = 0o777, exist_ok = True)</code> podemos ver el uso de <code>/</code> como operador para instancias de <code>Path</code>. El primer paréntesis nos devuelve una nueva instancia de <code>Path</code> que dispone del método <a href="https://docs.python.org/3/library/pathlib.html#pathlib.Path.mkdir"><code>mkdir</code></a> que hace lo que todos esperáis. Como <code>ficheros_py</code> era un iterador podemos usarlo en el bucle obteniendo nuevas instancias de <code>Path</code> con las rutas de los ficheros python que queremos mover. en la línea donde se define <code>target</code> hacemos uso del atributo <a href="https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.name"><code>name</code></a>,que nos devuelve la última parte de la ruta. Por último, el fichero con extensión <em>.py</em> definido en el <code>Path</code> <code>f</code> lo renombramos a una nueva ruta, definida en <code>target</code>.
  </p>

  <p>
    Y todo esto usando una única librería!!!
  </p>

  <p>
    Echadle un ojo a la <a href="https://docs.python.org/3/library/pathlib.html">documentación oficial</a> para descubrir otras cositas interesantes.
  </p>

  <p>
    Si además de usar una única librería usamos parte de la funcionalidad de <code>shutil</code> tenemos una pareja muy potente, <code>pathlib</code> + <code>shutil</code>.
  </p>
</div>

Limpieza II

  <p>
    Y para terminar, limpiamos nuestra estructura de ficheros pero usando ahora la función <code>borra_directorio_pathlib</code> que habíamos creado pero no usado aún:
  </p>
</div>

borra_directorio_pathlib()

Notas

Ya hay un nuevo PEP relacionado y aceptado.

Enjoy!!

Comentarios