(python)使用pathlib替代os.path(轉錄)

Ryan Lu
AI反斗城
Published in
15 min readNov 13, 2020

在 Python 3.4 之前和路徑相關操作函式都放在 os 模組裡面,尤其是 os.path 這個子模組,可以說 os.path 模組非常常用。而在 Python 3.4,標準庫添加了新的模組 - pathlib,它使用面向物件的程式設計方式來表示檔案系統路徑。

作為一個從 Python 2 時代過來的人,已經非常習慣使用 os,那麼為什麼我說「應該使用 pathlib 替代 os.path」呢?基於這段時間的體驗,我列出了幾個 pathlib 模組的優勢和特點。

基本用法

在過去,檔案的路徑是純字串,現在它會是一個 pathlib.Path 物件:

In : from pathlib import PathIn : p = Path('/home/ubuntu')In : p
Out: PosixPath('/home/ubuntu')
In : str(p)
Out: '/home/ubuntu'

使用 str 函式可以把一個 Path 物件轉化成字串。在 Python 3.6 之前,Path 物件是不能作為 os 模組下的引數的,需要手動轉化成字串:

➜  ~ ipython3.5
Python 3.5.5 (default, Aug 1 2019, 17:00:43)
Type 'copyright', 'credits' or 'license' for more information
IPython 7.3.0 -- An enhanced Interactive Python. Type '?' for help.
In : import osIn : pwd
Out: '/data/home/dongweiming'
In : p = Path('/')In : os.chdir(p)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-5-facfc9d7a7d1> in <module>
----> 1 os.chdir(p)
TypeError: argument should be string, bytes or integer, not PosixPathIn : os.chdir(str(p))In : pwd
Out: '/'

從 Python 3.6 開始,這些接受路徑作為引數的函式內部會先通過 os.fspath 呼叫 Path 物件的 __fspath__ 方法獲得字串型別的路徑再去執行下面的邏輯。所以要注意: 如果你想全面使用 pathlib 模組,應該使用 Python3.6 或者更高版本!

和 os 功能對應的方法列表

先看一下 os (os.path) 模組裡部分函式與 pathlib.Path 對應的方法吧。下面列出的這些可以直接用 pathlib 裡面的用法代替:

舉 2 個例子:

# 原來的寫法
In : os.path.isdir(os.getcwd())
Out: True
# 新的寫法
In : Path.cwd().is_dir()
Out: True
# 原來的寫法
In : os.path.basename('/usr/local/etc/mongod.conf')
Out: 'mongod.conf'
# 新的寫法
In : Path('/usr/local/etc/mongod.conf').name
Out: 'mongod.conf'

接著感受下 pathlib 帶來的變化。

/ 拼接路徑

過去路徑拼接最正確的方法是用 os.path.join :

In : os.path.join('/', 'home', 'dongwm/code')
Out: '/home/dongwm/code'
In : os.path.join('/home', 'dongwm/code')
Out: '/home/dongwm/code'

現在可以用 pathlib.Path 提供的 joinpath 來拼接:

In : Path('/').joinpath('home', 'dongwm/code')
Out: PosixPath('/home/dongwm/code')

但是更簡單和方便的方法是用 / 運算子:

In : Path('/') / 'home' / 'dongwm/code'
Out: PosixPath('/home/dongwm/code')
In : Path('/') / Path('home') / 'dongwm/code'
Out: PosixPath('/home/dongwm/code')
In : '/' / Path('home') / 'dongwm/code'
Out: PosixPath('/home/dongwm/code')

這也不是什麼神奇魔法,有興趣的可以看 Path 物件 __truediv____rtruediv__ 方法的實現。

鏈式呼叫

鏈式呼叫是 OOP 帶來的重要改變,從前面的例子中也能感受到,過去的都是把路徑作為函式引數傳進去,現在可以鏈式呼叫。我們看一個更復雜一點的例子:

In : os.path.isfile(os.path.join(os.path.expanduser('~/lyanna'), 'config.py'))
Out: True

長不長?現在的寫法呢:

