Adiós os.path: 15 trucos de Pathlib para dominar rápidamente el sistema de archivos en Python

Un amigo robot. — Vía medio viaje

Pathlib puede ser mi biblioteca favorita (después de Sklearn, obviamente). Y dado que hay más de 130 mil bibliotecas, eso es decir algo. Pathlib me ayuda a convertir código como este escrito en os.path:

import os

dir_path = "/home/user/documents"

# Find all text files inside a directory
files = [os.path.join(dir_path, f) for f in os.listdir(dir_path)
if os.path.isfile(os.path.join(dir_path, f)) and f.endswith(".txt")]

dentro de esto:

from pathlib import Path

# Find all text files inside a directory
files = list(dir_path.glob("*.txt"))

Pathlib salió en Python 3.4 como reemplazo de la pesadilla que era os.path. También marcó un hito importante para el lenguaje Python en general: finalmente convirtieron cada cosa en un objeto (incluso nada).

El mayor inconveniente de os.path trataba las rutas del sistema como cadenas, lo que generaba un código desordenado e ilegible y una curva de aprendizaje pronunciada.

Al representar las rutas como completas objetosPathlib resuelve todos estos problemas e introduce elegancia, consistencia y una bocanada de aire fresco en el manejo de caminos.

Y este artículo mío, largamente esperado, describirá algunas de las mejores funciones/características y trucos de pathlib para realizar tareas que hubieran sido experiencias verdaderamente horribles en os.path.

Aprender estas funciones de Pathlib hará que todo lo relacionado con rutas y archivos sea más fácil para usted como profesional de datos, especialmente durante los flujos de trabajo de procesamiento de datos en los que tiene que mover miles de imágenes, CSV o archivos de audio.

¡Empecemos!

Trabajando con caminos

1. Creando caminos

Casi todas las características de pathlib es accesible a través de su Path class, que puede usar para crear rutas a archivos y directorios.

Hay algunas maneras de crear caminos con Path. Primero, hay métodos de clase como cwd y home para los directorios de usuarios domésticos y de trabajo actuales:

from pathlib import Path

Path.cwd()

PosixPath('/home/bexgboost/articles/2023/4_april/1_pathlib')
Path.home()
PosixPath('/home/bexgboost')

También puede crear rutas a partir de rutas de cadena:

p = Path("documents")

p

PosixPath('documents')

Unir rutas es muy fácil en Pathlib con el operador de barra inclinada:

data_dir = Path(".") / "data"
csv_file = data_dir / "file.csv"

print(data_dir)
print(csv_file)

data
data/file.csv

Por favor, no dejes que nadie te atrape usando os.path.join después de este.

Para verificar si una ruta, puede usar la función booleana exists:

data_dir.exists()
True
csv_file.exists()
True

A veces, el objeto Path completo no será visible y deberá verificar si se trata de un directorio o un archivo. Entonces, puedes usar is_dir o is_file funciones para hacerlo:

data_dir.is_dir()
True
csv_file.is_file()
True

La mayoría de las rutas con las que trabaje serán relativas a su directorio actual. Sin embargo, hay casos en los que debe proporcionar la ubicación exacta de un archivo o un directorio para que sea accesible desde cualquier secuencia de comandos de Python. Esto es cuando usas absolute caminos:

csv_file.absolute()
PosixPath('/home/bexgboost/articles/2023/4_april/1_pathlib/data/file.csv')

Por último, si tiene la desgracia de trabajar con bibliotecas que aún requieren rutas de cadenas, puede llamar str(path):

str(Path.home())
'/home/bexgboost'

La mayoría de las bibliotecas en la pila de datos han soportado durante mucho tiempo Path objetos, incluyendo sklearn, pandas, matplotlib, seabornetc.

2. Atributos de ruta

Path los objetos tienen muchos atributos útiles. Veamos algunos ejemplos usando este objeto de ruta que apunta a un archivo de imagen.

image_file = Path("images/midjourney.png").absolute()

image_file

PosixPath('/home/bexgboost/articles/2023/4_april/1_pathlib/images/midjourney.png')

Comencemos con el parent. Devuelve un objeto de ruta que está un nivel por encima del directorio de trabajo actual.

image_file.parent
PosixPath('/home/bexgboost/articles/2023/4_april/1_pathlib/images')

