Por qué deberías usar pathlib

Del traductor: ¡Hola, Habr! Te presento la traducción del artículo Por qué deberías usar pathlib y su continuación. No, realmente, pathlib es genial . Ahora se está prestando mucha atención a nuevas características de Python como asyncio, el operador = = y la escritura opcional. Al mismo tiempo, el riesgo de un radar no es tan significativo (aunque :: llame a una innovación seria que un idioma no resulta ser una innovación seria), sino innovaciones muy útiles en un idioma. En particular, en un habr de artículos dedicados a un tema, no encontré (a excepción de un párrafo aquí ), por lo tanto, decidí corregir la situación.


Cuando descubrí el entonces nuevo módulo pathlib hace unos años, decidí desde el fondo de mi mente que era solo una versión un poco incómoda orientada a objetos del módulo os.path . Estaba equivocado pathlib es realmente maravilloso !


En este artículo intentaré enamorarme de pathlib . Espero que este artículo te pathlib a usar pathlib en cualquier situación relacionada con el trabajo con archivos en Python .



Parte 1


os.path incómodo


El módulo os.path siempre os.path sido lo que usamos cuando se trataba de rutas de Python. En principio, hay todo lo que necesita, pero a menudo no se ve muy elegante.


¿Debo importarlo así?


 import os.path BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) TEMPLATES_DIR = os.path.join(BASE_DIR, 'templates') 

O asi?


 from os.path import abspath, dirname, join BASE_DIR = dirname(dirname(abspath(__file__))) TEMPLATES_DIR = join(BASE_DIR, 'templates') 

Tal vez la función de join tiene un nombre demasiado general, y deberíamos hacer algo como esto:


 from os.path import abspath, dirname, join as joinpath BASE_DIR = dirname(dirname(abspath(__file__))) TEMPLATES_DIR = joinpath(BASE_DIR, 'templates') 

Para mí, todas las opciones anteriores no parecen demasiado convenientes. Pasamos cadenas a funciones que devuelven cadenas que pasamos a las siguientes funciones que funcionan con cadenas. Dio la casualidad de que todos contienen caminos, pero siguen siendo solo líneas.


El uso de cadenas para entrada y salida en funciones os.path muy inconveniente porque tiene que leer el código de adentro hacia afuera. Me gustaría convertir estas llamadas de anidadas a secuenciales. ¡Esto es lo que pathlib te permite hacer!


 from pathlib import Path BASE_DIR = Path(__file__).resolve().parent.parent TEMPLATES_DIR = BASE_DIR.joinpath('templates') 

El módulo os.path requiere llamadas a funciones anidadas, pero pathlib nos permite crear cadenas de llamadas consecutivas a métodos y atributos de la clase Path con un resultado equivalente.


Sé lo que piensas: detente, estos objetos de Path no son los mismos que antes, ¡ya no operamos en líneas de ruta! Volveremos a este tema más tarde (pista: en casi cualquier situación, estos dos enfoques son intercambiables).


os sobrecargado


El clásico módulo os.path diseñado para trabajar con rutas. Pero después de que desee hacer algo con la ruta (por ejemplo, crear un directorio), deberá acceder a otro módulo, a menudo os .


os contiene un montón de utilidades para trabajar con archivos y directorios: mkdir , getcwd , chmod , stat , remove , rename , rmdir . También chdir , link , walk , listdir , makedirs , renames , removedirs , unlink , symlink . Y un montón de cosas que no están relacionadas con los sistemas de archivos: fork , getenv , putenv , getlogin , getlogin , system , ... Algunas docenas de cosas más que no mencionaré aquí.


El módulo os está diseñado para una amplia gama de tareas; Este es un cuadro con todo lo relacionado con el sistema operativo. Hay muchas utilidades en el sistema os , pero no siempre es fácil de navegar: a menudo es necesario profundizar un poco en el módulo antes de encontrar lo que necesita.


pathlib transfiere la mayoría de las funciones del sistema de archivos a objetos Path .


