Por que você deve usar o pathlib

Do tradutor: Olá, Habr! Apresento a você a tradução do artigo Por que você deveria usar o pathlib e sua continuação? Não, na verdade, o pathlib é ótimo . Está sendo prestada muita atenção agora a novos recursos do Python como assíncio, operador: = e digitação opcional. Ao mesmo tempo, não tão significativo (embora :: chamar uma linguagem de inovação séria não se mostre uma inovação séria) correndo o risco de passar por trás do radar, mas inovações muito úteis na linguagem. Em particular, em um grande número de artigos dedicados a um assunto, não encontrei (exceto um parágrafo aqui ), portanto, decidi corrigir a situação.


Quando descobri o então novo módulo pathlib , há alguns anos, decidi de fundo que era apenas uma versão um pouco estranha e orientada a os.path módulo os.path . Eu estava errado. pathlib é realmente maravilhoso !


Neste artigo, tentarei me apaixonar pelo pathlib . Espero que este artigo o pathlib a usar o pathlib em qualquer situação referente ao trabalho com arquivos em Python .



Parte 1


os.path estranho


O módulo os.path sempre foi o que usamos quando se tratava de caminhos Python. Em princípio, há tudo o que você precisa, mas muitas vezes não parece muito elegante.


Devo importá-lo assim?


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

Ou então?


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

Talvez a função de join tenha um nome muito geral, e devemos fazer algo assim:


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

Para mim, todas as opções acima parecem não muito convenientes. Passamos strings para funções que retornam strings que passamos para as próximas funções que trabalham com strings. Aconteceu que todos eles contêm caminhos, mas ainda são apenas linhas.


O uso de strings para entrada e saída nas funções os.path muito inconveniente, pois é necessário ler o código de dentro para fora. Gostaria de converter essas chamadas de aninhadas para seqüenciais. É isso que o pathlib permite que você faça!


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

O módulo os.path requer chamadas de função aninhadas, mas pathlib nos permite criar cadeias de chamadas consecutivas para métodos e atributos da classe Path com um resultado equivalente.


Eu sei o que você pensa: pare, esses objetos Path não são os mesmos de antes, não operamos mais nas linhas de caminho! Voltaremos a esse problema mais tarde (dica: em quase qualquer situação, essas duas abordagens são intercambiáveis).


os sobrecarregado


O módulo os.path clássico os.path projetado para trabalhar com caminhos. Mas depois que você quiser fazer algo com o caminho (por exemplo, criar um diretório), precisará acessar outro módulo, geralmente o os .


os contém vários utilitários para trabalhar com arquivos e diretórios: mkdir , getcwd , chmod , stat , remove , rename , rmdir . Também chdir , link , walk , listdir , makedirs , renames , removedirs , unlink , symlink . E um monte de coisas que não estão relacionadas a sistemas de arquivos: fork , getenv , putenv , getlogin , getlogin , system , ... Mais algumas dúzias de coisas que não vou mencionar aqui.


O módulo os foi projetado para uma ampla variedade de tarefas; essa é uma caixa com tudo relacionado ao sistema operacional. Existem muitas utilidades no sistema os , mas nem sempre é fácil navegar: muitas vezes é necessário aprofundar um pouco no módulo antes de encontrar o que você precisa.


pathlib transfere a maioria das funções do sistema de arquivos para os objetos Path .


Aqui está o código que cria o src/__pypackages__ e renomeia nosso arquivo .editorconfig para 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')) 

Aqui está um código semelhante usando Path


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

Observe que o segundo exemplo de código é muito mais fácil de ler, porque é organizado da esquerda para a direita - tudo isso graças às cadeias de métodos.


Não se esqueça da glob


Não apenas os e os.path contêm métodos relacionados ao sistema de arquivos. Também vale a pena mencionar sobre glob , que não pode ser chamado de inútil.


Podemos usar a função glob.glob para procurar arquivos por um padrão específico:


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

O módulo pathlib também fornece métodos semelhantes:


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

