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

Python 개선 제안 한국어 번역

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 자체의 결정에서 비롯되었습니다. 이는 파일 시스템 경로에 사용하기에 충분히 쓸 만한 형식이지만, 반드시 최적이라고 할 수는 없습니다. 문제는 모든 파일 시스템 경로를 문자열이나 바이트열로 표현할 수 있는 반면, 모든 문자열이나 바이트열이 파일 시스템 경로를 나타내지는 않는다는 점입니다. 이로 인해 예를 들어 어떤 문자열이든 실제로 경로를 나타내는지와 관계없이 덕 타이핑에 의해 파일 시스템 경로로 취급되는 문제가 생길 수 있습니다.

파일 시스템 경로의 표현을 문자열과 바이트열에서 더 풍부한 객체 표현으로 끌어올리는 데 도움이 되도록, PEP 428을 통해 Python 3.4에 pathlib 모듈 [4]이 잠정적으로 도입되었습니다. 일부에서는 파일 시스템 경로를 문자열과 바이트열로 다루는 것보다 개선된 방식으로 여겼지만, pathlib은 널리 채택되지 못했습니다. 낮은 채택률의 주요 원인으로는 대개 표준 라이브러리의 지원 부족이 꼽혀 왔습니다. 이러한 지원 부족 때문에 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의 추가/제거를 수반하므로 구체적으로 자세히 설명할 필요가 있습니다.

내장

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로 강제 변환하므로, __fspath__()가 있으면 이를 호출하여 경로 객체를 str 또는 bytes 표현으로 변환한 다음, __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()가 존재하기 이전 버전의 Python도 지원하려는 라이브러리는 path.__fspath__() if hasattr(path, "__fspath__") else path 관용구를 사용할 수 있습니다.

구현

다음은 이 PEP가 Python 3.6에서 변경할 것을 제안하는 작업 목록입니다:

  1. pathlib에서 path 속성 제거 (완료)
  2. pathlib의 잠정(provisional) 상태 해제 (완료)
  3. os.PathLike 추가 (코드 및 문서 완료)
  4. PyOS_FSPath() 추가 (코드 및 문서 완료)
  5. os.fspath() 추가 (완료 <done)
  6. os.fsencode() 갱신 (완료)
  7. os.fsdecode() 갱신 (완료)
  8. pathlib.PurePath 및 pathlib.Path 갱신 (완료)
    1. __fspath__() 추가
    2. 생성자에 os.PathLike 지원 추가
  9. DirEntry에 __fspath__() 추가 (완료)
  10. builtins.open() 갱신 (완료)
  11. os.path 갱신 (완료)
  12. “path-like”에 대한 용어집 항목 추가 (완료)
  13. “What’s New” 갱신 (완료)

기각된 아이디어

프로토콜 메서드의 다른 이름

이 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 님께도 특별히 감사드립니다.

참고 문헌