Aquí está el código que crea el src/__pypackages__ y cambia el nombre de nuestro archivo .editorconfig a src/.editorconfig :


 import os import os.path os.makedirs(os.path.join('src', '__pypackages__'), exist_ok=True) os.rename('.editorconfig', os.path.join('src', '.editorconfig')) 

Aquí hay un código similar usando Path


 from pathlib import Path Path('src/__pypackages__').mkdir(parents=True, exist_ok=True) Path('.editorconfig').rename('src/.editorconfig') 

Tenga en cuenta que el segundo ejemplo de código es mucho más fácil de leer, porque está organizado de izquierda a derecha; todo esto es gracias a las cadenas de métodos.


No te olvides de glob


No solo os y os.path contienen métodos relacionados con el sistema de archivos. También vale la pena mencionar sobre glob , que no se puede llamar inútil.


Podemos usar la función glob.glob para buscar archivos por un patrón específico:


 from glob import glob top_level_csv_files = glob('*.csv') all_csv_files = glob('**/*.csv', recursive=True) 

El módulo pathlib también proporciona métodos similares:


 from pathlib import Path top_level_csv_files = Path.cwd().glob('*.csv') all_csv_files = Path.cwd().rglob('*.csv') 

Después de cambiar al módulo pathlib , la necesidad del glob desaparece por completo : todo lo que necesita ya es una parte integral de los objetos Path


pathlib hace que las cosas simples sean aún más fáciles


pathlib simplifica muchas situaciones difíciles, pero también hace que algunos fragmentos de código sean aún más fáciles .


¿Quieres leer todo el texto en uno o más archivos?


Puede abrir el archivo, leer el contenido y cerrar el archivo con el bloque with :


 from glob import glob file_contents = [] for filename in glob('**/*.py', recursive=True): with open(filename) as python_file: file_contents.append(python_file.read()) 

O puede usar el método read_text en objetos Path y generar listas para obtener el mismo resultado en una expresión:


 from pathlib import Path file_contents = [ path.read_text() for path in Path.cwd().rglob('*.py') ] 

Pero, ¿qué pasa si necesita escribir en un archivo?


Esto es lo que parece usar open :


 with open('.editorconfig') as config: config.write('# config goes here') 

O puede usar el método write_text :


 Path('.editorconfig').write_text('# config goes here') 

Si por alguna razón necesita usar open , ya sea como administrador de contexto o para preferencias personales, Path ofrece el método open como alternativa:


 from pathlib import Path path = Path('.editorconfig') with path.open(mode='wt') as config: config.write('# config goes here') 

O, comenzando con Python 3.6, puede pasar su Path directamente para open :


 from pathlib import Path path = Path('.editorconfig') with open(path, mode='wt') as config: config.write('# config goes here') 

Los objetos de ruta hacen que su código sea más obvio


¿Qué indican las siguientes variables? ¿Cuál es el significado de sus significados?


 person = '{"name": "Trey Hunner", "location": "San Diego"}' pycon_2019 = "2019-05-01" home_directory = '/home/trey' 

Cada variable apunta a una línea. Pero cada uno de ellos tiene diferentes significados: el primero es JSON, el segundo es la fecha y el tercero es la ruta del archivo.


Esta representación de objetos es un poco más útil:


 from datetime import date from pathlib import Path person = {"name": "Trey Hunner", "location": "San Diego"} pycon_2019 = date(2019, 5, 1) home_directory = Path('/home/trey') 

Los objetos JSON se pueden deserializar en un diccionario, las fechas se pueden representar de forma nativa usando datetime.date y los objetos de ruta de archivo se pueden representar como Path


El uso de objetos Path hace que su código sea más explícito. Si desea trabajar con fechas, use date . Si desea trabajar con rutas de archivos, use Path .


No soy un gran defensor de OOP. Las clases agregan una capa adicional de abstracción, y las abstracciones a veces tienden a complicar el sistema en lugar de simplificarlo. Al mismo tiempo, creo que pathlib.Path es una abstracción útil . Bastante rápido, se convierte en una decisión aceptada.


