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'))
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
— . !