PEP 428 – pathlib 모듈 – 객체 지향 파일 시스템 경로
- Author:
- Antoine Pitrou <solipsis at pitrou.net>
- Status:
- Final
- Type:
- Standards Track
- Created:
- 30-Jul-2012
- Python-Version:
- 3.4
- Post-History:
- 05-Oct-2012
- Resolution:
- Python-Dev message
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 PEP는 서드파티 모듈인 pathlib을 표준 라이브러리에 포함할 것을 제안합니다. 이 포함은 PEP 411에 설명된 대로 잠정(provisional) 레이블 아래에서 제안됩니다. 따라서 API 변경은 PEP 과정의 일부로서, 또는 표준 라이브러리에 채택된 후에(그리고 잠정 레이블이 제거되기 전까지) 이루어질 수 있습니다.
이 라이브러리의 목적은 파일 시스템 경로와 사용자가 경로에 대해 수행하는 일반적인 연산을 다루기 위한 간단한 클래스 계층 구조를 제공하는 것입니다.
관련 작업
파일 시스템 경로를 위한 객체 지향 API는 이미 PEP 355에서 제안되었다가 거부된 바 있습니다. 객체 지향 파일 시스템 경로라는 아이디어를 구현한 서드파티 구현이 이미 여럿 존재합니다:
- Jason Orendorff, Jason R. Coombs 등이 만든 오래된 path.py module은
str을 서브클래싱하는Path클래스를 제공합니다; - Twisted의 약간 특화된 FilePath class;
str이 아닌tuple을 서브클래싱하는 AlternativePathClass proposal;- Unipath: str 서브클래싱 방식의 변형으로, 두 개의 공개 클래스, 즉 I/O를 수행하지 않는 연산을 위한
AbstractPath클래스와 모든 일반적인 연산을 위한Path클래스를 제공합니다.
이 제안은 이러한 이전 시도들과 PEP 355의 거부로부터 교훈을 얻고자 합니다.
구현
이 제안의 구현은 pathlib의 Mercurial repository에 있는 pep428 브랜치에서 추적됩니다.
왜 객체 지향 API인가
파일 시스템 경로를 전용 클래스로 표현하는 근거는 날짜, 시각, IP 주소 같은 다른 종류의 상태 없는 객체의 경우와 같습니다. Python은 C 언어의 API를 엄격하게 그대로 따르던 방식에서 점차 벗어나, 온갖 일반적인 기능에 대해 더 낫고 더 유용한 추상화를 제공하는 방향으로 나아가고 있습니다. 이 PEP가 채택되지 않더라도, 언젠가는 다른 형태의 파일 시스템 처리 추상화가 표준 라이브러리에 채택될 가능성이 높습니다.
실제로 많은 사람이 숫자 타임스탬프와 time 모듈 API를 사용하기보다는 datetime 모듈이 제공하는 고수준 객체를 사용해 날짜와 시각을 다루는 쪽을 선호할 것입니다. 더욱이 전용 클래스를 사용하면 Windows 경로의 대소문자 비구분처럼 바람직한 동작을 기본으로 활성화할 수 있습니다.
제안
클래스 계층 구조
pathlib 모듈은 간단한 클래스 계층 구조를 구현합니다:
+----------+
| |
---------| PurePath |--------
| | | |
| +----------+ |
| | |
| | |
v | v
+---------------+ | +-----------------+
| | | | |
| PurePosixPath | | | PureWindowsPath |
| | | | |
+---------------+ | +-----------------+
| v |
| +------+ |
| | | |
| -------| Path |------ |
| | | | | |
| | +------+ | |
| | | |
| | | |
v v v v
+-----------+ +-------------+
| | | |
| PosixPath | | WindowsPath |
| | | |
+-----------+ +-------------+
이 계층 구조는 경로 클래스를 두 가지 차원으로 나눕니다:
- 경로 클래스는 순수(pure) 클래스이거나 구체(concrete) 클래스일 수 있습니다: 순수 클래스는 실제 I/O를 수행할 필요가 없는 연산, 즉 대부분의 경로 조작 연산만 지원합니다; 구체 클래스는 순수 클래스의 모든 연산에 더해 I/O를 수행하는 연산도 지원합니다.
- 경로 클래스는 자신이 나타내는 운영 체제 경로의 종류에 따라 특정한 플레이버를 가집니다. pathlib은 두 가지 플레이버를 구현합니다. Windows 시스템에 구현된 파일 시스템 의미를 위한 Windows 경로와, 그 밖의 시스템을 위한 POSIX 경로입니다.
모든 순수 클래스는 어느 시스템에서나 인스턴스화할 수 있습니다: 예를 들어 Windows에서 PurePosixPath 객체를, Unix에서 PureWindowsPath 객체를 조작하는 등의 일이 가능합니다. 그러나 구체 클래스는 일치하는 시스템에서만 인스턴스화할 수 있습니다: 실제로 Unix에서 WindowsPath 객체로 I/O를 수행하기 시작하거나 그 반대로 하는 것은 오류를 일으키기 쉬울 것입니다.
또한 시스템에 따라 달라지는 팩토리 역할도 하는 베이스 클래스가 두 개 있습니다: PurePath는 운영 체제에 따라 PurePosixPath 또는 PureWindowsPath를 인스턴스화합니다. 마찬가지로 Path는 PosixPath 또는 WindowsPath를 인스턴스화합니다.
대부분의 경우 Path 클래스를 사용하는 것으로 충분할 것으로 예상되며, 이 클래스의 이름이 모든 클래스 중 가장 짧은 것도 그 때문입니다.
내장 타입과의 혼동 없음
이 제안에서 경로 클래스는 내장 타입을 상속하지 않습니다. 이는 str을 상속했던 다른 몇몇 Path 클래스 제안과 대조됩니다. 또한 경로 클래스는 시퀀스 프로토콜을 구현하는 척하지도 않습니다: 경로를 시퀀스처럼 다루고 싶다면 전용 속성(parts 속성)을 조회해야 합니다.
str을 상속하지 않는 핵심 이유는 경로를 나타내는 문자열과 경로를 나타내지 않는 문자열 사이의 연산(예: path + an_accident)을 실수로 수행하는 것을 막기 위해서입니다. 문자열과의 연산이 반드시 유효하거나 예상한 파일 시스템 경로로 이어지는 것은 아니므로, 문자열을 서브클래싱하지 않음으로써 문자열과의 우발적인 연산을 피하는 것이 “명시적인 것이 암시적인 것보다 낫다”는 원칙에 부합합니다. Python 코어 개발자가 작성한 blog post에서 이 특정한 설계 결정의 이유를 더 자세히 다룹니다.
불변성
경로 객체는 불변이므로 해시 가능하며, 특정 부류의 프로그래밍 오류도 방지합니다.
합리적인 동작
os.path의 기능은 거의 재사용하지 않습니다. 많은 os.path 함수는 하위 호환성 때문에 혼란스럽거나 명백히 잘못된 동작에 묶여 있습니다(예를 들어 os.path.abspath()가 심볼릭 링크를 먼저 해석하지 않고 “..” 경로 구성 요소를 단순화한다는 점).
비교
같은 플레이버의 경로는 순수 경로인지 여부와 관계없이 비교 및 정렬이 가능합니다:
>>> PurePosixPath('a') == PurePosixPath('b')
False
>>> PurePosixPath('a') < PurePosixPath('b')
True
>>> PurePosixPath('a') == PosixPath('a')
True
Windows 경로 객체의 비교와 정렬은 대소문자를 구분하지 않습니다:
>>> PureWindowsPath('a') == PureWindowsPath('A')
True
서로 다른 플레이버의 경로는 항상 같지 않은 것으로 비교되며, 정렬할 수 없습니다:
>>> PurePosixPath('a') == PureWindowsPath('a')
False
>>> PurePosixPath('a') < PureWindowsPath('a')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unorderable types: PurePosixPath() < PureWindowsPath()
경로는 내장 타입(예: str)의 인스턴스 및 그 밖의 모든 타입과 같지 않은 것으로 비교되며, 이들과 함께 정렬할 수 없습니다.
유용한 표기법
이 API는 마법 같은 동작을 피하면서도 유용한 표기법을 제공하고자 합니다. 몇 가지 예는 다음과 같습니다.:
>>> p = Path('/home/antoine/pathlib/setup.py')
>>> p.name
'setup.py'
>>> p.suffix
'.py'
>>> p.root
'/'
>>> p.parts
('/', 'home', 'antoine', 'pathlib', 'setup.py')
>>> p.relative_to('/home/antoine')
PosixPath('pathlib/setup.py')
>>> p.exists()
True
순수 경로 API
PurePath API의 철학은 os.path처럼 함수를 뒤죽박죽으로 노출하지 않으면서, 유용한 경로 조작 연산의 일관된 배열을 제공하는 것입니다.
정의
먼저 몇 가지 규약을 정합니다:
- 모든 경로는 드라이브와 루트를 가질 수 있습니다. POSIX 경로의 경우 드라이브는 항상 비어 있습니다.
- 상대 경로에는 드라이브도 루트도 없습니다.
- POSIX 경로는 루트가 있으면 절대 경로입니다. Windows 경로는 드라이브와 루트를 모두 가지고 있어야 절대 경로입니다. Windows UNC 경로(예:
\\host\share\myfile.txt)는 항상 드라이브와 루트를 가집니다(여기서는 각각\\host\share와\입니다). - 드라이브 또는 루트 중 하나를 가진 경로를 앵커가 있는 경로라고 합니다. 경로의 앵커는 드라이브와 루트를 이어 붙인 것입니다. POSIX에서는 “앵커가 있는”이 “절대”와 같은 의미입니다.
생성
생성과 결합은 비슷한 의미 체계를 드러내므로 함께 설명하겠습니다.
경로를 생성하는 가장 간단한 방법은 경로의 문자열 표현을 전달하는 것입니다:
>>> PurePath('setup.py')
PurePosixPath('setup.py')
불필요한 경로 구분자와 "." 구성 요소는 제거됩니다:
>>> PurePath('a///b/c/./d/')
PurePosixPath('a/b/c/d')
여러 인자를 전달하면 자동으로 결합됩니다:
>>> PurePath('docs', 'Makefile')
PurePosixPath('docs/Makefile')
결합 의미 체계는 앵커가 있는 경로가 앞서 결합된 구성 요소의 정보를 무시한다는 점에서 os.path.join과 비슷합니다:
>>> PurePath('/etc', '/usr', 'bin')
PurePosixPath('/usr/bin')
하지만 Windows 경로에서는 필요에 따라 드라이브가 유지됩니다:
>>> PureWindowsPath('c:/foo', '/Windows')
PureWindowsPath('c:/Windows')
>>> PureWindowsPath('c:/foo', 'd:')
PureWindowsPath('d:')
또한 경로 구분자는 플랫폼 기본값으로 정규화됩니다:
>>> PureWindowsPath('a/b') == PureWindowsPath('a\\b')
True
불필요한 경로 구분자와 "." 구성 요소는 제거되지만, ".." 구성 요소는 제거되지 않습니다:
>>> PurePosixPath('a//b/./c/')
PurePosixPath('a/b/c')
>>> PurePosixPath('a/../b')
PurePosixPath('a/../b')
여러 개의 선행 슬래시는 경로 유형에 따라 다르게 처리됩니다. Windows 경로에서는 (UNC 표기법 때문에) 항상 유지됩니다:
>>> PureWindowsPath('//some/path')
PureWindowsPath('//some/path/')
POSIX에서는 선행 슬래시가 정확히 두 개인 경우를 제외하면 하나로 합쳐집니다. 두 개인 경우는 pathname resolution에 관한 POSIX 명세에서 특별히 다루는 경우입니다(Cygwin 호환성을 위해서도 필요합니다):
>>> PurePosixPath('///some/path')
PurePosixPath('/some/path')
>>> PurePosixPath('//some/path')
PurePosixPath('//some/path')
인자 없이 생성자를 호출하면 논리적인 “현재 디렉터리”를 가리키는 경로 객체가 생성됩니다(절대 경로를 조회하지는 않으며, 그것은 구체 경로의 cwd() 클래스 메서드가 할 일입니다):
>>> PurePosixPath()
PurePosixPath('.')
표현
경로를 표현하려면(예: 서드파티 라이브러리에 전달하려면) 경로에 대해 str()을 호출하기만 하면 됩니다:
>>> p = PurePath('/home/antoine/pathlib/setup.py')
>>> str(p)
'/home/antoine/pathlib/setup.py'
>>> p = PureWindowsPath('c:/windows')
>>> str(p)
'c:\\windows'
문자열 표현에서 슬래시(forward slash)를 강제로 사용하려면 as_posix() 메서드를 사용하십시오:
>>> p.as_posix()
'c:/windows'
바이트 표현을 얻으려면(Unix 시스템에서 유용할 수 있습니다) 경로에 대해 bytes()를 호출하십시오. 이는 내부적으로 os.fsencode()를 사용합니다:
>>> bytes(p)
b'/home/antoine/pathlib/setup.py'
경로를 file: URI로 표현하려면 as_uri() 메서드를 호출하십시오:
>>> p = PurePosixPath('/etc/passwd')
>>> p.as_uri()
'file:///etc/passwd'
>>> p = PureWindowsPath('c:/Windows')
>>> p.as_uri()
'file:///c:/Windows'
경로의 repr()은 가독성을 위해, 그리고 슬래시를 써도 괜찮다는 점을 사용자에게 상기시키기 위해 Windows에서도 항상 슬래시를 사용합니다:
>>> p = PureWindowsPath('c:/Windows')
>>> p
PureWindowsPath('c:/Windows')
속성
모든 경로에는 몇 가지 간단한 속성이 제공됩니다(각 속성은 비어 있을 수 있습니다):
>>> p = PureWindowsPath('c:/Downloads/pathlib.tar.gz')
>>> p.drive
'c:'
>>> p.root
'\\'
>>> p.anchor
'c:\\'
>>> p.name
'pathlib.tar.gz'
>>> p.stem
'pathlib.tar'
>>> p.suffix
'.gz'
>>> p.suffixes
['.tar', '.gz']
새 경로 파생
결합
/ 연산자를 사용하여 경로를 다른 경로와 결합할 수 있습니다:
>>> p = PurePosixPath('foo')
>>> p / 'bar'
PurePosixPath('foo/bar')
>>> p / PurePosixPath('bar')
PurePosixPath('foo/bar')
>>> 'bar' / p
PurePosixPath('bar/foo')
생성자와 마찬가지로 여러 경로 구성 요소를 한데 묶어서 지정하거나 따로따로 지정할 수 있습니다:
>>> p / 'bar/xyzzy'
PurePosixPath('foo/bar/xyzzy')
>>> p / 'bar' / 'xyzzy'
PurePosixPath('foo/bar/xyzzy')
같은 동작을 하는 joinpath() 메서드도 제공됩니다:
>>> p.joinpath('Python')
PurePosixPath('foo/Python')
경로의 마지막 구성 요소 변경하기
with_name() 메서드는 이름이 변경된 새 경로를 반환합니다:
>>> p = PureWindowsPath('c:/Downloads/pathlib.tar.gz')
>>> p.with_name('setup.py')
PureWindowsPath('c:/Downloads/setup.py')
경로에 실제 이름이 없으면 ValueError를 발생시키며 실패합니다:
>>> p = PureWindowsPath('c:/')
>>> p.with_name('setup.py')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "pathlib.py", line 875, in with_name
raise ValueError("%r has an empty name" % (self,))
ValueError: PureWindowsPath('c:/') has an empty name
>>> p.name
''
with_suffix() 메서드는 접미사가 변경된 새 경로를 반환합니다. 하지만 경로에 접미사가 없으면 새 접미사가 추가됩니다:
>>> p = PureWindowsPath('c:/Downloads/pathlib.tar.gz')
>>> p.with_suffix('.bz2')
PureWindowsPath('c:/Downloads/pathlib.tar.bz2')
>>> p = PureWindowsPath('README')
>>> p.with_suffix('.bz2')
PureWindowsPath('README.bz2')
경로를 상대 경로로 만들기
relative_to() 메서드는 한 경로와 다른 경로 사이의 상대적 차이를 계산합니다:
>>> PurePosixPath('/usr/bin/python').relative_to('/usr')
PurePosixPath('bin/python')
메서드가 의미 있는 값을 반환할 수 없으면 ValueError가 발생합니다:
>>> PurePosixPath('/usr/bin/python').relative_to('/etc')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "pathlib.py", line 926, in relative_to
.format(str(self), str(formatted)))
ValueError: '/usr/bin/python' does not start with '/etc'
시퀀스 방식의 접근
parts 프로퍼티는 경로의 구성 요소에 읽기 전용 시퀀스로 접근할 수 있게 해 주는 튜플을 반환합니다:
>>> p = PurePosixPath('/etc/init.d')
>>> p.parts
('/', 'etc', 'init.d')
Windows 경로는 드라이브와 루트를 하나의 경로 구성 요소로 취급합니다:
>>> p = PureWindowsPath('c:/setup.py')
>>> p.parts
('c:\\', 'setup.py')
(C:는 C:\\의 부모가 아니므로 둘을 분리하는 것은 잘못입니다).
parent 프로퍼티는 경로의 논리적 부모를 반환합니다:
>>> p = PureWindowsPath('c:/python33/bin/python.exe')
>>> p.parent
PureWindowsPath('c:/python33/bin')
parents 프로퍼티는 경로의 논리적 조상들로 이루어진 불변 시퀀스를 반환합니다:
>>> p = PureWindowsPath('c:/python33/bin/python.exe')
>>> len(p.parents)
3
>>> p.parents[0]
PureWindowsPath('c:/python33/bin')
>>> p.parents[1]
PureWindowsPath('c:/python33')
>>> p.parents[2]
PureWindowsPath('c:/')
조회하기
is_relative()는 경로가 상대 경로이면(위의 정의 참조) True를, 그렇지 않으면 False를 반환합니다.
is_reserved()는 Windows 경로가 CON이나 NUL 같은 예약된 경로이면 True를 반환합니다. POSIX 경로에 대해서는 항상 False를 반환합니다.
match()는 경로를 glob 패턴과 대조합니다. 개별 구성 요소 단위로 동작하며 오른쪽부터 대조합니다:
>>> p = PurePosixPath('/usr/bin')
>>> p.match('/usr/b*')
True
>>> p.match('usr/b*')
True
>>> p.match('b*')
True
>>> p.match('/u*')
False
이 동작은 다음과 같은 기대를 충족합니다:
- “*.py” 같은 단순한 패턴은 마지막 구성 요소만 일치하면 임의의 길이의 경로와 일치합니다. 예를 들어 “/usr/foo/bar.py”가 그렇습니다.
- 더 복잡한 대조를 위해 더 긴 패턴도 사용할 수 있습니다. 예를 들어 “/usr/foo/*.py”는 “/usr/foo/bar.py”와 일치합니다.
구체 경로 API
구체 경로는 순수 API의 연산 외에도, 실제로 파일 시스템에 접근하여 정보를 조회하거나 변경하는 추가 메서드를 제공합니다.
생성하기
클래스 메서드 cwd()는 현재 작업 디렉터리를 절대 경로 형태로 가리키는 경로 객체를 생성합니다:
>>> Path.cwd()
PosixPath('/home/antoine/pathlib')
파일 메타데이터
stat()은 파일의 stat() 결과를 반환합니다. 마찬가지로 lstat()은 파일의 lstat() 결과를 반환합니다(파일이 심볼릭 링크인 경우에만 결과가 다릅니다):
>>> p.stat()
posix.stat_result(st_mode=33277, st_ino=7483155, st_dev=2053, st_nlink=1, st_uid=500, st_gid=500, st_size=928, st_atime=1343597970, st_mtime=1328287308, st_ctime=1343597964)
더 고수준의 메서드는 파일의 종류를 검사하는 데 도움을 줍니다:
>>> p.exists()
True
>>> p.is_file()
True
>>> p.is_dir()
False
>>> p.is_symlink()
False
>>> p.is_socket()
False
>>> p.is_fifo()
False
>>> p.is_block_device()
False
>>> p.is_char_device()
False
파일 소유자와 그룹의 이름(숫자 id가 아닌)은 해당 메서드를 통해 조회합니다:
>>> p = Path('/etc/shadow')
>>> p.owner()
'root'
>>> p.group()
'shadow'
경로 해석
resolve() 메서드는 경로를 절대 경로로 만들며, 그 과정에서 모든 심볼릭 링크를 해석합니다(POSIX realpath() 호출과 같습니다). 이것은 “..” 경로 구성 요소를 제거하는 유일한 연산입니다. Windows에서는 이 메서드가 (대소문자가 올바른) 정규 경로를 반환하는 일도 처리합니다.
디렉터리 순회
단순한(재귀적이지 않은) 디렉터리 접근은 iterdir() 메서드를 호출하여 수행하며, 이 메서드는 자식 경로에 대한 이터레이터를 반환합니다:
>>> p = Path('docs')
>>> for child in p.iterdir(): child
...
PosixPath('docs/conf.py')
PosixPath('docs/_templates')
PosixPath('docs/make.bat')
PosixPath('docs/index.rst')
PosixPath('docs/_build')
PosixPath('docs/_static')
PosixPath('docs/Makefile')
이를 통해 리스트 컴프리헨션으로 간단히 필터링할 수 있습니다:
>>> p = Path('.')
>>> [child for child in p.iterdir() if child.is_dir()]
[PosixPath('.hg'), PosixPath('docs'), PosixPath('dist'), PosixPath('__pycache__'), PosixPath('build')]
단순 글로빙과 재귀 글로빙도 제공됩니다:
>>> for child in p.glob('**/*.py'): child
...
PosixPath('test_pathlib.py')
PosixPath('setup.py')
PosixPath('pathlib.py')
PosixPath('docs/conf.py')
PosixPath('build/lib/pathlib.py')
파일 열기
open() 메서드는 내장 open() 메서드와 비슷한 파일 열기 API를 제공합니다:
>>> p = Path('setup.py')
>>> with p.open() as f: f.readline()
...
'#!/usr/bin/env python3\n'
파일 시스템 수정
몇 가지 일반적인 파일 시스템 연산이 메서드로 제공됩니다: touch(), mkdir(), rename(), replace(), unlink(), rmdir(), chmod(), lchmod(), symlink_to(). 예를 들어 shutil 모듈 기능의 일부처럼 더 많은 연산을 제공할 수도 있습니다.
제안된 API의 자세한 문서는 pathlib docs에서 찾을 수 있습니다.
논의
나눗셈 연산자
경로 결합 연산자에 관한 poll에서 나눗셈 연산자가 1위를 차지했습니다. pathlib의 초기 버전은 그 대신 대괄호(즉, __getitem__)를 사용했습니다.
joinpath()
joinpath() 메서드는 처음에 join()이라고 불렸지만, 의미가 다른 str.join()과 혼동될 수 있다며 여러 사람이 반대했습니다. 그래서 joinpath()로 이름이 바뀌었습니다.
대소문자 구분
드물게 Windows에서 일부 외부 파일 시스템 마운트가 대소문자를 구분하는 경우가 있기는 하지만, Windows 사용자는 파일 시스템 경로가 대소문자를 구분하지 않는다고 여기며 경로 객체도 그 특성을 따르기를 기대합니다.
한 의견 제시자의 말을 빌리면 다음과 같습니다.
“Windows에서 glob(”*.py”)가 SETUP.PY를 찾지 못한다면, 그것은 사용성 측면에서 재앙일 것입니다”.—Paul Moore, https://mail.python.org/pipermail/python-dev/2013-April/125254.html
Copyright
This document has been placed into the public domain.