Gracias a PEP 519 , las Path convierten en estándar para trabajar con rutas. En el momento de Python 3.6, la mayoría de los os.path os , shutil , os.path funcionan correctamente con estos objetos. ¡Puede cambiar a pathlib , transparente para su base de código!


¿Qué falta en pathlib ?


Aunque pathlib genial, no es exhaustivo. Definitivamente hay varias posibilidades que me gustaría incluir en el módulo .


Lo primero que viene a la mente es la falta de métodos de ruta equivalentes a shutil . Aunque puede pasar Path como parámetros shutil para copiar / eliminar / mover archivos y directorios, no puede llamarlos como métodos en objetos Path .


Entonces, para copiar archivos, debe hacer algo como esto:


 from pathlib import Path from shutil import copyfile source = Path('old_file.txt') destination = Path('new_file.txt') copyfile(source, destination) 

Tampoco existe un análogo del método os.chdir . Esto significa que debe importarlo si necesita cambiar el directorio actual:


 from pathlib import Path from os import chdir parent = Path('..') chdir(parent) 

Tampoco hay equivalente a la función os.walk . Aunque puedes escribir tu propia función en el espíritu de una walk sin mucha dificultad.


Espero que algún día los objetos pathlib.Path contengan métodos para algunas de las operaciones mencionadas. Pero incluso en este escenario, me resulta mucho más fácil usar pathlib con algo más que usar os.path y todo lo demás .


¿Es siempre necesario usar pathlib ?


Comenzando con Python 3.6, las Rutas funcionan en casi todas partes donde usas cadenas . Por lo tanto, no veo ninguna razón para no usar pathlib si está utilizando Python 3.6 y superior.


Si está utilizando una versión anterior de Python 3, en cualquier momento puede ajustar el objeto Path en una llamada de str para obtener una cadena si necesita regresar al país de líneas. Esto no es demasiado elegante, pero funciona:


 from os import chdir from pathlib import Path chdir(Path('/home/trey')) #   Python 3.6+ chdir(str(Path('/home/trey'))) #      

Parte 2. Respuestas a las preguntas.


Después de que se publicó la primera parte, algunas personas tuvieron algunas preguntas. Alguien dijo que pathlib os.path y pathlib deshonestamente. Algunos dijeron que el uso de os.path tan arraigado en la comunidad de Python que cambiar a una nueva biblioteca llevará mucho tiempo. También vi algunas preguntas sobre el rendimiento.


En esta parte, me gustaría comentar sobre estos temas. Esto puede considerarse tanto pathlib protección de pathlib como una pequeña carta de amor a PEP 519 .


Compare os.path y pathlib para ser honesto


En la última parte, comparé los siguientes dos fragmentos de código:


 import os import os.path os.makedirs(os.path.join('src', '__pypackages__'), exist_ok=True) os.rename('.editorconfig', os.path.join('src', '.editorconfig')) 

 from pathlib import Path Path('src/__pypackages__').mkdir(parents=True, exist_ok=True) Path('.editorconfig').rename('src/.editorconfig') 

Esto puede parecer una comparación injusta, porque el uso de os.path.join en el primer ejemplo garantiza que se usen los delimitadores correctos en todas las plataformas, lo que no hice en el segundo ejemplo. De hecho, todo está en orden, porque Path normaliza automáticamente los separadores de ruta


Podemos probar esto al convertir el objeto Path en una cadena en Windows:


 >>> str(Path('src/__pypackages__')) 'src\\__pypackages__' 

No joinpath si usamos el método joinpath , el '/' en la línea de ruta, el operador / (otra buena característica de Path ) o si pasamos argumentos individuales al constructor de Path, obtenemos el mismo resultado:


 >>> Path('src', '.editorconfig') WindowsPath('src/.editorconfig') >>> Path('src') / '.editorconfig' WindowsPath('src/.editorconfig') >>> Path('src').joinpath('.editorconfig') WindowsPath('src/.editorconfig') >>> Path('src/.editorconfig') WindowsPath('src/.editorconfig') 