In : (Path('~/lyanna').expanduser() / 'config.py').is_file()
Out: True

是不是非常符合我們從左向右的閱讀習慣呢?

自帶屬性

Path 物件帶了多個有用的屬性

parent/parents

如果想獲得某個檔案的父級目錄通常需要使用 os.path.dirname 或者字串的 rpartition (或 split) 方法:

In : p = '/Users/dongweiming/test'In : p.rpartition('/')[0]
Out: '/Users/dongweiming'
In : p.rsplit('/', maxsplit=1)[0]
Out: '/Users/dongweiming'
In : os.path.dirname(p)
Out: '/Users/dongweiming'

如果想獲得父級的父級更麻煩一些,例如用 os.path.dirname ,需要這樣:

In : from os.path import dirnameIn : dirname(dirname(p))
Out: '/Users'

使用 Path 物件的 parents 屬性可以拿到各級目錄列表 (索引值越大越接近 root),而 parent 就表示父級目錄:

In : p = Path('/Users/dongweiming/test')In : p.parents[0]
Out: PosixPath('/Users/dongweiming')
In : p.parents[1]
Out: PosixPath('/Users')
In : p.parents[2]
Out: PosixPath('/')
In : p.parent
Out: PosixPath('/Users/dongweiming')
In : p.parent.parent
Out: PosixPath('/Users')

由於 parent 返回的還是 Path 物件,所以可以鏈式的獲取其 parent 屬性。

suffix/stem

在過去獲得檔案字尾名,以及去掉字尾的檔名字,需要使用 os.path.basenameos.path.splitext :

In : base = os.path.basename('/usr/local/etc/my.cnf')In : base
Out: 'my.cnf'
In : stem, suffix = os.path.splitext(base)In : stem, suffix
Out: ('my', '.cnf')

現在就很方便了:

In : p = Path('/usr/local/etc/my.cnf')In : p.suffix, p.stem
Out: ('.cnf', 'my')

注意:當檔案有多個字尾,可以用 suffixes 返回檔案所有後綴列表:

In : Path('my.tar.bz2').suffixes
Out: ['.tar', '.bz2']
In : Path('my.tar').suffixes
Out: ['.tar']
In : Path('my').suffixes
Out: []

實用方法

Path 物件裡面有多個實用的方法,我舉例一些。

touch 方法

Python 語言沒有內建建立檔案的方法 (linux 下的 touch 命令),過去這麼做:

with open('new.txt', 'a') as f:
...

現在可以直接用 Path 的 touch 方法:

Path('new.txt').touch()

touch 接受 mode 引數,能夠在建立時確認檔案許可權,還能通過 exist_ok 引數方式確認是否可以重複 touch (預設可以重複建立,會更新檔案的 mtime)

home

獲得使用者的 HOME 目錄比較常見,過去的寫法:

In : os.path.expanduser('~')
Out: '/Users/dongweiming'

現在就 Path.home 就可以了:

In : Path.home()
Out: PosixPath('/Users/dongweiming')

讀寫檔案

Path 物件自帶了操作檔案的方法:

In : p = Path('~/1.txt').expanduser()In : p.write_text('123\n')
Out: 4
In : p.read_text()
Out: '123\n'
In : p.write_bytes(b'456\n')
Out: 4
In : p.read_bytes()
Out: b'456\n'
In : with p.open() as f:
...: for line in f:
...: print(line)
...:
456

可以用 write_text 寫入字串,用 write_bytes 將檔案以二進位制模式開啟寫入位元組資料。對應的,可以用 read_text 讀取文字內容,也可以用 read_bytes 以位元組物件的形式返回路徑指向的檔案的二進位制內容。還可以用 open 獲得檔案控制代碼。

不過需要注意,Path 裡面帶的這幾個方法只是一些「快捷方式」,「一次性的」。舉個例子:

In : p = Path('~/1.txt').expanduser()In : p.read_text()
Out: '456\n'
In : p.write_text('789\n')
Out: 4
In : p.write_text('1011\n')
Out: 5
In : p.read_text()
Out: '1011\n'

