PEP 355 – 경로 - 객체 지향 파일 시스템 경로
- Author:
- Björn Lindqvist <bjourne at gmail.com>
- Status:
- Rejected
- Type:
- Standards Track
- Created:
- 24-Jan-2006
- Python-Version:
- 2.5
- Post-History:
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
거부 통지
이 PEP는 (이 형태로는) 거부되었습니다. 제안된 경로 클래스는 궁극의 만능 도구입니다. 그러나 경로를 사용하는 모든 기능을 하나의 클래스에 메서드로 구현하는 것이 더 낫다는 발상은 안티패턴입니다. (예를 들어 open()은 왜 안 됩니까? 아니면 execfile()은 어떻습니까?) str을 상속하는 것은 특히 좋지 않은 생각입니다. 많은 문자열 연산은 경로에 적용할 때 아무런 의미가 없습니다. 이 PEP는 계속 남아 있었고, 논의가 이따금씩 다시 불붙기는 하지만, 이제 이 PEP를 고통에서 벗어나게 할 때가 되었습니다. 덜 억지스러운 제안이라면 더 받아들여지기 쉬울 것입니다.
초록
이 PEP는 객체 지향 방식으로 경로를 처리하기 위해 os 모듈에 추가할 새로운 클래스인 Path를 설명합니다. 관련된 여러 함수의 “약한” 사용 중단도 논의하고 권장합니다.
배경
이 PEP에서 표현된 아이디어는 최근의 것이 아니며, 수년 동안 Python 커뮤니티에서 논의되어 왔습니다. 많은 사람은 os.path 모듈에서 제공하는 파일 경로 조작 API가 불충분하다고 느껴 왔습니다. Path 객체에 대한 최초의 제안은 2001년에 Just van Rossum이 python-dev에서 제기했습니다 [2]. 2003년에 Jason Orendorff는 “path module” 버전 1.0을 공개했으며, 이는 객체를 사용하여 경로를 표현한 최초의 공개 구현이었습니다 [3].
path module은 빠르게 큰 인기를 얻었고, Python 표준 라이브러리에 path module을 포함시키려는 수많은 시도가 이루어졌습니다 [4], [5], [6], [7].
이 PEP는 사람들이 path module에 관해 표현한 아이디어와 제안을 요약하고, 수정된 버전을 표준 라이브러리에 포함해야 한다고 제안합니다.
동기
파일 시스템 경로를 다루는 일은 모든 프로그래밍 언어에서 흔한 작업이며, Python과 같은 고수준 언어에서는 특히 흔합니다. 다음과 같은 이유로 이 작업을 잘 지원해야 합니다.
- 거의 모든 프로그램은 파일에 접근하기 위해 경로를 사용합니다. 매우 자주 수행되는 작업이라면 가능한 한 직관적이고 쉽게 수행할 수 있어야 한다는 점은 당연합니다.
- 이는 Python을 지나치게 복잡한 셸 스크립트를 대체하는 데 더욱 뛰어난 언어로 만들어 줍니다.
현재 Python에는 경로를 처리하기 위한 서로 다른 함수가 약 6개의 모듈에 흩어져 많이 존재합니다. 이 때문에 초보자와 숙련된 개발자 모두 적절한 메서드를 선택하기가 어렵습니다.
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() |
| 부모 | os.path.dirname() |
| 이름 | os.path.basename() |
| splitpath() | os.path.split() |
| 드라이브 | os.path.splitdrive() |
| 확장자 | 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는Path객체를 구성하는 데 사용되는os.path.join()으로 연결됩니다. 이러한 변경으로 문제가 있던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의 경로 모듈에 있는 일부 기능은 생략되었습니다:
- 경로를 여는 함수 - 내장
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객체에 맞게 수정하지 않아도 해당 객체와 함께 “그냥 작동”하도록 해 주기 때문입니다.문제 중 하나는 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의 대체물로 사용할 수 있다는 뜻입니다.
- 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] 이 메서드는 모든 플랫폼에서 사용할 수 있다고 보장되지 않습니다.
Copyright
This document has been placed in the public domain.