El último ejemplo causó cierta confusión por parte de las personas que sugirieron que pathlib no pathlib lo suficientemente inteligente como para reemplazar / con \ en la cadena de ruta. Afortunadamente, ¡todo está en orden!


Con los objetos de Path , ya no necesita preocuparse por la dirección de las barras: defina todas sus rutas usando / , y el resultado será predecible para cualquier plataforma.


No tiene que preocuparse por normalizar los caminos.


Si está ejecutando en Linux o Mac, es muy fácil agregar accidentalmente errores al código que afectan solo a los usuarios de Windows. Si no supervisa de cerca el uso de os.path.join y \ u os.path.normcase para convertir barras inclinadas a las adecuadas para la plataforma actual, puede escribir código que no funcione correctamente en Windows .


Aquí hay un ejemplo de un error específico de Windows:


 import sys import os.path directory = '.' if not sys.argv[1:] else sys.argv[1] new_file = os.path.join(directory, 'new_package/__init__.py') 

Además, dicho código funcionará correctamente en todas partes:


 import sys from pathlib import Path directory = '.' if not sys.argv[1:] else sys.argv[1] new_file = Path(directory, 'new_package/__init__.py') 

Anteriormente, el programador era responsable de concatenar y normalizar rutas, al igual que en Python 2, el programador era responsable de decidir dónde usar unicode en lugar de bytes. Esta ya no es tu tarea: Path resuelve todos esos problemas por ti.


No uso Windows, y no tengo una computadora con Windows. Pero es muy probable que muchas personas que usarán mi código usen Windows, y quiero que todo funcione correctamente para ellos.


Si existe la posibilidad de que su código se ejecute en Windows, debería considerar seriamente cambiar a pathlib .


No se preocupe por la normalización : use Path todos modos cuando se trata de rutas de archivos.


Suena genial, ¡pero tengo una biblioteca de terceros que no usa pathlib !


Tiene una base de código grande que funciona con cadenas como rutas. ¿Por qué cambiar a pathlib si eso significa que todo debe reescribirse?


Imaginemos que tiene la siguiente función:


 import os import os.path def make_editorconfig(dir_path): """Create .editorconfig file in given directory and return filename.""" filename = os.path.join(dir_path, '.editorconfig') if not os.path.exists(filename): os.makedirs(dir_path, exist_ok=True) open(filename, mode='wt').write('') return filename 

La función toma un directorio y crea un archivo .editorconfig , algo como esto:


 >>> import os.path >>> make_editorconfig(os.path.join('src', 'my_package')) 'src/my_package/.editorconfig' 

Si reemplaza las líneas con Path , todo funcionará también:


 >>> from pathlib import Path >>> make_editorconfig(Path('src/my_package')) 'src/my_package/.editorconfig' 

Pero ... como?


os.path.join acepta objetos Path (desde Python 3.6). Lo mismo puede decirse de os.makedirs .
De hecho, la función open incorporada acepta Path , shutil acepta Path y todo en la biblioteca estándar utilizada para aceptar cadenas ahora debería funcionar tanto con Path como con cadenas.


Deberíamos agradecer a PEP 519 por esto , que proporcionó la clase abstracta os.PathLike y anunció que todas las utilidades integradas para trabajar con rutas de archivos ahora deberían funcionar tanto con cadenas como con Path .


¡Pero mi biblioteca favorita tiene Path, mejor que el estándar!


Es posible que ya esté utilizando una biblioteca de terceros que proporciona su implementación Path , que es diferente de la estándar. Quizás te guste más.


Por ejemplo, django- environmental , path.py , plumbum y visidata contienen cada uno sus propios objetos Path . Algunas de estas bibliotecas son más antiguas que pathlib y decidieron heredar de str para poder pathlib a funciones que esperan cadenas como rutas. Gracias a PEP 519, la integración de bibliotecas de terceros en su código será más fácil y sin la necesidad de herencia de str .