可以看到,多次寫入最終結果是最後一次寫入的內容。而讀取也沒有快取區,全部讀取出來。其實讀一下原始碼即能理解:

In [96]: p.read_text??
Signature: p.read_text(encoding=None, errors=None)
Source:
def read_text(self, encoding=None, errors=None):
"""
Open the file in text mode, read it, and close the file.
"""
with self.open(mode='r', encoding=encoding, errors=errors) as f:
return f.read()
File: /usr/local/lib/python3.7/pathlib.py
Type: method
In [97]: p.write_text??
Signature: p.write_text(data, encoding=None, errors=None)
Source:
def write_text(self, data, encoding=None, errors=None):
"""
Open the file in text mode, write to it, and close the file.
"""
if not isinstance(data, str):
raise TypeError('data must be str, not %s' %
data.__class__.__name__)
with self.open(mode='w', encoding=encoding, errors=errors) as f:
return f.write(data)
File: /usr/local/lib/python3.7/pathlib.py
Type: method

** 在實際工作中這些方法要謹慎使用!``

with_name/with_suffix

以前我寫過一些修改檔名字或者路徑字尾的需求:基於某個檔案路徑生成另外一個檔案路徑。舉個例子,有一個檔案地址 '/home/gentoo/screenshot/abc.jpg' ,2 個需求:

過去需要這麼做:

In : p = '/home/gentoo/screenshot/abc.jpg'In : '{}.png'.format(os.path.splitext(p)[0])
Out: '/home/gentoo/screenshot/abc.png'
In : root, ext = os.path.splitext(p)In : '{}/{}{}'.format(root.rpartition('/')[0], 123, ext)
Out: '/home/gentoo/screenshot/123.jpg'

可讀性很差。現在呢:

In : p = Path('/home/gentoo/screenshot/abc.jpg')In : p.with_suffix('.png')
Out: PosixPath('/home/gentoo/screenshot/abc.png')
In : p.with_name(f'123{p.suffix}')
Out: PosixPath('/home/gentoo/screenshot/123.jpg')

mkdir

過去建立目錄時,用 os.mkdir 只能建立一級目錄:

In : os.mkdir('1/2/3')
---------------------------------------------------------------------------
FileNotFoundError Traceback (most recent call last)
<ipython-input-160-71cc3a9a36b4> in <module>
----> 1 os.mkdir('1/2/3')
FileNotFoundError: [Errno 2] No such file or directory: '1/2/3'

所以通常這種一次要建立多級目錄,需要用到 os.makedirs ,我一直覺得這麼搞很分裂。在 Path 對應上有 mkdir 方法,還接受 parents ,以及 modeexist_ok 引數:

In : Path('1/2/3').mkdir()
---------------------------------------------------------------------------
FileNotFoundError Traceback (most recent call last)
<ipython-input-163-202ce4b81bf9> in <module>
----> 1 Path('1/2/3').mkdir()
/usr/local/Cellar/python/3.7.3/Frameworks/Python.framework/Versions/3.7/lib/python3.7/pathlib.py in mkdir(self, mode, parents, exist_ok)
1249 self._raise_closed()
1250 try:
-> 1251 self._accessor.mkdir(self, mode)
1252 except FileNotFoundError:
1253 if not parents or self.parent == self:
FileNotFoundError: [Errno 2] No such file or directory: '1/2/3'In : Path('1/2/3').mkdir(parents=True)In : !tree 1/
1/
└── 2
└── 3
2 directories, 0 files

我認為這麼用的體驗著實好了很多~

owner

有時候操作檔案前需要確認擁有此檔案的使用者,過去我都是這麼寫:

In : import pwdIn : pwd.getpwuid(os.stat('/usr/local/etc/my.cnf').st_uid).pw_name
Out: 'dongweiming'

現在封裝起來可以直接用了:

In : p.owner()
Out: 'dongweiming'

後記

以上就是我使用的體驗了, pathlib 可以說是 Python 3.6 + 的正確之選~

[轉錄]
你應該使用pathlib替代os.path

--

--