Namu | 나무 개발자 블로그입니다


파이썬에서 경로 다루기 by namu

post image
image by 729714

[순서]

  1. os.path vs pathlib
  2. os.path
  3. pathlib
  4. 파일 입출력하기
  5. 시스템에 작업 디렉토리 등록하기


os.path vs pathlib

파이썬으로 파일의 경로를 다룰 때는 os.path 혹은 pathlib 를 활용합니다. 후자는 경로를 다루는데 있어서 Path 객체를 중심으로 접근하는 방식으로, 파이썬 3.4 버전부터 빌트인 모듈로 포함되고 있습니다.

관점에 따라 다를 수 있지만, 현재는 프로그래머에게 있어서 객체지향 방식이 읽거나 활용하기 편하다는 의견이 많습니다. 저도 path = os.path.join(path1, path2) 이런 방식보다는 path = path1 / path2 와 같은 방식이 더 편합니다


os.path

일단 os 패키지는 운영체제 종속 기능을 사용하는 이식성 있는 여러 툴들을 제공합니다. 이 os 의 경로 관리 모듈이 os.path입니다. 위에서 언급했듯이 이것은 pathlib 가 본격적으로 사용되기 이전까지 일반적인 경로관리 방식이었습니다.

경로 결합, 절대경로 구하기

import os


my_dir = os.getcwd()  # current path. 'pwd' in posix system
my_file_name = 'my_file_name.txt'

file_path = os.path.join(my_dir, my_file_name)
print(file_path)  # /my/dir/my_file_name.txt

abs_file_path = os.path.abspath(file_path)
print(abs_file_path)  # /absolute/my/dir/my_file_name.txt

os.path.join() 함수는 넘겨받은 두 인자를 결합한 경로를 문자열로 반환합니다. 따라서 file_path 는 현재 작업경로에 있는 ‘my_file_name.txt’ 파일을 경로와 함께 나타냅니다.

os.path.abspath() 함수는 넘겨받은 경로 문자열에 대한 절대경로를 문자열로 반환합니다. 따라서 abs_file_pathfile_path 의 숨겨진 절대경로를 모두 나타냅니다. 만약 file_path 가 이미 절대경로라면 동일한 문자열을 반환하겠죠?

디렉토리 경로, 파일명 구하기

import os


my_dir = os.getcwd()  # current path. 'pwd' in posix system
my_file_name = 'my_file_name.txt'
file_path = os.path.join(my_dir, my_file_name)

dir_name = os.path.dirname(file_path)
print(dir_name)  # /my/dir/name/

file_name = os.path.basename(file_path)
print(file_name)  # my_file_name.txt

split_file = os.path.split(file_path)
print(type(split_file))  # <class 'tuple'>
print(split_file)  # ('/my/dir/name/', 'my_file_name.txt')

os.path.dirname() 함수는 입력받은 문자열의 디렉토리 경로를 문자열로 반환합니다. 여기서 만약 파일명이 포함되어 있다면 그것은 제외됩니다.

이와 반대로 os.path.basename() 함수는 디렉토리 경로를 제외하고 파일명만을 문자열로 반환합니다.

마지막으로 os.path.split() 함수는 입력받은 경로를 분리합니다. 분리 형태는 (head, tail) 형태의 튜플인데, tail 은 경로의 가장 마지막에 있는 요소입니다. 그리고 슬래시(/)를 포함하지 않기 때문에, 입력받은 경로가 슬래시로 끝난다면 tail 은 빈 값으로 출력됩니다. 반대로 입력받은 경로에 슬래시가 하나도 없다면 head 쪽이 빈 값으로 출력됩니다. head 는 tail 을 제외한 앞의 모든 요소입니다.


pathlib

이제 살펴볼 것은 Path 객체를 사용하는 pathlib 패키지입니다. 다음 설명은 pathlib 공식문서의 일부입니다.

이 모듈은 서로 다른 운영 시스템에 의미적으로 적합한 파일 시스템 경로를 나타내는 클래스를 제공합니다. Path 클래스는 I/O 작업 없이 순전히 (경로)계산만을 위한 pure paths 부분과, pure paths 를 상속하지만 I/O 작업을 제공하는 concrete paths 부분으로 나눠집니다.