Depois de mudar para o módulo pathlib , a necessidade do glob desaparece completamente : tudo o que você precisa já é parte integrante dos objetos Path


pathlib torna as coisas simples ainda mais fáceis


pathlib simplifica muitas situações difíceis, mas também facilita alguns trechos de código simples .


Deseja ler todo o texto em um ou mais arquivos?


Você pode abrir o arquivo, ler o conteúdo e fechar o arquivo usando o bloco 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()) 

Ou você pode usar o método read_text em objetos Path e gerar listas para obter o mesmo resultado em uma expressão:


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

Mas e se você precisar gravar em um arquivo?


Aqui está o que parece usando open :


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

Ou você pode usar o método write_text :


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

Se, por algum motivo, você precisar usar o open , como gerenciador de contexto ou por preferências pessoais, o Path fornecerá o método open como uma alternativa:


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

Ou, começando com Python 3.6, você pode passar seu Path diretamente para open :


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

Objetos de caminho tornam seu código mais óbvio


O que as seguintes variáveis ​​indicam? Qual é o significado de seus significados?


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

Cada variável aponta para uma linha. Mas cada um deles tem significados diferentes: o primeiro é JSON, o segundo é a data e o terceiro é o caminho do arquivo.


Essa representação de objetos é um pouco mais ú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') 

Objetos JSON podem ser desserializados em um dicionário, datas podem ser representadas nativamente usando datetime.date e objetos de caminho de arquivo podem ser representados como Path


O uso de objetos Path torna seu código mais explícito. Se você quiser trabalhar com datas, use date . Se você deseja trabalhar com caminhos de arquivo, use Path .


Eu não sou um grande apoiador da OOP. As classes adicionam uma camada extra de abstração, e as abstrações às vezes tendem a complicar o sistema, em vez de simplificá-lo. Ao mesmo tempo, acredito que pathlib.Path é uma abstração útil . Muito rapidamente, torna-se uma decisão aceita.


Graças ao PEP 519 , os Path se tornam padrão para trabalhar com caminhos. No momento do Python 3.6, a maioria dos os.path os , shutil , os.path funciona corretamente com esses objetos. Você pode mudar para o pathlib , transparente para sua base de código!


O que está faltando no pathlib ?


Embora pathlib legal, não é abrangente. Definitivamente, existem várias possibilidades que eu gostaria de incluir no módulo .


A primeira coisa que vem à mente é a falta de métodos de caminho equivalentes ao shutil . Embora você possa passar o Path como parâmetros do shutil para copiar / excluir / mover arquivos e diretórios, não é possível chamá-los como métodos nos objetos Path .


Portanto, para copiar arquivos, você precisa fazer algo assim:


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

Também não há análogo do método os.chdir . Isso significa que você precisará importá-lo se precisar alterar o diretório atual:


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

Também não há equivalente à função os.walk . Embora você possa escrever sua própria função no espírito de uma walk sem muita dificuldade.


Espero que um dia os objetos pathlib.Path contenham métodos para algumas das operações mencionadas. Mas mesmo nesse cenário, acho muito mais fácil usar o pathlib com outra coisa do que usar os.path e tudo mais .


É sempre necessário usar o pathlib ?


A partir do Python 3.6, os Paths funcionam em quase todos os lugares em que você usa strings . Portanto, não vejo razão para não usar o pathlib se você estiver usando o Python 3.6 e superior.


Se você estiver usando uma versão anterior do Python 3, poderá, a qualquer momento, agrupar o objeto Path em uma chamada str para obter uma string se precisar retornar ao país de linhas. Isso não é muito elegante, mas funciona:


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

Parte 2. Respostas às perguntas.


Depois que a primeira parte foi publicada, algumas pessoas tiveram algumas perguntas. Alguém disse que comparei as pathlib e pathlib desonesta. Alguns disseram que o uso do os.path tão arraigado na comunidade Python que a mudança para uma nova biblioteca levará muito tempo. Eu também vi algumas perguntas sobre desempenho.