Imaginemos que no desea utilizar pathlib , porque Path son objetos inmutables y realmente desea cambiar su estado. Con PEP 519, puede crear su mejor versión mutable de Path . Para hacer esto, simplemente implemente el método __fspath__


Cualquier implementación Path de Path ahora puede funcionar de forma nativa con funciones integradas de Python que esperan rutas de archivos. Incluso si no le gusta pathlib , el hecho de su existencia es una gran ventaja para las bibliotecas de terceros con su propia Path


Pero pathlib.Path y str no se mezclan, ¿verdad?


Probablemente piense: esto es todo, por supuesto, genial, pero ¿este enfoque con a veces línea y a veces ruta agregará cierta complejidad a mi código?


La respuesta a esta pregunta es sí, hasta cierto punto. Pero este problema tiene una solución bastante simple.


PEP 519 agregó algunas cosas más además de PathLike : en primer lugar, es una forma de convertir cualquier PathLike en una cadena y, en segundo lugar, es una forma de convertir cualquier PathLike en una Path .


Tomemos dos objetos: una cadena y Path (o lo que sea con el método fspath ):


 from pathlib import Path import os.path p1 = os.path.join('src', 'my_package') p2 = Path('src/my_package') 

La función os.fspath normaliza ambos objetos y los convierte en cadenas:


 >>> from os import fspath >>> fspath(p1), fspath(p2) ('src/my_package', 'src/my_package') 

En este caso, Path puede tomar ambos objetos en un constructor y convertirlos a Path :


 >>> Path(p1), Path(p2) (PosixPath('src/my_package'), PosixPath('src/my_package')) 

Esto significa que puede convertir el resultado de make_editorconfig a Path si es necesario:


 >>> from pathlib import Path >>> Path(make_editorconfig(Path('src/my_package'))) PosixPath('src/my_package/.editorconfig') 

Aunque, por supuesto, la mejor solución sería reescribir make_editorconfig usando pathlib .


pathlib demasiado lento


He visto varias veces sobre el rendimiento de pathlib . Es cierto: pathlib puede ser lento. Crear miles de objetos Path puede afectar significativamente el comportamiento del programa.


Decidí medir el rendimiento de pathlib y os.path en mi computadora usando dos programas diferentes que buscan todos los archivos .py en el directorio actual


Aquí está la versión de os.walk :


 from os import getcwd, walk extension = '.py' count = 0 for root, directories, filenames in walk(getcwd()): for filename in filenames: if filename.endswith(extension): count += 1 print(f"{count} Python files found") 

Y aquí está la versión con Path.rglob :


 from pathlib import Path extension = '.py' count = 0 for filename in Path.cwd().rglob(f'*{extension}'): count += 1 print(f"{count} Python files found") 

Probar el rendimiento de los programas que funcionan con el sistema de archivos es una tarea difícil, porque el tiempo de funcionamiento puede cambiar bastante. Decidí ejecutar cada script 10 veces y comparé los mejores resultados para cada programa.


Ambos programas encontraron archivos 97507 en el directorio en el que los ejecuté. El primero funcionó en 1.914 segundos, el segundo terminó en 3.430 segundos.


Cuando configuro el parámetro extension='' , estos programas encuentran aproximadamente 600,000 archivos, y la diferencia aumenta. El primer programa funcionó en 1.888 segundos, y el segundo en 7.485 segundos.


Por lo tanto, pathlib es aproximadamente el doble de lento para los archivos con la extensión .py , y cuatro veces más lento cuando se inicia en mi directorio de inicio. La brecha relativa de rendimiento entre pathlib y os es amplia.


En mi caso, esta velocidad no cambia mucho. Busqué todos los archivos en mi directorio y perdí 6 segundos. Si tuviera la tarea de procesar 10 millones de archivos, probablemente lo reescribiría. Pero si bien no existe tal necesidad, puede esperar.


Si tiene un código caliente y pathlib obviamente afecta negativamente su funcionamiento, no hay nada de malo en reemplazarlo por una alternativa. No debe optimizar el código, que no es un cuello de botella , esto es una pérdida de tiempo adicional, que generalmente también conduce a un código poco legible, sin mucho agotamiento.


