PEP 519 – 파일 시스템 경로 프로토콜 추가
- Author:
- Brett Cannon <brett at python.org>, Koos Zevenhoven <k7hoven at gmail.com>
- Status:
- Final
- Type:
- Standards Track
- Created:
- 11-May-2016
- Python-Version:
- 3.6
- Post-History:
- 11-May-2016, 12-May-2016, 13-May-2016
- Resolution:
- Python-Dev message
Table of Contents
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 PEP는 파일 시스템 경로를 나타내는 클래스가 str 또는 bytes 표현을 제공할 수 있도록 하는 프로토콜을 제안합니다. 또한 역사적으로 str 및/또는 bytes 파일 시스템 경로만 허용되던 곳에서 경로 객체를 쉽게 사용할 수 있도록, 적절한 경우 이 프로토콜을 활용하도록 Python 표준 라이브러리를 변경할 것을 제안합니다. 목표는 str 또는 bytes를 예상하는 코드로 쉽게 작업할 수 있도록 하면서 사용자가 풍부한 경로 객체로 마이그레이션하도록 지원하는 것입니다.
근거
역사적으로 Python에서 파일 시스템 경로는 문자열 또는 바이트로 표현되어 왔습니다. 이러한 표현 방식을 선택한 것은 파일 시스템 경로를 const char * [3]로 표현하기로 한 C 자체의 결정에서 비롯되었습니다. 이는 파일 시스템 경로에 사용하기에 충분히 적합한 형식이지만, 반드시 최적인 것은 아닙니다. 문제는 모든 파일 시스템 경로를 문자열 또는 바이트로 표현할 수 있는 반면, 모든 문자열 또는 바이트가 파일 시스템 경로를 나타내는 것은 아니라는 사실입니다. 이로 인해 예를 들어 어떤 문자열이 실제로 경로를 나타내는지 여부와 관계없이 파일 시스템 경로처럼 덕 타이핑되는 문제가 발생할 수 있습니다.
파일 시스템 경로의 표현을 문자열 및 바이트 표현에서 더 풍부한 객체 표현으로 발전시키는 데 도움을 주기 위해, pathlib 모듈 [4]이 PEP 428을 통해 Python 3.4에서 잠정적으로 도입되었습니다. 일부에서는 이를 파일 시스템 경로에 대한 문자열 및 바이트의 개선으로 여겼지만, 널리 채택되지 못했습니다. 일반적으로 낮은 채택률의 핵심 문제로 지적된 것은 표준 라이브러리의 지원 부족이었습니다. 이러한 지원 부족으로 인해 pathlib 사용자는 str(path)를 호출하여 경로 객체를 문자열로 수동 변환해야 했으며, 많은 사용자가 이를 오류가 발생하기 쉬운 방식이라고 여겼습니다.
경로 객체를 문자열로 변환할 때의 한 가지 문제는 경로의 문자열 표현을 얻는 일반적인 방법이 객체를 str()에 전달하는 것뿐이었다는 사실에서 비롯됩니다. 거의 모든 Python 객체가 경로인지 여부와 관계없이 어떤 문자열 표현을 가지므로 이를 무분별하게 수행하면 문제가 발생할 수 있습니다. 예를 들어 str(None)은 builtins.open() [5]이 새 파일을 만드는 데 아무 문제 없이 사용할 결과를 반환합니다.
이 모든 상황을 더욱 악화시키는 것은 DirEntry 객체 [8]입니다. 경로 객체에는 str()를 사용하여 추출할 수 있는 표현이 있는 반면, DirEntry 객체는 대신 path 속성을 노출합니다. 경로 객체, DirEntry 및 기타 서드파티 경로 라이브러리 사이에 공통 인터페이스가 없다는 점이 문제가 되었습니다. 경로를 나타내는 모든 객체가 자신이 경로임을 선언하고 모든 경로 객체가 지원할 수 있는 저수준 표현을 추출할 방법을 제공하는 해결책이 필요합니다.
따라서 이 PEP는 파일 시스템 경로를 나타내는 객체가 따라야 할 새로운 프로토콜을 도입할 것을 제안합니다. 프로토콜을 제공하면 어떤 객체가 파일 시스템 경로를 나타내는지 명시적으로 알릴 수 있을 뿐만 아니라, 문자열 또는 바이트만 지원하는 이전 API와 함께 사용할 수 있는 저수준 표현을 추출할 방법도 제공할 수 있습니다.
이 PEP로 이어진 경로 객체에 관한 논의는 2016년 3월과 4월의 python-ideas 메일링 리스트 아카이브 [1]에 있는 여러 스레드와 2016년 4월의 python-dev 메일링 리스트 아카이브 [2]에서 확인할 수 있습니다.
제안
이 제안은 두 부분으로 나뉩니다. 한 부분은 객체가 파일 시스템 경로 표현을 노출한다고 선언하고 이를 지원할 수 있도록 하는 프로토콜에 대한 제안입니다. 다른 부분은 새로운 프로토콜을 지원하도록 Python 표준 라이브러리를 변경하는 내용을 다룹니다. 이러한 변경으로 pathlib 모듈도 잠정적 상태를 벗어나게 됩니다.
프로토콜
다음 추상 베이스 클래스는 객체가 경로 객체로 간주되기 위한 프로토콜을 정의합니다.:
import abc
import typing as t
class PathLike(abc.ABC):
"""Abstract base class for implementing the file system path protocol."""
@abc.abstractmethod
def __fspath__(self) -> t.Union[str, bytes]:
"""Return the file system path representation of the object."""
raise NotImplementedError
파일 시스템 경로를 나타내는 객체는 경로의 str 또는 bytes 표현을 반환하는 __fspath__() 메서드를 구현합니다. str 표현은 사람이 읽을 수 있고 사람들이 역사적으로 경로를 나타내 온 방식이므로 선호되는 저수준 경로 표현입니다.
표준 라이브러리 변경 사항
현재 파일 시스템 경로를 허용하는 Python 표준 라이브러리의 대부분 API는 경로 객체를 허용하도록 적절히 업데이트될 것으로 예상합니다(이를 위해 코드 변경이 필요한지 아니면 단순히 문서를 업데이트하면 되는지는 각각 다릅니다). 그러나 아래에 언급된 모듈은 경로 객체를 사용할 수 있도록 하는 근본적인 변경 사항이 있거나 API의 추가 또는 제거를 수반하므로 구체적인 세부 사항을 설명할 필요가 있습니다.
builtins
open() [5]은 str 및 bytes를 계속 허용하면서 경로 객체도 허용하도록 업데이트됩니다.
os
다음 의미 체계에 따라 fspath() 함수가 추가됩니다.:
import typing as t
def fspath(path: t.Union[PathLike, str, bytes]) -> t.Union[str, bytes]:
"""Return the string representation of the path.
If str or bytes is passed in, it is returned unchanged. If __fspath__()
returns something other than str or bytes then TypeError is raised. If
this function is given something that is not str, bytes, or os.PathLike
then TypeError is raised.
"""
if isinstance(path, (str, bytes)):
return path
# Work from the object's type to match method resolution of other magic
# methods.
path_type = type(path)
try:
path = path_type.__fspath__(path)
except AttributeError:
if hasattr(path_type, '__fspath__'):
raise
else:
if isinstance(path, (str, bytes)):
return path
else:
raise TypeError("expected __fspath__() to return str or bytes, "
"not " + type(path).__name__)
raise TypeError("expected str, bytes or os.PathLike object, not "
+ path_type.__name__)
os.fsencode() [6] 및 os.fsdecode() [7] 함수는 경로 객체를 허용하도록 업데이트됩니다. 두 함수는 각각 인자를 bytes 및 str로 강제 변환하므로, 경로 객체를 str 또는 bytes 표현으로 변환하기 위해 __fspath__()가 있으면 이를 호출하도록 업데이트되며, 그런 다음 __fspath__()의 반환 값이 해당 강제 변환 함수의 원래 인자였던 것처럼 적절한 강제 변환 작업을 수행합니다.
os.fspath()의 추가, os.fsencode()/os.fsdecode()의 업데이트 및 현재 pathlib.PurePath의 의미 체계는 원하는 경로 표현을 얻는 데 필요한 의미 체계를 제공합니다. 경로 객체에는 pathlib.PurePath/Path를 사용할 수 있습니다. 강제 변환 없이 str 또는 bytes 표현을 얻으려면 os.fspath()를 사용할 수 있습니다. str이 필요하고 bytes의 인코딩이 기본 파일 시스템 인코딩이라고 가정해야 한다면 os.fsdecode()를 사용해야 합니다. bytes 표현이 필요하고 모든 문자열을 기본 파일 시스템 인코딩을 사용하여 인코딩해야 한다면 os.fsencode()를 사용합니다. 이 PEP에서는 가능한 경우 경로 객체를 사용하고, 필요에 따라 문자열 경로로 대체하며, 최후의 수단으로 bytes를 사용할 것을 권장합니다.
이를 보는 또 다른 방법은 파일 시스템 경로 표현의 계층 구조(높은 수준에서 낮은 수준 순)입니다: path → str → bytes. 논의 중인 함수와 클래스는 모두 계층 구조에서 동일한 수준의 객체를 허용할 수 있지만, 객체를 다른 수준으로 승격하거나 강등하는 방식은 서로 다릅니다. pathlib.PurePath 클래스는 str을 경로 객체로 승격할 수 있습니다. os.fspath() 함수는 __fspath__()가 반환하는 값에 따라 경로 객체를 str 또는 bytes 인스턴스로 강등할 수 있습니다. os.fsdecode() 함수는 경로 객체를 문자열로 강등하거나 bytes 객체를 str로 승격합니다. os.fsencode() 함수는 경로 또는 문자열 객체를 bytes로 강등합니다. 문자열로 강등하는 과정을 거치지 않고 경로 객체를 직접 bytes로 강등하는 방법을 제공하는 함수는 없습니다.
DirEntry 객체 [8]에 __fspath__() 메서드가 추가됩니다. 이 메서드는 DirEntry 인스턴스의 path 속성에서 현재 확인되는 것과 동일한 값을 반환합니다.
Protocol ABC가 os.PathLike라는 이름으로 os 모듈에 추가됩니다.
os.path
os.path [9]의 다양한 경로 조작 함수는 경로 객체를 허용하도록 업데이트됩니다. 바이트와 문자열을 모두 허용하는 다형성 함수는 단순히 os.fspath()를 사용하도록 업데이트됩니다.
이 PEP로 이어진 논의에서 “명시적인 것이 암묵적인 것보다 낫다”는 주장을 근거로 os.path를 업데이트하지 말아야 한다는 의견이 제시되었습니다. __fspath__()가 그 자체로 다형적이므로 os.path를 사용하는 코드가 경로 객체에서 경로 표현을 명시적으로 추출하도록 하는 편이 더 나을 수 있다는 취지였습니다. 또한 저수준 OS API의 이처럼 깊은 수준에 지원을 추가하면 문서를 업데이트하지 않아도 코드가 마법처럼 경로 객체를 지원하게 되어, 프로젝트 작성자가 알지 못하는 사이 작동하지 않을 때 잠재적인 불만이 제기될 수 있다는 고려도 있었습니다.
그러나 이 경우에는 “실용성이 순수성보다 우선한다”는 것이 이 PEP의 견해입니다. 경로 객체 지원으로의 전환을 원활하게 진행하려면, 프로젝트에서 경로 객체에 대해 예상하지 못했거나 문서화되지 않은 덕 타이핑 지원을 걱정하기보다 전환을 가능한 한 쉽게 만드는 편이 낫습니다.
또한 os.path함수가 긴밀한 반복문에서 사용될 수 있으며 __fspath__()를 확인하거나 호출하는 오버헤드가 너무 클 것이라는 의견도 제시되었습니다. 이 경우에는 경로를 소비하는 API만 직접 업데이트하고, os.path의 함수와 같은 경로 조작 API는 수정하지 않게 됩니다. 라이브러리 작성자가 경로 조작을 수행한다면 경로 객체를 지원하도록 코드를 업데이트해야 하지만, 라이브러리 코드가 경로를 그대로 전달하기만 한다면 라이브러리를 업데이트할 필요가 없습니다. 그러나 이 PEP와 Guido의 견해는 이것이 불필요한 걱정이며 성능도 여전히 허용 가능한 수준일 것이라는 것입니다.
pathlib
pathlib.PurePath와 pathlib.Path의 생성자가 PathLike 객체를 허용하도록 업데이트됩니다. PurePath와 Path는 계속해서 bytes 경로 표현을 허용하지 않으므로 __fspath__()가 bytes 를 반환하면 예외가 발생합니다.
이 PEP로 인해 path 속성이 불필요해지므로 해당 속성이 제거됩니다(출시된 Python 버전에 포함된 적이 없으므로 하위 호환성 문제는 아닙니다).
C API
C API에 os.fspath()와 동등한 함수가 추가됩니다.:
/*
Return the file system path representation of the object.
If the object is str or bytes, then allow it to pass through with
an incremented refcount. If the object defines __fspath__(), then
return the result of that method. All other types raise a TypeError.
*/
PyObject *
PyOS_FSPath(PyObject *path)
{
_Py_IDENTIFIER(__fspath__);
PyObject *func = NULL;
PyObject *path_repr = NULL;
if (PyUnicode_Check(path) || PyBytes_Check(path)) {
Py_INCREF(path);
return path;
}
func = _PyObject_LookupSpecial(path, &PyId___fspath__);
if (NULL == func) {
return PyErr_Format(PyExc_TypeError,
"expected str, bytes or os.PathLike object, "
"not %S",
path->ob_type);
}
path_repr = PyObject_CallFunctionObjArgs(func, NULL);
Py_DECREF(func);
if (!PyUnicode_Check(path_repr) && !PyBytes_Check(path_repr)) {
Py_DECREF(path_repr);
return PyErr_Format(PyExc_TypeError,
"expected __fspath__() to return str or bytes, "
"not %S",
path_repr->ob_type);
}
return path_repr;
}
하위 호환성
명시적인 하위 호환성 문제는 없습니다. 객체가 우연히 이미 __fspath__()메서드를 정의하고 있는 경우가 아니라면 기존 코드가 중단되거나 그 의미가 암묵적으로 변경될 것이라고 예상할 이유가 없습니다.
경로 객체와 Python 3.6 이전 버전 및 os.fspath()가 존재하지 않는 환경을 지원하려는 라이브러리는 path.__fspath__() if hasattr(path, "__fspath__") else path 관용구를 사용할 수 있습니다.
구현
다음은 이 PEP가 Python 3.6에서 변경하도록 제안하는 작업 목록입니다.
- pathlib에서
path속성 제거 (done) - pathlib의 잠정 상태 제거 (done)
os.PathLike추가 (code 및 docs 완료)PyOS_FSPath()추가 (code 및 docs 완료)os.fspath()추가 (done <done)os.fsencode()업데이트 (done)os.fsdecode()업데이트 (done)pathlib.PurePath와pathlib.Path업데이트 (done)__fspath__()추가- 생성자에
os.PathLike지원 추가
DirEntry에__fspath__()추가 (done)builtins.open()을 업데이트합니다 (done)os.path를 업데이트합니다 (done)- “path-like”에 대한 glossary 항목을 추가합니다 (done)
- “What’s New”을 업데이트합니다 (done)
거부된 아이디어
프로토콜 메서드의 다른 이름
이 PEP로 이어지는 논의 중에 __path__, __pathname__, __fspathname__을 비롯한 다양한 이름이 제안되었습니다. 결국 사람들은 불필요하게 길지 않으면서도 모호하지 않다는 이유로 __fspath__를 선호하는 듯했습니다.
str/bytes 메서드 분리
한때 __fspath__()가 문자열만 반환하고, 바이트를 반환하는 __fspathb__()라는 다른 메서드를 도입하자는 제안이 있었습니다. __fspath__()를 다형적으로 만들지 않으면 잠재적인 문자열 또는 바이트 표현을 더 쉽게 처리할 수 있다는 생각이었습니다. 그러나 일반적인 합의는 바이트를 반환하는 경우가 십중팔구 드물 것이며, __fspath__()에 대한 직접 호출보다 os 모듈의 다양한 함수가 장려하기에 더 나은 추상화라는 것이었습니다.
path 속성 제공
pathlib.PurePath가 str에서 상속되지 않는 문제를 해결하기 위해, 처음에는 os.DirEntry가 제공하는 것과 유사한 path 속성을 도입하자는 제안이 있었습니다. 그러나 결국 프로토콜을 사용하면 대부분의 사람이 직접 상호 작용할 필요가 없는 API를 직접 노출하지 않고도 동일한 결과를 제공할 수 있다고 판단되었습니다.
__fspath__()가 문자열만 반환하도록 하기
이 PEP로 이어진 논의의 상당 부분은 __fspath__()가 다형적으로 작동하여 str뿐만 아니라 bytes도 반환해야 하는지, 아니면 str만 반환해야 하는지를 중심으로 이루어졌습니다. 이 관점의 일반적인 정서는 bytes가 인코딩에 관한 정보가 본질적으로 부족하여 다루기 어렵고, PEP 383은 surrogateescape 처리기를 사용하여 모든 파일 시스템 경로를 str로 표현할 수 있게 한다는 것이었습니다. 따라서 고수준 경로 객체의 저수준 경로 표현으로 str을 강제로 사용하도록 장려하는 것이 더 나았습니다.
결국 경로를 표현하는 데 bytes를 사용하는 방식은 단순히 사라지지 않을 것이므로 어느 정도 지원해야 한다고 결정되었습니다. 사람들이 pathlib와 같은 경로 객체를 선호하고 bytes를 직접 조작하는 방식에서 멀어지기를 기대합니다.
일반적인 문자열 인코딩 메커니즘
한때 의미론적 의미를 지닌 객체의 문자열 표현을 추출하는 일반적인 메커니즘을 개발하는 논의가 있었습니다(__str__()는 디버깅에 도움이 되는 것 이상으로 반드시 의미론적으로 중요한 것을 반환하지는 않습니다). 결국 이 메커니즘은 이 PEP가 특정한 방식으로 해결하려는 문제 외에는 이를 뒷받침할 필요성이 부족하다고 판단되었습니다.
__fspath__을 속성으로 만들기
__fspath__를 메서드가 아닌 속성으로 만들자는 방안이 잠시 고려되었습니다. 이는 두 가지 이유로 거부되었습니다. 첫째, 역사적으로 프로토콜은 “매직 메서드”로 구현되어 왔으며 “매직 메서드와 속성”으로 구현된 것이 아닙니다. 둘째, 경로 객체의 저수준 표현이 미리 계산되어 있다는 보장이 없으므로, 속성이 프로퍼티로 구현될 경우 내부적으로 비용이 큰 계산이 없다고 사용자를 오도할 가능성이 있습니다.
이는 같은 목적을 달성하기 위해 path 속성을 도입하자는 아이디어와도 간접적으로 연결됩니다. 그러나 이 아이디어에는 path 속성을 가진 모든 객체가 실수로 프로토콜의 덕 타이핑을 충족하게 된다는 추가적인 문제가 있습니다. 프로토콜에 새로운 매직 메서드를 도입하면 실수로 프로토콜을 채택하는 일을 효과적으로 방지할 수 있습니다.
구체적인 타입 힌팅 지원을 제공합니다.
문자열 표현을 반환하는 경로 객체의 타입 힌트를 지정할 수 있도록, 예를 들어 typing.PathLike[str]와 같이 사용할 수 있는 제네릭 typing.PathLike 클래스를 제공하는 방안이 일부 검토되었습니다. 잠재적으로 유용할 수 있지만, 타입 힌트 클래스를 추가할 만큼 유용하지는 않다고 판단되었습니다.
또한 허용 가능한 모든 경로 표현 타입의 합집합을 나타내는 클래스를 typing 모듈에 두려는 필요성도 사라졌습니다. 이는 typing.Union[str, bytes, os.PathLike]로 충분히 쉽게 표현할 수 있으며, 사용자들이 점차 경로 객체만 사용하게 되기를 기대하기 때문입니다.
os.fspathb()를 제공합니다.
예를 들어 os.getcwd()/os.getcwdb()와 같은 구조를 따르도록 os.fspath()는 str만 반환하고, bytes만 반환하는 os.fspathb()라는 다른 함수를 도입하자는 제안이 있었습니다. 그러나 이는 *b() 함수의 목적이 파일 시스템을 조회하여 원시 바이트를 반환해야 하는 경우와 연관되어 있으므로 거부되었습니다. 이 PEP는 파일 시스템의 데이터를 직접 다루지는 않지만(그럴 수도 있지만), 이러한 구분은 불필요하다고 판단되었습니다. 또한 os.fsencode()가 유사한 기능을 제공하므로 바이트만 필요한 경우가 이러한 특정 방식으로 지원해야 할 만큼 흔하지는 않을 것으로 여겨집니다.
인스턴스에서 __fspath__()를 호출합니다.
이 PEP의 초기 초안에서는 os.fspath()가 type(path).__fspath__(path) 대신 path.__fspath__()를 호출했습니다. 이는 Python에서 다른 매직 메서드가 확인되는 방식과 일관되도록 변경되었습니다.
감사의 말
python-ideas와 python-dev 양쪽에 걸쳐 진행된 이 PEP 관련 다양한 논의에 참여해 주신 모든 분께 감사드립니다. 이 PEP의 초기 초안에 직접 의견을 보내 주신 Stephen Turnbull에게 특별히 감사드립니다. 이 PEP의 초기 초안에 의견을 보내 주셨을 뿐만 아니라, 두 메일링 리스트에서 이 주제에 관한 전반적인 논의를 이끌어 가는 데 도움을 주신 Koos Zevenhoven과 Ethan Furman에게 더욱 특별히 감사드립니다.
참고 자료
Copyright
This document has been placed in the public domain.