콘텐츠로 건너뛰기
Home » 파이썬 pathlib 사용법

파이썬 pathlib 사용법

파이썬에서 파일 경로를 다루는 문제를 다루는 블로그 포스팅이나 지식iN 답변 같은 걸 종종 보게 되는데, 그 때마다 거의 os.path 모듈을 사용하는 코드를 소개하는 것이 많습니다. os.path 모듈은 파일 시스템에서 특정한 파일이나 디렉토리의 경로를 다루는데 특별히 부족한 점은 없지만, 그 자체로 너무 오래 되었고 경로 자체를 객체로 다루지 않고 단순 문자열로만 취급하다 보니 불편한 점이 있습니다.

파이썬 사용자들 사이에서는 파일 시스템의 경로를 좀 더 우아하게 다룰 수 있는 고수준 API에 대한 요구가 오래전부터 있어왔고, 이에 파이썬3.4에서 pathlib 이라는 모듈이 새롭게 추가되었습니다. pathlib은 파일 시스템 상의 경로를 객체로 정의하고 객체 지향적인 방법으로 경로를 다룰 수 있게 해주는 일련의 기능을 제공합니다.

pathlib은 Path라는 클래스를 사용해서 경로를 객체화하여 표현합니다. 경로 구분자를 비롯해서 윈도와 그외의 플랫폼은 경로와 관련한 몇 가지 차이가 있기 때문에, 이 Path라는 클래스는 윈도에서 실행되는 파이썬에서는 WindowsPath라는 클래스이고, 그 외의 플랫폼에서는 PosixPath라는 클래스로 작동합니다.

경로 구분자

파일 경로와 관련해서 사소한 것 같으면서도 신경을 긁는 부분은 바로 경로 구분자에 관한 것입니다. 리눅스나 macOS의 경우에는 경로 구분자로 슬래시(/)를 사용합니다만, 윈도에서는 경로 구분자로 역슬래시를 사용합니다. 보통 역슬래시는 문자열 내에서 이스케이프 시퀀스를 표현하는 문자로 사용되기 때문에, 윈도에서 파일 경로를 문자열로 표현할 때에는 “D:\\My Folder\\Homework\\Python\\s01″과 같은 식으로 역슬래시를 두 번 사용해야 합니다. 역슬래시를 두 번 사용해야 한다는 점도 실수하기 쉽지만, OS에 따라서 경로 구분자가 다르다는 것 역시 상당히 거슬립니다. 사실 파이썬에서 경로를 표현할 때에는 역슬래시를 쓰든, 슬래시를 쓰든 상관없습니다. 파이썬 내부에서 OS에 따라 적절하게 대응하여 사용할 수 있습니다. 이는 pathlib을 도입하기 이전부터 제공되던 기능입니다.

어쨌든 파이썬은 비록 윈도 플랫폼이라고 경로 구분자로 슬래시(/)를 사용해도 괜찮다고 인식하면 됩니다. 이렇게 경로 구분자를 문자열 내에서 슬래시로 통일하는 것은 여러 모로 편리한 점이 있는데, pathlib에서는 아예 / 를 경로를 결합하는 연산자로 사용할 수 있게 해줍니다. 두 개의 Path 객체나 Path 객체와 문자열 객체를 / 연산자로 연결하여 경로를 확장하는데 사용합니다. os.path 모듈을 사용할 때에는 os.path.join() 함수를 사용하기도 하는데, 이것보다는 훨씬 다루기가 편리합니다. (물론 Path에도 여러 개의 경로 조각을 한 번에 합치는 용도로 .join() 메소드를 제공합니다.)

pathlib에서 사용하는 경로구분자는 슬래시로 통일된다고 생각하면 편합니다. 슬래시는 단순히 문자열 내에서 경로 구분자로 사용되는 것 외에 경로 결합에 관한 연산자로도 사용할 수 있습니다. 즉 별개의 두 Path 객체나 Path 객체와 문자열을 편리하게 결합할 수 있습니다. os.path 모듈을 사용할 때에는 os.path.join() 을 사용하는데, 이것보다는 훨씬 다루기가 편리합니다.

