Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 355 – Path - 객체 지향 파일 시스템 경로

Author:
Björn Lindqvist <bjourne at gmail.com>
Status:
Rejected
Type:
Standards Track
Created:
24-Jan-2006
Python-Version:
2.5
Post-History:


Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

거부 공지

이 PEP는 (이 형태로는) 거부되었습니다. 제안된 path 클래스는 온갖 기능을 다 집어넣은 궁극의 잡동사니입니다. 그러나 경로를 사용하는 모든 기능을 단일 클래스의 메서드로 구현하는 편이 낫다는 생각은 안티 패턴입니다. (예를 들어 open()은 왜 안 됩니까? 아니면 execfile()은 어떻습니까?) str을 서브클래싱하는 것은 특히 나쁜 생각입니다. 많은 문자열 연산은 경로에 적용했을 때 아무런 의미가 없습니다. 이 PEP는 오랫동안 방치되어 왔으며, 때때로 논의가 다시 불붙기는 하지만 이제는 이 PEP를 고통에서 해방시켜 줄 때입니다. 덜 억지스러운 제안이라면 더 받아들여지기 쉬울 것입니다.

초록

이 PEP는 경로를 객체 지향 방식으로 다루기 위해 os 모듈에 추가할 새 클래스 Path를 설명합니다. 여러 관련 함수의 “약한” 폐지(deprecation)에 대해서도 논의하고 권고합니다.

배경

이 PEP에 담긴 아이디어는 새로운 것이 아니며, Python 커뮤니티에서 여러 해 동안 논의되어 왔습니다. 많은 사람이 os.path 모듈에서 제공하는 파일 경로 조작 API가 부족하다고 느껴 왔습니다. Path 객체에 대한 최초의 제안은 2001년 Just van Rossum이 python-dev에서 제기했습니다 [2]. 2003년 Jason Orendorff는 “path 모듈” 버전 1.0을 공개했는데, 이것이 경로를 표현하기 위해 객체를 사용한 최초의 공개 구현이었습니다 [3].

path 모듈은 빠르게 큰 인기를 얻었으며, path 모듈을 Python 표준 라이브러리에 포함시키려는 시도가 여러 차례 있었습니다; [4], [5], [6], [7].

이 PEP는 사람들이 path 모듈에 대해 표명한 아이디어와 제안을 요약하고, 수정된 버전을 표준 라이브러리에 포함할 것을 제안합니다.

동기

파일 시스템 경로를 다루는 일은 어떤 프로그래밍 언어에서든 흔한 작업이며, Python 같은 고수준 언어에서는 매우 흔한 작업입니다. 이 작업을 잘 지원해야 하는 이유는 다음과 같습니다:

  • 거의 모든 프로그램이 파일에 접근하기 위해 경로를 사용합니다. 이처럼 자주 수행되는 작업이라면 가능한 한 직관적이고 쉽게 수행할 수 있어야 한다는 것은 당연합니다.
  • Python을 지나치게 복잡한 셸 스크립트를 대체하는 언어로서 한층 더 뛰어나게 만들어 줍니다.

현재 Python에는 경로를 다루기 위한 수많은 함수가 대여섯 개의 모듈에 흩어져 있습니다. 이 때문에 초보자와 숙련된 개발자 모두 올바른 메서드를 고르기가 어렵습니다.

Path 클래스는 현재의 일반적인 관행에 비해 다음과 같은 개선점을 제공합니다:

  • 하나의 “통합된” 객체가 기존 함수들의 모든 기능을 제공합니다.
  • 서브클래싱 가능성 - Path 객체를 확장하여 파일 시스템 경로 이외의 경로도 지원할 수 있습니다. 프로그래머는 새로운 API를 배울 필요 없이, Path에 대한 지식을 재사용하여 확장된 클래스를 다룰 수 있습니다.
  • 관련 기능이 모두 한곳에 모여 있으므로, 알맞은 함수를 찾기 위해 여러 모듈을 뒤질 필요가 없어 올바른 접근법을 배우기가 더 쉽습니다.
  • Python은 객체 지향 언어입니다. 파일, 날짜/시간, 소켓이 객체인 것처럼 경로도 객체이며, 단순히 함수에 전달되는 문자열이 아닙니다. Path 객체는 본질적으로 파이썬다운 아이디어입니다.
  • Path는 프로퍼티를 활용합니다. 프로퍼티를 사용하면 코드를 더 읽기 쉽게 만들 수 있습니다.:
    if imgpath.ext == 'jpg':
        jpegdecode(imgpath)
    

    다음보다 낫습니다.:

    if os.path.splitexit(imgpath)[1] == 'jpg':
        jpegdecode(imgpath)
    

근거

다음 항목은 설계를 요약한 것입니다.

  • Path는 문자열을 확장하므로, 문자열 경로명을 기대하는 코드는 모두 수정할 필요가 없으며 기존 코드도 전혀 깨지지 않습니다.
  • Path 객체는 클래스 메서드 Path.cwd를 사용하거나, 경로를 나타내는 문자열로 클래스를 인스턴스화하거나, Path(".")와 동등한 기본 생성자를 사용하여 생성할 수 있습니다.
  • Path는 일반적인 경로명 조작, 패턴 확장, 패턴 매칭, 그리고 복사를 포함한 기타 고수준 파일 연산을 제공합니다. 기본적으로 Path는 파일 내용 조작을 제외하고 경로와 관련된 모든 것을 제공합니다. 파일 내용 조작에는 파일 객체가 더 적합합니다.
  • 플랫폼 비호환성은 시스템별 메서드를 인스턴스화하지 않는 방식으로 처리합니다.

명세

이 클래스는 다음과 같은 공개 인터페이스를 정의합니다(독스트링은 참조 구현에서 추출하여 간결하게 줄였습니다. 자세한 내용은 참조 구현을 참고하십시오).:

class Path(str):

    # Special Python methods:
    def __new__(cls, *args) => Path
        """
        Creates a new path object concatenating the *args.  *args
        may only contain Path objects or strings.  If *args is
        empty, Path(os.curdir) is created.
        """
    def __repr__(self): ...
    def __add__(self, more): ...
    def __radd__(self, other): ...

    # Alternative constructor.
    def cwd(cls): ...

    # Operations on path strings:
    def abspath(self) => Path
        """Returns the absolute path of self as a new Path object."""
    def normcase(self): ...
    def normpath(self): ...
    def realpath(self): ...
    def expanduser(self): ...
    def expandvars(self): ...
    def basename(self): ...
    def expand(self): ...
    def splitpath(self) => (Path, str)
        """p.splitpath() -> Return (p.parent, p.name)."""
    def stripext(self) => Path
        """p.stripext() -> Remove one file extension from the path."""
    def splitunc(self): ...  # See footnote [1]
    def splitall(self): ...
    def relpath(self): ...
    def relpathto(self, dest): ...

    # Properties about the path:
    parent => Path
        """This Path's parent directory as a new path object."""
    name => str
        """The name of this file or directory without the full path."""
    ext => str
        """
        The file extension or an empty string if Path refers to a
        file without an extension or a directory.
        """
    drive => str
        """
        The drive specifier.  Always empty on systems that don't
        use drive specifiers.
        """
    namebase => str
        """
        The same as path.name, but with one file extension
        stripped off.
        """
    uncshare[1]

    # Operations that return lists of paths:
    def listdir(self, pattern = None): ...
    def dirs(self, pattern = None): ...
    def files(self, pattern = None): ...
    def walk(self, pattern = None): ...
    def walkdirs(self, pattern = None): ...
    def walkfiles(self, pattern = None): ...
    def match(self, pattern) => bool
        """Returns True if self.name matches the given pattern."""

    def matchcase(self, pattern) => bool
        """
        Like match() but is guaranteed to be case sensitive even
        on platforms with case insensitive filesystems.
        """
    def glob(self, pattern):

    # Methods for retrieving information about the filesystem
    # path:
    def exists(self): ...
    def isabs(self): ...
    def isdir(self): ...
    def isfile(self): ...
    def islink(self): ...
    def ismount(self): ...
    def samefile(self, other): ...  # See footnote [1]
    def atime(self): ...
        """Last access time of the file."""
    def mtime(self): ...
        """Last-modified time of the file."""
    def ctime(self): ...
        """
        Return the system's ctime which, on some systems (like
        Unix) is the time of the last change, and, on others (like
        Windows), is the creation time for path.
        """
    def size(self): ...
    def access(self, mode): ...  # See footnote [1]
    def stat(self): ...
    def lstat(self): ...
    def statvfs(self): ...  # See footnote [1]
    def pathconf(self, name): ...  # See footnote [1]

    # Methods for manipulating information about the filesystem
    # path.
    def utime(self, times) => None
    def chmod(self, mode) => None
    def chown(self, uid, gid) => None # See footnote [1]
    def rename(self, new) => None
    def renames(self, new) => None

    # Create/delete operations on directories
    def mkdir(self, mode = 0777): ...
    def makedirs(self, mode = 0777): ...
    def rmdir(self): ...
    def removedirs(self): ...

    # Modifying operations on files
    def touch(self): ...
    def remove(self): ...
    def unlink(self): ...

    # Modifying operations on links
    def link(self, newpath): ...
    def symlink(self, newlink): ...
    def readlink(self): ...
    def readlinkabs(self): ...

    # High-level functions from shutil
    def copyfile(self, dst): ...
    def copymode(self, dst): ...
    def copystat(self, dst): ...
    def copy(self, dst): ...
    def copy2(self, dst): ...
    def copytree(self, dst, symlinks = True): ...
    def move(self, dst): ...
    def rmtree(self, ignore_errors = False, onerror = None): ...

    # Special stuff from os
    def chroot(self): ...  # See footnote [1]
    def startfile(self): ...  # See footnote [1]

기존 함수를 Path 클래스로 대체하기

이 절에서 “a ==> b”는 b를 a의 대체로 사용할 수 있다는 의미입니다.

다음 예제에서는 Path 클래스를 from path import Path로 임포트했다고 가정합니다.

  • os.path.join 대체하기:
    os.path.join(os.getcwd(), "foobar")
    ==>
    Path(Path.cwd(), "foobar")
    
    os.path.join("foo", "bar", "baz")
    ==>
    Path("foo", "bar", "baz")
    
  • os.path.splitext 대체하기:
    fname = "Python2.4.tar.gz"
    os.path.splitext(fname)[1]
    ==>
    fname = Path("Python2.4.tar.gz")
    fname.ext
    

    또는 두 부분이 모두 필요하다면:

    fname = "Python2.4.tar.gz"
    base, ext = os.path.splitext(fname)
    ==>
    fname = Path("Python2.4.tar.gz")
    base, ext = fname.namebase, fname.extx
    
  • glob.glob 대체하기:
    lib_dir = "/lib"
    libs = glob.glob(os.path.join(lib_dir, "*s.o"))
    ==>
    lib_dir = Path("/lib")
    libs = lib_dir.files("*.so")
    

폐지 예정 사항

이 모듈을 표준 라이브러리에 도입하면 기존의 여러 모듈과 함수를 “약하게” 폐지 예정으로 지정할 필요가 생깁니다. 이러한 모듈과 함수는 매우 널리 사용되므로 DeprecationWarning을 발생시키는 식으로 진정하게 폐지 예정으로 지정할 수는 없습니다. 여기서 “약한 폐지 예정”이란 문서에 기재하는 것만을 의미합니다.

아래 표는 폐지 예정으로 지정해야 하는 기존 기능을 나열합니다.

Path 메서드/프로퍼티 폐지 예정 대상 함수
normcase() os.path.normcase()
normpath() os.path.normpath()
realpath() os.path.realpath()
expanduser() os.path.expanduser()
expandvars() os.path.expandvars()
parent os.path.dirname()
name os.path.basename()
splitpath() os.path.split()
drive os.path.splitdrive()
ext os.path.splitext()
splitunc() os.path.splitunc()
__new__() os.path.join(), os.curdir
listdir() os.listdir() [fnmatch.filter()]
match() fnmatch.fnmatch()
matchcase() fnmatch.fnmatchcase()
glob() glob.glob()
exists() os.path.exists()
isabs() os.path.isabs()
isdir() os.path.isdir()
isfile() os.path.isfile()
islink() os.path.islink()
ismount() os.path.ismount()
samefile() os.path.samefile()
atime() os.path.getatime()
ctime() os.path.getctime()
mtime() os.path.getmtime()
size() os.path.getsize()
cwd() os.getcwd()
access() os.access()
stat() os.stat()
lstat() os.lstat()
statvfs() os.statvfs()
pathconf() os.pathconf()
utime() os.utime()
chmod() os.chmod()
chown() os.chown()
rename() os.rename()
renames() os.renames()
mkdir() os.mkdir()
makedirs() os.makedirs()
rmdir() os.rmdir()
removedirs() os.removedirs()
remove() os.remove()
unlink() os.unlink()
link() os.link()
symlink() os.symlink()
readlink() os.readlink()
chroot() os.chroot()
startfile() os.startfile()
copyfile() shutil.copyfile()
copymode() shutil.copymode()
copystat() shutil.copystat()
copy() shutil.copy()
copy2() shutil.copy2()
copytree() shutil.copytree()
move() shutil.move()
rmtree() shutil.rmtree()

Path 클래스는 os.path, shutil, fnmatch, glob 전체를 폐기합니다. os의 상당 부분도 폐기됩니다.

해결된 문제

이 PEP가 python-dev에 처음 등장한 이후 여러 논쟁적인 쟁점이 해결되었습니다:

  • __div__() 메서드가 제거되었습니다. / (나눗셈) 연산자를 오버로딩하는 것은 “지나친 마법”일 수 있으며, 경로 연결을 나눗셈처럼 보이게 만들 수 있습니다. BDFL이 원한다면 이 메서드는 언제든 나중에 다시 추가할 수 있습니다. 그 대신 __new__()에 Path 객체와 문자열 객체를 모두 받는 *args 인자가 추가되었습니다. *args는 os.path.join()으로 연결되며, 그 결과는 Path 객체를 생성하는 데 사용됩니다. 이러한 변경으로 문제가 많던 joinpath() 메서드가 불필요해져 제거되었습니다.
  • getatime()/atime, getctime()/ctime, getmtime()/mtime, getsize()/size 메서드와 프로퍼티는 서로 중복되었습니다. 이 메서드와 프로퍼티는 atime(), ctime(), mtime(), size()로 통합되었습니다. 이것들을 프로퍼티로 만들지 않은 이유는 값이 예기치 않게 바뀔 가능성이 있기 때문입니다. 다음 예제는 항상 단언문을 통과한다고 보장되지 않습니다:
    p = Path("foobar")
    s = p.size()
    assert p.size() == s
    

미해결 문제