A veces, es posible que solo desee el archivo name en lugar de todo el camino. Hay un atributo para eso:

image_file.name
'midjourney.png'

que devuelve solo el nombre del archivo con la extensión.

También hay stem para el nombre del archivo sin el sufijo:

image_file.stem
'midjourney'

O el suffix mismo con el punto para la extensión del archivo:

image_file.suffix
'.png'

Si desea dividir una ruta en sus componentes, puede utilizar parts en lugar de str.split('/'):

image_file.parts
('/',
'home',
'bexgboost',
'articles',
'2023',
'4_april',
'1_pathlib',
'images',
'midjourney.png')

Si desea que esos componentes sean Path objetos en sí mismos, puede utilizar parents atributo, que crea un generador:

for i in image_file.parents:
print(i)
/home/bexgboost/articles/2023/4_april/1_pathlib/images
/home/bexgboost/articles/2023/4_april/1_pathlib
/home/bexgboost/articles/2023/4_april
/home/bexgboost/articles/2023
/home/bexgboost/articles
/home/bexgboost
/home
/

Trabajar con archivos

bexgboost_classified_files._8k._sharp_quality._ed73fcdc-67e6-4b3c-ace4-3092b268cc42.png
archivos clasificados. — A mitad de camino

Para crear archivos y escribir en ellos, no tiene que usar open función más. Solo crea un Path objeto y write_text o write_btyes a ellos:

markdown = data_dir / "file.md"

# Create (override) and write text
markdown.write_text("# This is a test markdown")

O, si ya tiene un archivo, puede read_text o read_bytes:

markdown.read_text()
'# This is a test markdown'
len(image_file.read_bytes())
1962148

Sin embargo, tenga en cuenta que write_text o write_bytes anula el contenido existente de un archivo.

# Write new text to existing file
markdown.write_text("## This is a new line")
# The file is overridden
markdown.read_text()
'## This is a new line'

Para agregar nueva información a archivos existentes, debe usar open método de Path objetos en a (añadir) modo:

# Append text
with markdown.open(mode="a") as file:
file.write("n### This is the second line")

markdown.read_text()

'## This is a new linen### This is the second line'

También es común cambiar el nombre de los archivos. rename El método acepta la ruta de destino para el archivo renombrado.

Para crear la ruta de destino en el directorio actual, es decir, cambiar el nombre del archivo, puede utilizar with_stem en el camino existente, que sustituye al stem del archivo original:

renamed_md = markdown.with_stem("new_markdown")

markdown.rename(renamed_md)

PosixPath('data/new_markdown.md')

Arriba, file.md se convierte en new_markdown.md.

Veamos el tamaño del archivo a través de stat().st_size:

# Display file size
renamed_md.stat().st_size
49 # in bytes

o la última vez que se modificó el archivo, que fue hace unos segundos:

from datetime import datetime

modified_timestamp = renamed_md.stat().st_mtime

datetime.fromtimestamp(modified_timestamp)

datetime.datetime(2023, 4, 3, 13, 32, 45, 542693)

st_mtime devuelve una marca de tiempo, que es el conteo de segundos desde el 1 de enero de 1970. Para que sea legible, puede usar el fromtimestamp funcion de datatime.

Para eliminar archivos no deseados, puede unlink a ellos:

renamed_md.unlink(missing_ok=True)

Configuración missing_ok a True no generará ninguna alarma si el archivo no existe.

Trabajando con directorios

imagen.png
Carpetas en una oficina. — A mitad de camino

Hay algunos buenos trucos para trabajar con directorios en Pathlib. Primero, veamos cómo crear directorios recursivamente.

new_dir = (
Path.cwd()
/ "new_dir"
/ "child_dir"
/ "grandchild_dir"
)

new_dir.exists()

False

El new_dir no existe, así que vamos a crearlo con todos sus hijos:

new_dir.mkdir(parents=True, exist_ok=True)

Por defecto, mkdir crea el último hijo de la ruta dada. Si los padres intermedios no existen, debe configurar parents a True.

Para eliminar directorios vacíos, puede usar rmdir. Si el objeto de ruta dado está anidado, solo se elimina el último directorio secundario:

# Removes the last child directory
new_dir.rmdir()