Path class in pathlib

일단 기본적으로는 Path 클래스를 활용한다고 생각하면 좋습니다. 그럼 내부적으로 운영체제에 맞춰서 PosixPath 혹은 WindowsPath 객체가 사용됩니다.

경로 결합, 절대경로 구하기

from pathlib import Path


my_dir = Path.cwd()  # same as os.getcwd()
my_file_name = 'my_file_name.txt'

file_path = Path(my_dir, my_file_name)  # Path(my_dir / my_file_name)
print(file_path)  # /my/dir/my_file_name.txt
file_path = my_dir / my_file_name
print(file_path)  # /my/dir/my_file_name.txt

abs_file_path = Path.resolve(file_path)
print(abs_file_path)  # /absolute/my/dir/my_file_name.txt

Path 클래스를 인스턴스화 함으로써 원하는 경로를 조합할 수 있습니다. 또한 Path 클래스에 /(슬래시) 연산자를 사용해도 동일한 결과를 얻습니다. 이것은 경로를 조합한다는 면에서 os.path.join() 과 같은 효과를 발휘합니다. (연산자에 대한 자세한 설명은 Operators 를 참조하세요)

그리고 Path 객체에서 절대 경로를 구할때는 Path.resolve() 메서드를 사용합니다.

디렉토리 경로, 파일명 구하기

Path 객체의 디렉토리 경로를 구하기 위해 parent 어트리뷰트를 사용합니다. 만약 경로에 파일명이 포함되어 있다면 해당 파일의 디렉토리 경로가, 아니라면 입력된 디렉토리의 상위 디렉토리 경로가 반환됩니다.

from pathlib import Path


my_dir = Path.cwd()  # same as os.getcwd(), but returns a Path object
my_file_name = 'my_file_name.txt'
file_path = my_dir / my_file_name

parent_dir = file_path.parent
print(parent_dir)  # Print 'my_dir' path
print(parent_dir.parent)  # parent path of 'my_dir'

마지막 라인과 같이 parent 어트리뷰트를 반복 사용함으로써 더 상위 디렉토리의 경로를 얻을 수 있습니다.

파일명과 관련된 정보를 구하기 위해서는, name, stem, suffix 어트리뷰트를 사용합니다.

print(file_path.name)  # 'my_file_name.txt'
print(file_path.stem)  # 'my_file_name'
print(file_path.suffix)  # '.txt'

각각 순서대로 파일명, 확장자를 제외한 파일명, 확장자를 의미합니다.


파일 입출력하기

파이썬에서는 파일을 정확히 다루기 위해 앞서 설명한 경로 관련 툴들을 사용합니다. 이하에서는 os.pathpathlib 를 활용한 파일 다루기를 해보겠습니다. 편의를 위해 Posix 시스템에서 ‘/home/namu’ 디렉토리를 기반으로 하겠습니다.

os.path 활용

import os


my_dir = '/home/namu/'
my_file_name = 'my_file_name.txt'
file_path = os.path.join(my_dir, my_file_name)

# Check directory existence
if not os.path.exists(my_dir):
    print('Directory not exist. Create a new one.')
    os.mkdir(my_dir)

# Write file
with open(file_path, 'a', encoding='utf-8') as f:
    for i in range(5):
        f.write(f'[{i + 1}] new line!\n')

# Read file
with open(file_path, 'r', encoding='utf-8') as f:
    lines = f.readlines()
    for line in lines:
        print(line)

# Remove file
if os.path.exists(file_path):
    print('Removing file...')
    os.remove(file_path)
    print('Done.')

os.path.join() 함수로 경로를 조합한 뒤, 디렉토리 존재유무 체크, 파일 열기-쓰기-읽기 순으로 진행되었습니다.

파일을 열기 위해 사용된 빌트인 함수 open 은 조합된 문자열 경로 file_path 를 받습니다. 두 번째 인자 ‘a’ 는 ‘append’ 모드를 의미하며, ‘r’ 은 ‘read’ 모드입니다. ‘w’ 는 ‘write’ 모드로 사용됩니다. 그리고 세 번째 인자는 해당 파일을 ‘utf-8’ 으로 인코딩하여 open 합니다.