Jason Orendorff의 path 모듈 기능 중 일부는 제외되었습니다:

  • 경로를 여는 함수 - 내장 open()으로 처리하는 편이 낫습니다.
  • 파일 전체를 읽고 쓰는 함수 - 파일 객체 자체의 read()와 write() 메서드로 처리하는 편이 낫습니다.
  • chdir() 함수는 포함할 가치가 있을 수 있습니다.
  • 폐지 일정을 수립해야 합니다. Path는 어느 정도의 기능을 구현해야 합니까? 기존 기능 중 어느 정도를, 그리고 언제 지원 중단해야 합니까?
  • 이름은 당연히 “path” 또는 “Path” 중 하나여야 하지만, 어디에 두어야 합니까? 독자적인 모듈에 두어야 합니까, 아니면 os에 두어야 합니까?
  • Path가 str 또는 unicode를 서브클래싱하므로, Path 객체에서는 매직 메서드가 아닌 다음 공개 메서드를 사용할 수 있습니다:
    capitalize(), center(), count(), decode(), encode(),
    endswith(), expandtabs(), find(), index(), isalnum(),
    isalpha(), isdigit(), islower(), isspace(), istitle(),
    isupper(), join(), ljust(), lower(), lstrip(), replace(),
    rfind(), rindex(), rjust(), rsplit(), rstrip(), split(),
    splitlines(), startswith(), strip(), swapcase(), title(),
    translate(), upper(), zfill()
    

    python-dev에서는 이러한 상속이 타당한지를 두고 논쟁이 있었습니다. 논쟁에 참여한 사람 대부분은 대부분의 문자열 메서드가 파일 시스템 경로라는 맥락에서는 의미가 없으며, 그저 불필요한 짐일 뿐이라고 말했습니다. 역시 python-dev에서 제기된 반대 입장은, 문자열을 상속하면 코드를 Path 객체에 맞게 고치지 않아도 Path 객체와 함께 “그냥 동작”하므로 매우 편리하다는 것입니다.

    문제 중 하나는 Python 수준에서는 객체가 str이나 unicode를 상속하지 않는 한, 그 객체를 내장 함수 open() (및 문자열이나 버퍼를 기대하는 다른 내장 함수)에 전달할 수 있을 만큼 “충분히 문자열 같게” 만들 방법이 없다는 것입니다. 따라서 문자열을 상속하지 않으려면 CPython 코어를 변경해야 합니다.

이 새 모듈이 대체하려는 함수와 모듈(os.path, shutil, fnmatch, glob 및 os의 일부)은 하위 호환성을 유지하기 위해 향후 Python 버전에서도 오랫동안 사용할 수 있을 것으로 예상됩니다.

참조 구현

현재 Path 클래스는 표준 라이브러리 모듈 fnmatch, glob, os, os.path, shutil을 얇게 감싼 래퍼로 구현되어 있습니다. 이 PEP의 의도는 앞서 언급한 모듈들이 지원 중단되는 동안 그 기능을 Path로 옮기는 것입니다.

자세한 내용과 구현은 다음을 참조하십시오:

예시

이 절에서 “a ==> b”는 b를 a의 대체물로 사용할 수 있다는 의미입니다.

  • 디렉터리에 있는 모든 python 파일을 실행 가능하게 만들기:
    DIR = '/usr/home/guido/bin'
    for f in os.listdir(DIR):
        if f.endswith('.py'):
            path = os.path.join(DIR, f)
            os.chmod(path, 0755)
    ==>
    for f in Path('/usr/home/guido/bin').files("*.py"):
        f.chmod(0755)
    
  • emacs 백업 파일 삭제하기:
    def delete_backups(arg, dirname, names):
        for name in names:
            if name.endswith('~'):
                os.remove(os.path.join(dirname, name))
    os.path.walk(os.environ['HOME'], delete_backups, None)
    ==>
    d = Path(os.environ['HOME'])
    for f in d.walkfiles('*~'):
        f.remove()
    
  • 파일에 대한 상대 경로 찾기:
    b = Path('/users/peter/')
    a = Path('/users/peter/synergy/tiki.txt')
    a.relpathto(b)
    
  • 경로를 디렉터리와 파일 이름으로 나누기:
    os.path.split("/path/to/foo/bar.txt")
    ==>
    Path("/path/to/foo/bar.txt").splitpath()
    
  • 현재 디렉터리 트리에 있는 모든 Python 스크립트 나열하기:
    list(Path().walkfiles("*.py"))
    

참고 문헌 및 각주

[1] 이 메서드는 모든 플랫폼에서 사용할 수 있다는 보장이 없습니다.