Nesta parte, eu gostaria de comentar sobre essas questões. Isso pode ser considerado proteção contra pathlib e uma pathlib de carta de amor para o PEP 519 .


Compare os.path e pathlib para ser honesto


Na última parte, comparei os dois fragmentos de código a seguir:


 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') 

Isso pode parecer uma comparação injusta, porque o uso de os.path.join no primeiro exemplo garante que os delimitadores corretos sejam usados ​​em todas as plataformas, o que não fiz no segundo exemplo. De fato, tudo está em ordem, porque o Path normaliza automaticamente os separadores de caminho


Podemos provar isso, convertendo o objeto Path em uma string no Windows:


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

Não faz diferença se usamos o método joinpath , o '/' na linha do caminho, o operador / (outro recurso interessante do Path ) ou passamos argumentos individuais para o construtor Path, obtemos o mesmo 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') 

O último exemplo causou certa confusão por pessoas que sugeriram que o pathlib não pathlib inteligente o suficiente para substituir / por \ na cadeia de caminho. Felizmente, tudo está em ordem!


Com os objetos Path , você não precisa mais se preocupar com a direção das barras: defina todos os seus caminhos usando / , e o resultado será previsível para qualquer plataforma.


Você não precisa se preocupar em normalizar os caminhos.


Se você estiver executando no Linux ou Mac, é muito fácil adicionar erros acidentalmente ao código que afeta apenas usuários do Windows. Se você não monitorar de perto o uso de os.path.join e \ ou os.path.normcase para converter barras para aquelas adequadas para a plataforma atual, você pode escrever um código que não funcionará corretamente no Windows .


Aqui está um exemplo de um bug específico do 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') 

Além disso, esse código funcionará corretamente em qualquer lugar:


 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, o programador era responsável por concatenar e normalizar os caminhos, assim como no Python 2, o programador era responsável por decidir onde usar o unicode em vez de bytes. Esta não é mais sua tarefa - o Path resolve todos esses problemas para você.


Não uso o Windows e não tenho um computador com Windows. Mas muitas pessoas que usarão meu código provavelmente usarão o Windows, e eu quero que tudo funcione corretamente para elas.


Se houver uma chance de seu código ser executado no Windows, considere seriamente mudar para o pathlib .


Não se preocupe com a normalização : use o Path qualquer maneira quando se trata de caminhos de arquivo.


Parece legal, mas eu tenho uma biblioteca de terceiros que não usa o pathlib !


Você tem uma grande base de código que trabalha com seqüências de caracteres como caminhos. Por que mudar para pathlib se isso significa que tudo precisa ser reescrito?


Vamos imaginar que você tenha a seguinte função:


 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 

A função pega um diretório e cria um arquivo .editorconfig , algo como isto:


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

Se você substituir as linhas por Path , tudo funcionará também:


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

Mas como?


os.path.join aceita objetos Path (desde Python 3.6). O mesmo pode ser dito dos os.makedirs .
De fato, a função open shutil aceita Path , shutil aceita Path e tudo na biblioteca padrão usada para aceitar strings agora deve funcionar com Path e strings.


Deveríamos agradecer ao PEP 519 por isso , que forneceu a classe abstrata os.PathLike e anunciou que todos os utilitários os.PathLike para trabalhar com caminhos de arquivos devem agora funcionar com as strings e Path .


Mas minha biblioteca favorita tem Path, melhor que o padrão!


Você já pode estar usando uma biblioteca de terceiros que fornece sua implementação de Path , diferente da padrão. Talvez você goste mais dela.


Por exemplo, django-environ , path.py , plumbum e visidata contêm seus próprios objetos Path . Algumas dessas bibliotecas são mais antigas que o pathlib e decidiram herdar do str para que pudessem ser passadas para funções que esperam que strings sejam caminhos. Graças ao PEP 519, a integração de bibliotecas de terceiros em seu código será mais fácil e sem a necessidade de herança de str .


Vamos imaginar que você não deseja usar o pathlib , porque o Path é um objeto imutável e você realmente deseja alterar o estado deles. Com o PEP 519, você pode criar sua melhor versão mutável do Path . Para fazer isso, basta implementar o método __fspath__