d = Path("D:/My Folder/Homework")
f = d / "Python/s01"
f
# WindowsPath("D:/My Folder/Homework/Python/s01")

경로 만들기

pathlib에서는 Path 타입을 사용하여 경로를 만들 수 있습니다. 경로의 위치는 절대 경로나 상대 경로 어느 것을 사용해도 상관없습니다. 현재 디렉토리를 의미하는 . 을 사용하거나, 빈 입력을 넘겨주면 현재 작업 디렉토리에 대한 상대 경로의 Path 객체를 생성하게 됩니다.

혹은 현재 디렉토리에서의 특정 위치를 가리키는 상대 경로나 절대 경로를 넘겨서 경로 객체를 생성할 수 있습니다. 그 외에 기준이 되는 경로에 / 를 연산자처럼 사용해서 경로를 합성하는 것도 가능합니다. 이 연산은 굉장히 유연해서, / 를 반복적으로 사용해서 경로를 누적해서 합성할 수도 있고, p / 'path/to/somewhere' 과 같은 식으로 상대 경로를 표현하는 문자열만을 사용해서 한 번에 하위 경로를 합성할 수 있습니다. os.path.join()에 비해서 훨씬 간단하게 상위 경로와 하위 경로를 결합할 수 있습니다. 대신에 os.path.join() 함수는 여러 조각의 경로명을 하나로 합성할 수 있는데, Path.joinpath() 함수를 사용해서 여러 단계를 한 번에 합성하는 것도 가능합니다.

# 현재 위치
cwd = Path()  # Path('.')

# 현재 디렉토리 내의 파일
myfile = Path("myfile.txt")
myfile2 = Path("text/mytext.txt")

# 상위 디렉토리
parent = Path('..')

# 절대 경로를 사용한 경로
somefile = Path("D:/myfolder/mydata/text/myfile.txt")

# 특정 디렉토리 아래의 다른 경로 만들기
otherdir = somefile / '../../image/mypic.jpg'
#                   ^  ~~~~~~~~~~~~~~~~~~~~~~ 
#                   |                        \ 추가할 경로
#              경로 결합 연산자

상대 경로와 절대 경로

경로란 파일 시스템에서 특정한 파일이나 디렉토리의 위치를 지정하는 체계에 따른 주소입니다. 경로를 표현하는 방식에는 절대 경로와 상대 경로가 있는데, 이 둘의 차이는 출발점이 어디냐에 있습니다. 절대 경로는 최상위 위치로부터 시작되는 주소입니다. 일반적으로 우리가 사용하는 집주소도 가장 큰 단위로부터 시작하니 절대 경로와 같은 개념입니다. 상대 경로는 특정한 다른 위치 (주로 현재 위치)를 기준으로 하는 것입니다. 우리가 아파트에서 ‘윗집이 너무 시끄러워’와 같은 방식으로 다른 집을 가리킬 때에는 상대 위치를 사용하는 것이라고 보면 됩니다.

절대경로는 윈도의 경우에 드라이브 문자(“C:”, “D:” 등)로 시작하고, 리눅스나 macOS에서는 최상위 경로인 루트 디렉토리로부터 시작하기 때문에 슬래시(/)로부터 시작됩니다. 어떤 경로 객체가 상대경로인지 절대경로인지는 어디서부터 시작하는지를 보고 구분하게 됩니다. 아, 간혹 볼 수 있는 틸트(~)로부터 시작하는 경로도 절대 경로입니다. 윈도에서는 많이 쓰이지 않지만, 이 틸트는 현재 사용자의 홈 디렉토리를 표현하는데 사용됩니다.