Para enumerar el contenido de un directorio como ls en la terminal, puede usar iterdir. Una vez más, el resultado será un objeto generador, que arrojará el contenido del directorio como objetos de ruta separados, uno a la vez:

for p in Path.home().iterdir():
print(p)
/home/bexgboost/.python_history
/home/bexgboost/word_counter.py
/home/bexgboost/.azure
/home/bexgboost/.npm
/home/bexgboost/.nv
/home/bexgboost/.julia
...

Para capturar todos los archivos con una extensión específica o un patrón de nombre, puede usar el glob función con una expresión regular.

Por ejemplo, a continuación, encontraremos todos los archivos de texto dentro de mi directorio de inicio con glob("*.txt"):

home = Path.home()
text_files = list(home.glob("*.txt"))

len(text_files)

3 # Only three

Para buscar archivos de texto de forma recursiva, es decir, también dentro de todos los directorios secundarios, puede utilizar globo recursivo con rglob:

all_text_files = [p for p in home.rglob("*.txt")]

len(all_text_files)

5116 # Now much more

Obtenga más información sobre las expresiones regulares aquí.

También puedes usar rglob('*') para enumerar los contenidos del directorio de forma recursiva. Es como la versión sobrealimentada de iterdir().

Uno de los casos de uso de esto es contar la cantidad de formatos de archivo que aparecen dentro de un directorio.

Para ello, importamos el Counter clase de collections y proporcione todos los sufijos de archivo dentro de la carpeta de artículos de home:

from collections import Counter

file_counts = Counter(
path.suffix for path in (home / "articles").rglob("*")
)

file_counts

Counter({'.py': 12,
'': 1293,
'.md': 1,
'.txt': 7,
'.ipynb': 222,
'.png': 90,
'.mp4': 39})

Diferencias del sistema operativo

Lo siento, pero tenemos que hablar sobre esta pesadilla de un problema.

Hasta ahora, hemos estado tratando con PosixPath objetos, que son los predeterminados para sistemas tipo UNIX:

type(Path.home())
pathlib.PosixPath

Si estuviera en Windows, obtendría un WindowsPath objeto:

from pathlib import WindowsPath

# User raw strings that start with r to write windows paths
path = WindowsPath(r"C:users")
path

NotImplementedError: cannot instantiate 'WindowsPath' on your system

Crear una instancia de la ruta de otro sistema genera un error como el anterior.

Pero, ¿qué sucede si se ve obligado a trabajar con rutas de otro sistema, como el código escrito por compañeros de trabajo que usan Windows?

Como solución, pathlib ofrece objetos de ruta pura como PureWindowsPath o PurePosixPath:

from pathlib import PurePosixPath, PureWindowsPath

path = PureWindowsPath(r"C:users")
path

PureWindowsPath('C:/users')

Estos son objetos de ruta primitivos. Tiene acceso a algunos métodos y atributos de ruta, pero esencialmente, el objeto de ruta sigue siendo una cadena:

path / "bexgboost"
PureWindowsPath('C:/users/bexgboost')
path.parent
PureWindowsPath('C:/')
path.stem
'users'
path.rename(r"C:losers") # Unsupported
AttributeError: 'PureWindowsPath' object has no attribute 'rename'

Conclusión

Si te has dado cuenta, mentí en el título del artículo. En lugar de 15, creo que el recuento de nuevos trucos y funciones fue de 30.

No quería asustarte.

Pero espero haberte convencido lo suficiente como para deshacerte os.path y empezar a usar pathlib para operaciones de ruta mucho más fáciles y legibles.

Forjar un nuevo caminoSi tu quieres 🙂

bexgboost_Paths_and_pathlib._Extreme_quality._76f2bbe4-7c8d-45a6-abf4-ccc8d9e32144.png
Camino. — A mitad de camino

Si disfrutaste este artículo y, seamos sinceros, su estilo de escritura extraño, considera apoyarme registrándote para convertirte en miembro de Medium. La membresía cuesta 4,99 $ al mes y te da acceso ilimitado a todas mis historias y cientos de miles de artículos escritos por gente más experimentada. Si te registras a través de este enlace, ganaré una pequeña comisión sin costo extra para tu bolsillo.

[post_relacionado id=»1731″]


Comentarios

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *