为什么要使用pathlib

来自译者:您好,Habr! 我向您介绍了文章为什么您应该使用pathlib及其后续文章的翻译, 真的,pathlib很棒 现在,人们已经非常关注诸如asyncio 、: =运算符和可选类型等Python新功能。 同时,雷达的风险不是那么大(尽管:: = =称呼一项重大创新,一种语言并没有证明这是一项重大创新),但它却是一种非常有用的语言创新。 特别是,在专门讨论某个主题的文章中,我没有找到( 这里只有一段),因此我决定纠正这种情况。


几年前,当我发现当时新的pathlib模块时,我下意识地决定它只是os.path模块的一个有点尴尬的面向对象版本。 我错了 pathlib真的很棒


在本文中,我将尝试爱上pathlib 。 我希望本文能pathlib您在使用Python处理文件的任何情况下使用pathlib



第一部分


os.path尴尬


os.path模块一直是我们使用Python路径时使用的模块。 原则上,您需要任何东西,但通常看起来并不那么优雅。


我应该这样导入吗?


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

大概吧


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

也许join函数的名称太笼统,我们应该执行以下操作:


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

对我来说,以上所有选项似乎都不方便。 我们将字符串传递给返回字符串的函数,然后传递给下一个使用字符串的函数。 碰巧它们都包含路径,但它们仍然只是线。


os.path函数中使用字符串作为输入和输出非常不便,因为您必须从内而外读取代码。 我想将这些调用从嵌套转换为顺序。 这就是pathlib允许您执行的操作!


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

os.path模块需要嵌套的函数调用,但是pathlib 允许我们创建对Path类的方法和属性的连续调用的链,并获得等效的结果。


我知道您的想法:停止,这些Path对象与以前不同,我们不再对路径行进行操作! 我们稍后会再讨论这个问题(提示:在几乎任何情况下,这两种方法都是可以互换的)。


os超载


经典的os.path模块设计用于路径。 但是,要对路径进行某些操作 (例如,创建目录)后,需要访问另一个模块,通常是os


os包含一堆用于处理文件和目录的实用程序: mkdirgetcwdchmodstatremoverenamermdir 。 还可以使用chdirlinkwalklistdirmakedirsrenamesremovedirsunlinksymlink 。 还有一堆根本不与文件系统相关的东西: forkgetenvputenvenvirongetloginsystem ……还有几十个我在这里不会提及的东西。


os模块设计用于多种任务。 这是一个包含与操作系统相关的所有内容的盒子。 os有许多用途,但是导航并不总是那么容易:在找到所需内容之前,经常有必要深入研究模块。


pathlib将大多数文件系统功能转移到Path对象。


这是创建src/__pypackages__并将我们的.editorconfig文件重命名为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')) 

这是使用Path类似代码


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

请注意,第二个代码示例更容易阅读,因为它是从左到右组织的-这一切都归功于方法链。


不要忘了全球


不仅osos.path包含与文件系统相关的方法。 值得一提的是glob ,不能称之为无用。


我们可以使用glob.glob函数按特定模式搜索文件:


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

pathlib模块还提供了类似的方法:


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

切换到pathlib模块后,对glob需求完全消失了 :您所需的一切已经是Path对象的组成部分


pathlib使简单的事情变得更加容易


pathlib简化了许多困难的情况,但也使一些简单的代码片段更加容易


是否想读取一个或多个文件中的所有文本?


您可以使用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()) 

或者,您可以在Path对象上使用read_text方法并生成列表以在一个表达式中获得相同的结果:


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

但是,如果您需要写入文件怎么办?


这是使用open样子:


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

或者,您可以使用write_text方法:


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

如果出于某种原因需要将open用作上下文管理器或出于个人喜好, Path可以提供open方法作为替代方法:


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

或者,从Python 3.6开始,您可以直接将Path传递给open


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

路径对象使您的代码更明显


以下变量表示什么? 它们的含义是什么?


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

每个变量都指向一行。 但是它们每个都有不同的含义:第一个是JSON,第二个是日期,第三个是文件路径。


对象的这种表示稍微有用:


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

JSON对象可以反序列化为字典,日期可以使用datetime.date本地表示, 文件路径对象可以表示为Path