Path 객체를 생성할 때 문자열의 형태로 경로값을 전달할 수 있는데, 이 때 전달하는 값이 절대 경로이든 상대경로이든 중요하지 않습니다. 상대 경로로 생성된 Path 객체는 absolute() 메소드를 사용하여 절대 경로로 만들 수 있습니다. 기본적으로 상대경로의 출발점은 현재 디렉토리(CWD: Current Working Directory, 명령줄에서 파이썬 스크립트를 실행한 위치)의 경로를 상대 경로 앞에 붙인 것일 뿐입니다.

# 현재 디렉토리가 D:/data/image 일 때,
d = Path('../text/myfile01.txt')
a = d.absolute()
# WindowsPath('D:/data/image/../text/myfile01.txt')

위 예제에서 보는 경로 a 는 “D:/data/image/../text/myfile01.txt”입니다. 이 경로는 약간 특이한데, image 디렉토리로 내려갔다가 다시 “..”를 만나서 상위 디렉토리 (D:/data)로 거슬러 올라온 다음에 다시 text/myfile01.txt로 내려가는 방식으로 되어 있습니다. (이건 마치 빠른 길을 두고 빙 돌아가는 것처럼 보이지만, 실제로는 의미있는 경로입니다. 심볼릭 링크가 중간에 끼어있는 경우에 이런 경로를 사용하기도 하는데, 이 글에서는 대충 넘어가죠.) 이를 최단 경로 표현으로 정리하기 위해서는 resolve() 메소드를 사용합니다. 경로 변환과 관련하여 메소드를 조금 정리해보면 다음과 같습니다.

  • absolute() : 상대 경로를 절대 경로로 변환합니다. 이는 간단히 현재 경로(Path.cwd)를 현재 객체 앞에 연결하는 것입니다. 만약 ../으로 시작하는 상대경로를 만들었다면 D:/myfiles/images/2022-12-12/../2022-12-13 과 같은 방식으로 경로가 생성될 것입니다.
  • resolve() : 경로 내부에 상위로 경로로 돌아가는 부분이 있는 경우, 이를 제거하고 가장 짧은 경로로 내부 경로를 정리한 형태를 보여줍니다. 예를 들어 “D:/myfiles/images/../text/myfile1.txt” 의 경우, “D:/myfiles/text/myfile1.txt”의 형태로 정리하게 됩니다.
  • as_posix : posix 경로 형태로 변경한 문자열 값을 나타내는 속성입니다. posix 형식으로 표현하는 경우, 윈도의 경로에서도 경로 구분자는 슬래시(/)를 사용합니다.
  • as_uri : file:/// 로 시작하는 URL 형식으로 파일의 경로를 표현합니다. 일부 프로그램이나 라이브러리는 로컬 디스크 상의 파일에 대해서도 URL의 형식으로 파일에 접근하는 것을 선호하는 것들이 있습니다.

경로의 구성 요소

파일 시스템의 경로는 상위구조 아래에 하위 구조가 연결되는 트리 구조를 가지고 있습니다. 경로가 가리키는 끝부분에는 파일이나 디렉토리의 이름(name)이 오게 됩니다. 이름의 바로 왼쪽에는 해당 파일/디렉토리의 상위 디렉토리 이름이 오며, 왼쪽으로 갈 수록 상위 단계의 이름이 옵니다. 경로를 포함하는 상위 단계를 부모(parent)라고 합니다. 이름과 부모는 각각 .name, .parent 라는 속성으로 접근할 수 있습니다. name 속성은 확장자를 포함하는 파일의 이름을 나타내는 문자열이며, parent 속성은 해당 경로의 부모 경로를 표현하는 Path 객체입니다.

  • name : 노드의 이름. 파일의 경우 확장자를 포함합니다.
  • stem : 파일 확장자를 제외한 이름
  • suffix : 노드가 파일인 경우 확장자. 예를 들어 “a.jpg”의 경우 suffix는 “.jpg”입니다. (구두점을 포함합니다.)
  • parent : 부모 노드를 가리킵니다.
  • parents : 바로 상위의 부모로부터 거슬러 올라갈 수 있는 상위 노드들을 반환할 수 있는 제너레이터입니다. 절대 경로인 경우에는 최상위 노드까지 포함합니다.
  • parts : 표현 가능한 최상위노드로부터 각각의 노드 이름이 part가 되며, 상위에서부터 순서대로 이름의 튜플을 리턴합니다. 이 값들은 Path.joinpath() 에 전달하여 하나의 패스 경로로 합성할 수 있습니다.
# 현재 경로가 D:/myfiles/images/2022-12-12/ 일 때,
p = Path('icons/file.png')
# D:/myfiles/images/2022-12-12/icons/file.png

p.name
# "file.png"

p.stem, p.suffix
# ("file", ".png") 

p.parent
# WindowsPath('icons')  # 상대경로이므로

q = p.parent.absolut()
q
# WindowsPath('D:/myfiles/images/2022-12-12/icons')

list(q.parents)
# [
#  WindowsPath('D:/myfiles/images/2022-12-12'),
#  WindowsPath('D:/myfiles/images'),
#  WindowsPath('D:/myfiles'),
#  WindowsPath('D:/'),
# ]

q.parts
# ('D:\\', 'myfiles', 'images', '2022-12-12', 'icons')

경로 검사

Path 객체는 경로에 대한 검사 수단들을 제공합니다. 몇 가지 인터페이스를 통해서 해당 경로가 실제로 존재하는지, 파일인지 디렉토리인지 등등을 알 수 있습니다. 주로 is_* 로 시작하는 메소드들이 경로 검사와 관계됩니다.

  • is_absolute() – 절대 경로 여부
  • is_dir() / is_file() – 디렉토리인지 / 파일인지
  • exists() – 실제로 존재하는 경로인지
  • is_mount() – 해당 경로가 마운트된 장치를 가리키는지
  • is_relative_to(*other) – 경로가 다른 경로에 대한 상대 경로인지
  • match(pattern) : 주어진 glob 패턴에 매치하는 경로인지

경로 조작

pathlib을 사용할 가장 큰 이유 중의 하나는 경로 조작이 편리하다는 것입니다. 어떤 파일에 대한 절대 경로를 가지고 있을 때, 파일의 이름만 바꾼 경로를 얻고 싶다면 어떻게 할까요? os.path 모듈을 사용할 때에는 다음과 같은 과정을 거쳐야 합니다.

  1. os.path.split() 을 사용하여 디렉토리 경로와 파일 이름을 분리
  2. 기존 파일이름에 대해서 split()을 사용하여 파일 이름(stem)과 확장자(suffix)를 분리
  3. os.path.join()을 사용하여 디렉토리 경로와 새 파일이름(새 이름과 확장자를 결합한)을 합침

Path 객체는 with_*() 류의 메소드를 사용하여 특정한 요소만 교체한 경로를 쉽게 만들 수 있습니다. 특히 파일 경로를 입력으로 받아서 어떤 처리를 수행한 후, 원본 파일의 이름 일부만 변경한 대상 파일에 기록하는 형태의 프로그램을 작성할 때 매우 편리합니다.

  • p.with_name("a.jpg") : 상위 경로는 그대로 두고 파일 이름만 변경한 새 경로
  • p.with_stem("new_stem") : 파일 이름부분(stem)만 변경한 새 경로
  • p.with_suffix(".png") : 확장자만 변경한 새 경로

경로가 가리키는 곳의 파일을 조작하기

pathlib의 기능은 단순히 경로값 자체를 다루는 것 외에도 해당 경로가 가리키는 곳의 파일이나 디렉토리를 조작하는 몇 가지 메소드를 추가로 제공합니다. open() 내장함수처럼 path.open() 메소드를 사용해서 해당 경로 파일을 읽기/쓰기를 위해 열 수 있습니다. 이 메소드는 open() 함수와 매우 유사하며, 컨텍스트 매니저로 구현되어 있기 때문에 with path.open() as f: 와 같이 with 구문으로 사용될 수 있습니다. 단순한 읽기나 쓰기를 위해서는 read_bytes(), read_text(), write_bytes(), write_text() 와 같은 메소드를 사용하여 특정 경로의 파일을 바로 읽거나 쓸 수 있습니다.