마지막 부분에서는 생성되었던 파일을 삭제하였습니다. 출력은 다음과 같습니다.

[1] new line!
[2] new line!
[3] new line!
[4] new line!
[5] new line!
Removing file...
Done.

만약 디렉토리가 없었다면, 새로 생성한다는 라인이 포함되었을 것입니다.

참조: with 키워드를 활용한 부분은 컨텍스트 매니저context manager 파트를 참조하세요.

pathlib 활용

from pathlib import Path


my_dir = Path('/home/namu/')   # Use Path object
my_file_name = 'my_file_name.txt'
file_path = my_dir / my_file_name

# Create directory if it doesn't exist.
my_dir.mkdir(parents=True, exist_ok=True)

# Write file
with file_path.open(mode='a', encoding='utf-8') as f:
    for i in range(5):
        f.write(f'[{i + 1}] new line!\n')

# Read file
with file_path.open(mode='r', encoding='utf-8') as f:
    lines = f.readlines()
    for line in lines:
        print(line)

# Remove file
if file_path.exists():
    print('Removing file...')
    file_path.unlink()
    print('Done.')

os.path 를 활용한 과정과 동일합니다. 다만 디렉토리 존재유무를 확인하는 부분에서 Path 객체의 내장 메서드 mkdir 은 넘거받는 인자에 따라 디렉토리 존재유무 확인과 디렉토리 생성을 한 번에 할 수 있도록 합니다. 또한 parents=True 는 상위 디렉토리까지 연쇄적으로 생성하도록 합니다.

파일을 열기 위해 사용하는 Path 의 메서드 open 은 빌트인 함수인 open 과 사실상 동일하게 동작합니다.

pathlib 를 활용한 코드의 출력은 다음과 같습니다.

[1] new line!
[2] new line!
[3] new line!
[4] new line!
[5] new line!
Removing file...
Done.


시스템에 작업 디렉토리 등록하기

파이썬 스크립트를 실행하기 위해서는 시스템에 작업 디렉토리의 경로가 등록되어 있어야 합니다. 코드 레벨에서 절대 경로로 스크립트를 실행한다면 시스템에 등록할 필요가 없겠지만, 일반적으로는 작업 디렉토리 이하 상대 경로나 파일명만을 가지고 스크립트를 실행하기 때문입니다. 상대 경로를 사용하는 이유는 등록된 작업 디렉토리에 위치한 다른 모듈들에 대한 import, 다양한 os 에서 적합하게 실행하기 위한 목적 등이 있습니다.

다음과 같은 구조의 파이썬 프로젝트를 생각해 봅시다.

/project - /main - /module1 - __init__.py
         |       L /module2 - __init__.py
         L /scripts - my_script.py

(못그렸어도 양해좀..)

현재 실행하려는 스크립트는 /project/scripts/my_script.py 입니다. 그리고 이 스크립트는 /project/main/module1from main import module1 의 형태로 import 하고 있습니다. 아무 조건도 없는 상태에서 이 스크립트를 내가 원하는 타이밍에 실행시키려면 프로젝트 작업 디렉토리를 시스템에 등록해야 합니다.

위에서 살펴본 경로 패키지들로 스크립트를 구성해 봅시다.

os.path 확용

import sys
import os
sys.path.append(os.path.dirname(os.path.abspath(os.path.dirname(__file__))))

from main import module1


if __name__ == '__main__':
    # Write script contents

pathlib 활용

import sys
from pathlib import Path
sys.path.append(Path(__file__).resolve().parent.parent)

from main import module1


if __name__ == '__main__':
    # Write script contents

각각 os.pathpathlib 를 사용했지만, 결과는 동일합니다. 현재 실행하는 스크립트 파일(__file__)의 절대 경로를 구하고, 그것의 상위-상위 디렉토리 경로를 구해 sys.path 에 추가하였습니다.

이제 /project/main/module1 모듈을 import 가능합니다!