Qualquer implementação auto-escrita do Path agora pode trabalhar nativamente com funções internas do Python que esperam caminhos de arquivo. Mesmo se você não gostar do pathlib , o fato de sua existência é uma grande vantagem para bibliotecas de terceiros com seu próprio Path


Mas pathlib.Path e str não se misturam, certo?


Você provavelmente pensa: isso é tudo, é claro, ótimo, mas essa abordagem com o caminho às vezes linha e às vezes adiciona alguma complexidade ao meu código?


A resposta a esta pergunta é sim, até certo ponto. Mas esse problema tem uma solução bastante simples.


O PEP 519 adicionou mais algumas coisas além do PathLike : em primeiro lugar, é uma maneira de converter qualquer PathLike em uma string e, em segundo lugar, é uma maneira de transformar qualquer PathLike em um Path .


Vamos pegar dois objetos - uma string e Path (ou qualquer outra coisa com o método fspath ):


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

A função os.fspath normaliza os dois objetos e os transforma em strings:


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

Nesse caso, o Path pode levar esses dois objetos em um construtor e convertê-los em Path :


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

Isso significa que você pode converter o resultado de make_editorconfig novamente em Path se necessário:


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

Embora, é claro, a melhor solução seja reescrever o make_editorconfig usando o pathlib .


pathlib muito lento


Eu já vi várias vezes sobre o desempenho do pathlib . É verdade - o pathlib pode ser lento. Criar milhares de objetos Path pode afetar significativamente o comportamento do programa.


Decidi medir o desempenho do pathlib e do os.path no meu computador usando dois programas diferentes que procuram todos os arquivos .py no diretório atual


Aqui está a versão do 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") 

E aqui está a versão com 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") 

Testar o desempenho de programas que funcionam com o sistema de arquivos é uma tarefa complicada, porque o tempo de operação pode mudar bastante. Decidi executar cada script 10 vezes e comparei os melhores resultados para cada programa.


Ambos os programas encontraram arquivos 97507 no diretório em que eu os executei. O primeiro trabalhou em 1.914 segundos, o segundo terminou em 3.430 segundos.


Quando defino o parâmetro extension='' , esses programas encontram aproximadamente 600.000 arquivos e a diferença aumenta. O primeiro programa funcionou em 1.888 segundos e o segundo em 7.485 segundos.


Portanto, o pathlib é duas vezes mais lento para arquivos com a extensão .py e quatro vezes mais lento quando iniciado no meu diretório pessoal. A diferença de desempenho relativa entre pathlib e os é grande.


No meu caso, essa velocidade não muda muito. Eu procurei todos os arquivos no meu diretório e perdi 6 segundos. Se eu tivesse a tarefa de processar 10 milhões de arquivos, provavelmente o reescreveria. Mas enquanto não houver essa necessidade, você pode esperar.


Se você tem um pedaço quente de código e o pathlib obviamente afeta negativamente sua operação, não há nada errado em substituí-lo por uma alternativa. Você não deve otimizar o código, o que não é um gargalo - isso é um desperdício de tempo extra, que geralmente também leva a códigos mal legíveis, sem muita exaustão.


Melhoria da legibilidade


Gostaria de encerrar esse fluxo de pensamentos com alguns exemplos de refatoração usando pathlib . Peguei alguns pequenos exemplos de código que funcionam com arquivos e os fiz trabalhar com o pathlib . Deixarei a maior parte do código sem comentar em seu tribunal. Decida qual versão você mais gosta.


Aqui está a função 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 

E aqui está a versão reescrita no 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 

Aqui está um programa de console que segue uma linha com um diretório e imprime o conteúdo de um arquivo .gitignore , se existir:


 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='') 

A mesma coisa com o 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='') 

Aqui está um programa que imprime todos os arquivos duplicados na pasta e subpastas atuais:


 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') 

A mesma coisa com 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/pt453862/


All Articles