touch() 메소드는 경로가 가리키는 위치에 빈 파일을 생성합니다. (리눅스 쉘의 touch 명령과 동일합니다.) 디렉토리를 만들기 위해서는 mkdir() 메소드를 사용할 수 있습니다. mkdir(parents=True) 옵션을 사용하면 해당 경로의 부모나 부모의 부모가 없는 경우, 해당 경로 계층의 디렉토리를 한 번에 생성할 수 있습니다.

rename() 은 파일이나 디렉토리를 변경합니다. 이름은 rename() 이지만 실질적으로는 mv 명령과 동일합니다. 즉 파일이나 디렉토리의 이름만 바꾸는 것이 아니라 변경된 경로로 경로 전체를 바꾸기 때문에 파일이 위치도 바뀌게 됩니다. rename() 의 인자는 Path 객체이며, 원본 파일과 다른 위치를 가리킨다면 파일이 이동하니 주의해야 합니다. 파일 이름만 바꾸고 싶다면 path.rename(path.with_name('newname.txt')) 와 같이 기존 path 객체의 이름만 변경한 값을 전달해 주는 것이 좋습니다.

하위 경로 탐색

경로가 어떤 디렉토리를 가리킬 때, .iterdir() 은 디렉토리 내의 파일 및 서브 디렉토리를 순회하는 제너레이터를 리턴합니다. for 문을 돌면서 하위 파일 및 디렉토리의 목록에 대해 특정한 처리를 할 수 있습니다.

.glob() 는 glob 패턴이라 불리는 쉘에서 사용하는 패턴에 따라 특정 디렉토리 아래를 탐색합니다. *.jpg는 “.jpg”로 끝나는 모든 파일 (혹은 디렉토리)를 탐색합니다. glob 패턴은 하위 디렉토리 내부까지도 탐색이 가능합니다. **/*.jpg 는 현재 디렉토리 아래 및 하위의 모든 서브 디렉토리 계층을 탐색하여 원하는 패턴을 찾아 냅니다. 이 기능은 특히 연도별로 구분하여 정리해놓은 디렉토리 구조내에서 파일을 빠르게 찾아 사용할 수 있는 멋진 기능입니다.

예전에는 이와 비슷한 일을 하려면 os.walk() 함수를 사용했습니다. 참고로 os.walk() 는 특정한 디렉토리 하위의 구조를 탐색하면서 전체 파일 목록을 리턴하기 때문에 파일 이름의 패턴은 별도로 검사해야 합니다.

source_dir = Path('images')
dest_dir = Path('new_images/')
for imgfile in source_dir.glob('**/*.jpg'):
  imgfile.rename(dest_dir / imgfile.name)

# 
import os, os.path
source_dir = 'images'
dest_dir = 'new_images'

정리

pathlib은 os.path 라이브러리가 제공하는 기능을 고수준의 API로 만들어서 보다 편리하게 사용할 수 있도록 해줍니다. 현재는 파이썬 표준 라이브러리 내에서 파일 경로를 사용하는 대부분의 API 들은 경로를 “path-like” 객체로 받아들이도록 조정되어 있습니다. “Path-like” 한 객체는 Path 객체나 문자열 둘 다 받고 있다는 뜻입니다. 심지어 os.path 모듈의 모든 함수들도 현재는 path-like 객체를 지원하도록 되어 있습니다.

pathlib은 현재 파일 시스템과 관련한 대부분의 처리를 지원하고 있습니다. 단 파일이나 디렉토리를 복사하는 기능은 지원하지 않는데, 이는 파일 복사가 단순히 파일의 내용만 읽어서 다른 곳에 기록하는 것보다 더 많은 일을 해야하기 때문입니다. 이러한 기능은 fsutil 모듈을 사용합니다.(역시 이 모듈도 path-like 객체를 지원합니다.)