使用Path对象可使您的代码更加明确。 如果要使用日期,请使用date 。 如果要使用文件路径,请使用Path


我不是OOP的大力支持者。 类增加了额外的抽象层,抽象有时会使系统复杂而不是简化系统。 同时,我相信pathlib.Path有用的抽象 。 很快,这成为一个可以接受的决定。


借助PEP 519Path成为使用路径的标准。 在Python 3.6发行时,大多数osshutilos.path可以与这些对象一起正常工作。 您可以立即切换到pathlib ,对您的代码库而言是透明的!


pathlib缺少什么?


尽管pathlib很酷,但并不全面。 我肯定希望将其包含在模块中


首先想到的是缺少等同于shutil的路径方法。 尽管您可以将Path作为shutil参数传递给复制/删除/移动文件和目录,但是您不能将它们作为Path对象的方法来调用。


因此,要复制文件,您需要执行以下操作:


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

os.chdir方法也没有类似物。 这意味着如果需要更改当前目录,则需要将其导入:


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

也没有等效的os.walk函数。 尽管您可以本着walk的精神编写自己的函数,但没有太多困难。


我希望有一天pathlib.Path对象将包含上述某些操作的方法。 但是即使在这种情况下, 我发现将pathlib与其他东西一起使用比使用os.path和其他所有东西要容易得多


是否总是需要使用pathlib


从Python 3.6开始, 路径几乎可以在使用string的任何地方工作 。 因此,如果您使用的是Python 3.6及更高版本,我认为没有理由使用pathlib


如果使用的是Python 3的早期版本,则需要返回行所在的国家/地区时,可以随时将Path对象包装在str调用中以获取字符串。 这不是太优雅,但是它可以工作:


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

第2部分。问题的答案。


第一部分出版后,有些人提出了一些问题。 有人说我不诚实地比较了os.pathpathlib 。 有人说使用os.path在Python社区中根深蒂固,以至于迁移到新库将花费很长时间。 我还看到了一些有关性能的问题。


在这一部分中,我想对这些问题发表评论。 这既可以视为pathlib保护,也可以看作是给PEP 519的一封情书。


老实比较os.pathpathlib


在最后一部分中,我比较了以下两个代码片段:


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

这似乎是不公平的比较,因为在第一个示例中使用os.path.join确保在所有平台上使用正确的定界符,而在第二个示例中则没有。 实际上,一切都井井有条,因为Path会自动规范化路径分隔符


我们可以通过在Windows上将Path对象转换为字符串来证明这一点:


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

无论我们使用joinpath方法,路径行中的'/'/运算符( Path另一个不错的功能)还是将单独的参数传递给Path构造函数,都没有什么区别:


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

最后一个例子引起了一些人的困惑,他们认为pathlib不够聪明,无法在路径字符串中用\替换/ 。 幸运的是,一切都井然有序!


使用Path对象,您不再需要担心斜线的方向:使用/定义所有路径,并且结果对于任何平台都是可预测的。


您不必担心标准化路径。


如果您使用的是Linux或Mac,则很容易在代码中意外添加仅影响Windows用户的错误。 如果您不仔细监视os.path.join和\或os.path.normcase以将斜杠转换为适合当前平台的斜杠,则可以编写在Windows上无法正常工作的代码


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

而且,这样的代码将在任何地方正确工作:


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

以前,程序员负责连接和规范化路径,就像在Python 2中一样,程序员负责决定在哪里使用unicode而不是字节。 这不再是您的任务Path为您解决所有此类问题。


我不使用Windows,也没有Windows计算机。 但是,很多将使用我的代码的人很可能会使用Windows,我希望所有内容都能为他们正常工作。


如果您的代码有可能在Windows上运行,则应认真考虑切换到pathlib


不用担心规范化 :涉及文件路径时,请务必使用Path


听起来很酷,但是我有一个不使用pathlib的第三方库!


您有一个大型代码库,可以将字符串作为路径使用。 如果那意味着所有内容都需要重写,为什么要切换到pathlib


假设您具有以下功能:


 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 

该函数获取目录并在其中创建一个.editorconfig文件,如下所示:


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

如果将行替换为Path ,那么一切也将正常工作:


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

但是...如何?


os.path.join接受Path对象(自Python 3.6起)。 os.makedirs也可以这样说。
实际上,内置的open函数接受Pathshutil接受Path并且标准库中用于接受字符串的所有内容现在都应同时适用于Path和字符串。


我们应该为此感谢PEP 519 ,它提供了抽象类os.PathLike并宣布所有用于处理文件路径的内置实用程序现在都应同时适用于string和Path


但是我最喜欢的库具有Path,比标准库更好!


您可能已经在使用提供其Path实现的第三方库,该库与标准库不同。 也许您更喜欢她。


例如, django-environpath.pyplumbumvisidata各自包含自己的Path对象。 其中一些库比pathlib ,并且决定从str继承,以便可以将它们传递pathlib字符串为路径的函数。 借助PEP 519,将第三方库集成到您的代码中将变得更加容易,并且无需继承str


假设您不想使用pathlib ,因为Path是不可变的对象,并且您确实真的想更改它们的状态。 使用PEP 519,可以创建Path最佳可变版本。 为此,只需实现__fspath__方法


现在,任何自写的Path实现都可以在本机中与需要文件路径的Python内置函数一起使用。 即使您不喜欢pathlib ,它的存在对于具有自己的Path的第三方库也是一大优势


但是pathlib.Pathstr不能混合使用,对吗?


您可能会认为:这当然很好,但是这种有时使用行有时路径的方法会给我的代码增加一些复杂性吗?


在某种程度上,这个问题的答案是肯定的。 但是这个问题有一个非常简单的解决方法。


PEP 519除了PathLike之外还添加了其他一些PathLike :首先,这是一种将PathLike转换为字符串的方法,其次,这是一种将PathLike转换为Path


让我们接受两个对象-字符串和Path (或使用fspath方法的任何对象):


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

os.fspath函数对两个对象进行规范化并将它们转换为字符串:


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

在这种情况下, Path可以将这两个对象都带入构造函数并将其转换为Path


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

这意味着您可以根据需要将make_editorconfig的结果转换回Path


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

当然,最好的解决方案是使用pathlib重写pathlib


pathlib太慢


我已经看过几次有关pathlib性能的信息。 没错pathlib可能很慢。 创建数千个Path对象会严重影响程序行为。


我决定使用两个不同的程序在当前目录中查找所有.py文件的方式来测量我的计算机上pathlibos.path的性能。


这是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") 

这是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") 

测试与文件系统一起使用的程序的性能是一项艰巨的任务,因为操作时间可能会发生很大变化。 我决定将每个脚本运行10次,并比较每个程序的最佳结果。


这两个程序都在我运行它们的目录中找到了97507个文件。 第一个完成了1.914秒,第二个完成了3.430秒。


当我设置extension=''参数时,这些程序将找到大约600,000个文件,并且差异增加。 第一个程序耗时1.888秒,第二个程序耗时7.485秒。


因此,以.py扩展名的文件, pathlib速度大约是它的两倍 ,而在我的主目录中启动时, pathlib速度pathlib是它的四倍pathlibos之间的相对性能差距很大。


就我而言,这个速度变化不大。 我搜索了目录中的所有文件,并丢失了6秒钟。 如果我要处理1000万个文件,则很可能会重写它。 但是尽管没有这种需要,您可以等待。


如果您有一段热门代码,而pathlib显然会对它的操作产生负面影响,那么用替代方法替换它就没有错。 您不应该优化代码,这不是瓶颈 ,这是浪费时间,这通常也会导致代码可读性差,而不会造成很多浪费。


可读性提高


我想用一些使用pathlib重构示例来结束这一思路。 我拿了一些小示例代码来处理文件,并使其与pathlib一起工作。 我将大部分代码保留在法庭上,而无需评论-确定您最喜欢的版本。


这是我们之前看到的make_editorconfig函数:


 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 

这是在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 

这是一个控制台程序,该程序使用目录行并打印.gitignore文件的内容(如果存在):


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

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

这是一个打印当前文件夹和子文件夹中所有重复文件的程序:


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

与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/zh-CN453862/


All Articles