Mejora de legibilidad


Me gustaría terminar esta corriente de pensamientos con algunos ejemplos de refactorización utilizando pathlib . Tomé un par de pequeños ejemplos de código que funciona con archivos y los hice funcionar con pathlib . Dejaré la mayor parte del código sin comentarios en su corte; decida qué versión le gusta más.


Aquí está la función make_editorconfig que vimos anteriormente:


 import os import os.path def make_editorconfig(dir_path): """Create .editorconfig file in given directory and return filename.""" filename = os.path.join(dir_path, '.editorconfig') if not os.path.exists(filename): os.makedirs(dir_path, exist_ok=True) open(filename, mode='wt').write('') return filename 

Y aquí está la versión reescrita en pathlib :


 from pathlib import Path def make_editorconfig(dir_path): """Create .editorconfig file in given directory and return filepath.""" path = Path(dir_path, '.editorconfig') if not path.exists(): path.parent.mkdir(exist_ok=True, parent=True) path.touch() return path 

Aquí hay un programa de consola que toma una línea con un directorio e imprime el contenido de un archivo .gitignore , si existe:


 import os.path import sys directory = sys.argv[1] ignore_filename = os.path.join(directory, '.gitignore') if os.path.isfile(ignore_filename): with open(ignore_filename, mode='rt') as ignore_file: print(ignore_file.read(), end='') 

Lo mismo con pathlib :


 from pathlib import Path import sys directory = Path(sys.argv[1]) ignore_path = directory / '.gitignore' if ignore_path.is_file(): print(ignore_path.read_text(), end='') 

Aquí hay un programa que imprime todos los archivos duplicados en la carpeta y subcarpetas actuales:


 from collections import defaultdict from hashlib import md5 from os import getcwd, walk import os.path def find_files(filepath): for root, directories, filenames in walk(filepath): for filename in filenames: yield os.path.join(root, filename) file_hashes = defaultdict(list) for path in find_files(getcwd()): with open(path, mode='rb') as my_file: file_hash = md5(my_file.read()).hexdigest() file_hashes[file_hash].append(path) for paths in file_hashes.values(): if len(paths) > 1: print("Duplicate files found:") print(*paths, sep='\n') 

Lo mismo con c pathlib :


 from collections import defaultdict from hashlib import md5 from pathlib import Path def find_files(filepath): for path in Path(filepath).rglob('*'): if path.is_file(): yield path file_hashes = defaultdict(list) for path in find_files(Path.cwd()): file_hash = md5(path.read_bytes()).hexdigest() file_hashes[file_hash].append(path) for paths in file_hashes.values(): if len(paths) > 1: print("Duplicate files found:") print(*paths, sep='\n') 

, , -, . pathlib .


pathlib.Path


.


/ pathlib.Path . , .


 >>> path1 = Path('dir', 'file') >>> path2 = Path('dir') / 'file' >>> path3 = Path('dir/file') >>> path3 WindowsPath('dir/file') >>> path1 == path2 == path3 True 

Python (. open ) Path , , pathlib , !


 from shutil import move def rename_and_redirect(old_filename, new_filename): move(old, new) with open(old, mode='wt') as f: f.write(f'This file has moved to {new}') 

 >>> from pathlib import Path >>> old, new = Path('old.txt'), Path('new.txt') >>> rename_and_redirect(old, new) >>> old.read_text() 'This file has moved to new.txt' 

pathlib , , PathLike . , , , PEP 519 .


 >>> from plumbum import Path >>> my_path = Path('old.txt') >>> with open(my_path) as f: ... print(f.read()) ... This file has moved to new.txt 

pathlib , ( , ), , .


, pathlib . Python :


 from pathlib import Path gitignore = Path('.gitignore') if gitignore.is_file(): print(gitignore.read_text(), end='') 

pathlib — . !

Source: https://habr.com/ru/post/453862/


All Articles