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

Python 개선 제안 한국어 번역

PEP 575 – 함수/메서드 클래스 통합

Author:
Jeroen Demeyer <J.Demeyer at UGent.be>
Status:
Withdrawn
Type:
Standards Track
Created:
27-Mar-2018
Python-Version:
3.8
Post-History:
31-Mar-2018, 12-Apr-2018, 27-Apr-2018, 05-May-2018

Table of Contents

번역·라이선스 안내

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

철회 공지

사용자 정의 클래스를 빠르게 호출할 수 있게 하는 더 나은 해결책은 PEP 580을 참조하십시오.

이 PEP에서 다룬 다른 문제 중 일부에 대한 더 폭넓은 논의는 PEP 579를 참조하십시오.

초록

함수와 메서드의 클래스 계층 구조를 재구성하여, (C로 구현된) 내장 함수와 Python 함수 사이의 차이를 줄이는 것을 목표로 합니다. 주된 목표는 성능을 희생하지 않으면서 내장 함수가 Python 함수에 더 가깝게 동작하도록 만드는 것입니다.

새로운 베이스 클래스 base_function이 도입되며, 여러 함수 클래스와 method(bound_method로 이름이 바뀜)가 이 클래스를 상속합니다.

또한 Python function 클래스의 서브클래싱도 허용합니다.

동기

현재 CPython에는 서로 다른 두 가지 함수 클래스가 있습니다. 첫 번째는 Python 함수로, def나 lambda로 함수를 정의하면 얻게 되는 것입니다. 두 번째는 len, isinstance, numpy.dot과 같은 내장 함수입니다. 이들은 C로 구현되어 있습니다.

이 두 클래스는 완전히 독립적으로 구현되어 있으며 기능도 서로 다릅니다. 특히 현재는 함수를 C로 효율적으로 구현하면서(내장 함수만 가능합니다) 동시에 inspect.signature나 inspect.getsourcefile 같은 인트로스펙션을 허용하는 것(Python 함수만 가능합니다)이 불가능합니다. 이는 바로 그렇게 하기를 원하는 Cython [1]과 같은 프로젝트에 문제가 됩니다.

Cython에서는 cyfunction이라는 새로운 함수 클래스를 고안하여 이 문제를 우회했습니다. 안타깝게도 새로운 함수 클래스는 문제를 일으킵니다. inspect 모듈이 그러한 함수를 함수로 인식하지 못하며 [2], 성능도 더 나쁩니다(CPython에는 내장 함수 호출을 위한 특정 최적화가 있습니다).

두 번째 동기는 좀 더 일반적으로, 내장 함수와 메서드가 Python 함수와 메서드에 더 가깝게 동작하도록 만드는 것입니다. 예를 들어, Python의 언바운드 메서드는 그냥 함수이지만 확장 타입의 언바운드 메서드(예: dict.get)는 별개의 클래스입니다. Python 클래스의 바운드 메서드에는 __func__ 속성이 있지만, 확장 타입의 바운드 메서드에는 없습니다.

세 번째로, 이 PEP는 함수를 폭넓게 사용자 정의할 수 있게 합니다. function 클래스를 서브클래싱할 수 있게 되며, C로 구현된 함수에 대해서도 사용자 정의 함수 서브클래스가 허용됩니다. 후자의 경우, 진짜 내장 함수와 같은 성능으로 이를 수행할 수 있습니다. 모든 함수는 함수 객체(__call__의 self)에 접근할 수 있으며, 이는 PEP 573을 위한 길을 열어 줍니다.

새로운 클래스

다음은 함수와 메서드를 위한 새로운 클래스 계층 구조입니다:

              object
                 |
                 |
          base_function
         /       |     \
        /        |      \
       /         |   defined_function
      /          |        \
cfunction (*)    |         \
                 |       function
                 |
           bound_method (*)

(*)로 표시된 두 클래스는 서브클래싱을 허용하지 않습니다. 나머지 클래스는 허용합니다.

함수와 언바운드 메서드 사이에는 차이가 없으며, 바운드 메서드는 bound_method의 인스턴스입니다.

base_function

base_function 클래스는 모든 함수 타입의 새로운 베이스 클래스가 됩니다. 이 클래스는 기존 builtin_function_or_method 클래스를 기반으로 하지만, 다음과 같은 차이점과 새로운 기능이 있습니다:

  1. m_self가 NULL이면 함수를 메서드로 바꾸도록 __get__을 구현하는 디스크립터로 동작합니다. m_self가 NULL이 아니면 아무 일도 하지 않으며, 대신 기존 함수를 그대로 반환합니다.
  2. 새 읽기 전용 속성 __parent__로, C 구조체에서는 m_parent로 표현됩니다. 이 속성이 있으면 이 속성은 해당 함수를 정의한 객체를 나타냅니다. 확장 타입의 메서드에서는 정의한 클래스(순수 Python의 __class__)이고, 모듈의 함수에서는 정의한 모듈입니다. 일반적으로는 어떤 Python 객체든 될 수 있습니다. __parent__가 클래스이면 특별한 의미를 가집니다. 이 경우 함수는 self가 해당 클래스의 인스턴스인 상태로 호출되어야 합니다. 마지막으로, __qualname__과 __reduce__는 (이전의 __self__ 대신) __parent__를 네임스페이스로 사용합니다.
  3. 새 속성 __objclass__로, __parent__가 클래스이면 __parent__와 같습니다. 그렇지 않으면 __objclass__에 접근할 때 AttributeError가 발생합니다. 이는 method_descriptor와의 하위 호환성을 위한 것입니다.
  4. ml_doc 필드와 __doc__ 및 __text_signature__ 속성(Argument Clinic 참조)은 지원되지 않습니다.
  5. ml_flags에 대한 새 플래그 METH_PASS_FUNCTION입니다. 이 플래그가 설정되면 ml_meth에 저장된 C 함수는 함수 객체 자신을 추가 첫 번째 인자로 받아 호출됩니다.
  6. ml_flags에 대한 새 플래그 METH_BINDING으로, (클래스의 메서드가 아닌) 모듈의 함수에만 적용됩니다. 이 플래그가 설정되면 m_self는 모듈 대신 NULL로 설정됩니다. 이 플래그는 __get__을 활성화하므로 함수가 Python 함수에 더 가깝게 동작할 수 있게 합니다.
  7. Self slicing을 비활성화하기 위한 새 플래그 METH_CALL_UNBOUND입니다.
  8. ml_flags에 대한 새 플래그 METH_PYTHON입니다. 이 플래그는 이 함수를 Python 함수로 취급해야 함을 나타냅니다. 이 플래그는 덕 타이핑 철학에 어긋나므로 이상적으로는 사용을 피해야 합니다. 그렇지만 Profiling과 같은 몇몇 곳에서는 여전히 필요합니다.

base_function의 목표는 함수와 메서드를 호출하는 서로 다른 모든 방식을 단 하나의 구조체로 지원하는 것입니다. 예를 들어, 새 플래그 METH_PASS_FUNCTION은 메서드 구현에서 사용됩니다.

base_function의 인스턴스를 직접 생성할 수는 없습니다(tp_new가 NULL입니다). 하지만 C 코드에서 인스턴스를 수동으로 생성하는 것은 허용됩니다.

관련 C 구조체는 다음과 같습니다:

PyTypeObject PyBaseFunction_Type;

typedef struct {
    PyObject_HEAD
    PyCFunctionDef *m_ml;     /* Description of the C function to call */
    PyObject *m_self;         /* __self__: anything, can be NULL; readonly */
    PyObject *m_module;       /* __module__: anything (typically str) */
    PyObject *m_parent;       /* __parent__: anything, can be NULL; readonly */
    PyObject *m_weakreflist;  /* List of weak references */
} PyBaseFunctionObject;

typedef struct {
    const char *ml_name;   /* The name of the built-in function/method */
    PyCFunction ml_meth;   /* The C function that implements it */
    int ml_flags;          /* Combination of METH_xxx flags, which mostly
                              describe the args expected by the C func */
} PyCFunctionDef;

서브클래스는 PyCFunctionDef에 필드를 추가하여 확장할 수 있습니다.

Python 속성 __self__는 METH_STATIC이 설정된 경우를 제외하고 m_self를 반환합니다. 그 경우나 m_self가 NULL인 경우에는 __self__ 속성이 아예 존재하지 않습니다. 그런 이유로 이 PEP에서는 m_self와 __self__를 약간 다른 의미로 구분하여 씁니다.

cfunction

이것은 예전 builtin_function_or_method 클래스의 새 버전입니다. cfunction이라는 이름은 “builtins 모듈에 있는 무언가”라는 의미의 “내장(built-in)”과 혼동되지 않도록 선택되었습니다. 또한 PyCFunction 접두사를 사용하는 C API와도 더 잘 어울립니다.

cfunction 클래스는 base_function의 복사본이며, 다음과 같은 차이점이 있습니다:

  1. m_ml은 PyMethodDef 구조체를 가리키며, 이 구조체는 __doc__와 __text_signature__를 읽기 전용 속성으로 구현하기 위한 추가 ml_doc 필드를 두어 PyCFunctionDef를 확장합니다:
    typedef struct {
        const char *ml_name;
        PyCFunction ml_meth;
        int ml_flags;
        const char *ml_doc;
    } PyMethodDef;
    

    PyMethodDef는 파이썬 안정 ABI의 일부이며 사실상 모든 확장 모듈에서 사용되므로, 이 구조체는 절대로 변경할 수 없다는 점에 유의하십시오.

  2. Argument Clinic이 지원됩니다.
  3. __self__는 항상 존재합니다. base_function.__self__가 AttributeError를 발생시키는 경우에는 대신 None이 반환됩니다.

타입 객체는 PyTypeObject PyCFunction_Type이며, PyCFunctionObject를 PyBaseFunctionObject의 별칭으로 정의합니다(m_ml의 타입은 제외합니다).

defined_function

defined_function 클래스는 함수가 인트로스펙션을 지원함을 나타내기 위한 추상 베이스 클래스입니다. defined_function의 인스턴스는 Python 함수가 가진 모든 속성, 즉 __code__, __globals__, __doc__, __defaults__, __kwdefaults__, __closure__, __annotations__를 지원해야 합니다. 사용자가 추가한 속성을 지원하기 위한 __dict__도 있습니다.

이들 중 어느 것도 의미 있는 값일 필요는 없습니다. 특히 __code__는 제대로 동작하는 코드 객체가 아닐 수 있으며, 일부 필드만 채워져 있을 수도 있습니다. 이 PEP는 여러 속성이 어떻게 구현되는지를 규정하지 않습니다. 단순한 구조체 멤버일 수도 있고 더 복잡한 디스크립터일 수도 있습니다. 읽기 전용 지원만 요구되며, 어떤 속성도 쓰기 가능할 필요는 없습니다.

defined_function 클래스는 주로 자동 생성된 C 코드, 예를 들어 Cython [1]이 생성한 코드를 위한 것입니다. 이 클래스의 인스턴스를 생성하는 API는 없습니다.

C 구조체는 다음과 같습니다:

PyTypeObject PyDefinedFunction_Type;

typedef struct {
    PyBaseFunctionObject base;
    PyObject *func_dict;        /* __dict__: dict or NULL */
} PyDefinedFunctionObject;

TODO: defined_function에 더 나은 이름을 찾아보는 것이 좋을 수 있습니다. 다른 제안으로는 inspect_function (inspect.isfunction을 만족하는 모든 것), builtout_function (더 잘 갖춰진(built out) 함수로, builtin(내장)에 대한 말장난), generic_function (원래 제안이지만 functools.singledispatch 제네릭 함수와 충돌함), user_function (CPython이 아닌 사용자가 정의한 함수)이 있습니다.

function(함수)

이것은 Python으로 구현된 함수를 위한 클래스입니다. 다른 함수 타입과 달리, function의 인스턴스는 Python 코드에서 생성할 수 있습니다. 이 부분은 변경되지 않으므로 이 PEP에서는 자세히 설명하지 않습니다.

C 구조체의 레이아웃은 다음과 같습니다:

PyTypeObject PyFunction_Type;

typedef struct {
    PyBaseFunctionObject base;
    PyObject *func_dict;        /* __dict__: dict or NULL */
    PyObject *func_code;        /* __code__: code */
    PyObject *func_globals;     /* __globals__: dict; readonly */
    PyObject *func_name;        /* __name__: string */
    PyObject *func_qualname;    /* __qualname__: string */
    PyObject *func_doc;         /* __doc__: can be anything or NULL */
    PyObject *func_defaults;    /* __defaults__: tuple or NULL */
    PyObject *func_kwdefaults;  /* __kwdefaults__: dict or NULL */
    PyObject *func_closure;     /* __closure__: tuple of cell objects or NULL; readonly */
    PyObject *func_annotations; /* __annotations__: dict or NULL */
    PyCFunctionDef _ml;         /* Storage for base.m_ml */
} PyFunctionObject;

디스크립터 __name__은 func_name을 반환합니다. __name__을 설정하면 base.m_ml->ml_name도 UTF-8로 인코딩된 이름으로 갱신됩니다.

_ml 필드는 base.m_ml이 사용할 공간을 예약합니다.

base_function 인스턴스는 function의 인스턴스일 때, 그리고 그때에만 METH_PYTHON 플래그가 설정되어 있어야 합니다.

code와 globals로부터 function의 인스턴스를 생성할 때, 인스턴스는 base.m_ml = &_ml, base.m_self = NULL로 생성됩니다.

서브클래싱을 더 쉽게 하기 위해 복사 생성자도 추가합니다. f가 function의 인스턴스이면 types.FunctionType(f)는 f를 복사합니다. 이를 통해 사용자 정의 함수 타입을 데코레이터로 편리하게 사용할 수 있습니다:

>>> from types import FunctionType
>>> class CustomFunction(FunctionType):
...     pass
>>> @CustomFunction
... def f(x):
...     return x
>>> type(f)
<class '__main__.CustomFunction'>

이는 또한 functools.wraps의 많은 사용 사례를 없애 줍니다. 래퍼를 function의 서브클래스로 대체할 수 있기 때문입니다.

bound_method

bound_method 클래스는 내부 함수의 클래스와 관계없이 모든 바운드 메서드에 사용됩니다. 이 클래스는 base_function에 새 속성 하나를 추가합니다: __func__는 그 함수를 가리킵니다.

bound_method는 메서드로 바인딩된 Python 함수에만 사용되던 기존 method 클래스를 대체합니다.

임의의 호출 가능 객체로부터 메서드를 생성할 수 있도록 하려 하기 때문에 문제가 복잡해집니다. 이 객체는 이미 바인딩된 메서드일 수도 있고, 단순히 base_function의 인스턴스가 아닐 수도 있습니다. 따라서 실제로는 두 종류의 메서드가 있습니다:

  • 임의의 호출 가능 객체에 대해서는 METH_PASS_FUNCTION 플래그가 설정된 고정된 단일 PyCFunctionDef 구조체를 사용합니다.
  • Self slicing을 가진 base_function 인스턴스(더 정확히 말하면 Py_TPFLAGS_BASEFUNCTION 플래그가 설정된 인스턴스)를 바인딩하는 메서드에 대해서는 대신 원래 함수의 PyCFunctionDef를 사용합니다. 이렇게 하면 바운드 메서드를 호출할 때 성능 손실이 전혀 없습니다. 이 경우 __func__ 속성은 여러 속성을 구현하는 데에만 사용되며, 메서드를 호출하는 데에는 사용되지 않습니다.

base_function으로부터 새 메서드를 생성할 때, self 객체가 __objclass__의 인스턴스인지 검사하고(부모로 클래스가 지정된 경우), 그렇지 않으면 TypeError를 발생시킵니다.

C 구조체는 다음과 같습니다:

PyTypeObject PyMethod_Type;

typedef struct {
    PyBaseFunctionObject base;
    PyObject *im_func;  /* __func__: function implementing the method; readonly */
} PyMethodObject;

base_function 인스턴스 호출하기

base_function 인스턴스에 대한 __call__의 구현을 명세합니다.

__objclass__ 검사하기

우선, 함수의 __parent__가 클래스이면 타입 검사를 수행합니다(이 경우 __objclass__가 __parent__의 별칭이 된다는 점을 상기하십시오). m_self가 NULL이면(확장 타입의 언바운드 메서드가 이 경우에 해당합니다), 함수는 적어도 하나의 위치 인자와 함께 호출되어야 하며, 첫 번째 인자(보통 self라고 부릅니다)는 __objclass__의 인스턴스여야 합니다. 그렇지 않으면 TypeError가 발생합니다.

바운드 메서드는 m_self != NULL이므로 __objclass__를 검사하지 않는다는 점에 유의하십시오. 대신 __objclass__ 검사는 메서드를 생성할 때 수행됩니다.

플래그

편의를 위해 새 상수를 정의합니다. METH_CALLFLAGS는 호출할 C 함수의 시그니처를 지정하는 PyCFunctionDef.ml_flags의 모든 플래그를 결합합니다. 이 값은 다음과 같습니다

METH_VARARGS | METH_FASTCALL | METH_NOARGS | METH_O | METH_KEYWORDS | METH_PASS_FUNCTION

위의 처음 네 플래그 중 정확히 하나만 설정되어야 하며, METH_VARARGS와 METH_FASTCALL만 METH_KEYWORDS와 결합할 수 있습니다. 이 규칙을 위반하면 정의되지 않은 동작이 됩니다.

함수 호출에 영향을 주는 새 플래그가 있는데, 바로 METH_PASS_FUNCTION과 METH_CALL_UNBOUND입니다. 일부 플래그는 이미 [5]에 문서화되어 있습니다. 나머지 플래그는 아래에서 설명합니다.

셀프 슬라이싱

함수가 m_self == NULL이고 METH_CALL_UNBOUND 플래그가 설정되어 있지 않으면, 첫 번째 위치 인자(있는 경우)는 *args에서 제거되고 대신 C 함수에 첫 번째 인자로 전달됩니다. 사실상 첫 번째 위치 인자는 __self__로 취급됩니다. 이는 바인딩되지 않은 메서드를 지원하기 위한 것으로, C 함수가 바인딩된 메서드 호출과 바인딩되지 않은 메서드 호출의 차이를 알 수 없도록 합니다. 이는 키워드 인자에는 어떠한 영향도 주지 않습니다.

이 과정을 셀프 슬라이싱이라고 하며, m_self == NULL이고 METH_CALL_UNBOUND가 설정되어 있지 않으면 함수가 셀프 슬라이싱을 가진다고 합니다.

셀프 슬라이싱을 가지는 METH_NOARGS 함수는 사실상 인자 하나, 즉 self를 가진다는 점에 유의하십시오. 마찬가지로, 셀프 슬라이싱을 가지는 METH_O 함수는 인자 두 개를 가집니다.

METH_PASS_FUNCTION

이 플래그가 설정되면, C 함수는 추가적인 첫 번째 인자, 즉 함수 자신(base_function 인스턴스)과 함께 호출됩니다. 특별한 경우로, 함수가 bound_method이면 메서드의 기저 함수가 전달됩니다(단, 재귀적으로 적용되지는 않습니다. bound_method가 bound_method를 감싸는 경우 __func__는 한 번만 적용됩니다).

예를 들어, 일반적인 METH_VARARGS 함수는 (PyObject *self, PyObject *args) 시그니처를 가집니다. METH_VARARGS | METH_PASS_FUNCTION을 사용하면 이는 (PyObject *func, PyObject *self, PyObject *args)가 됩니다.

METH_FASTCALL

이는 기존에 존재하지만 문서화되지 않은 플래그입니다. 이 플래그를 공식적으로 지원하고 문서화할 것을 제안합니다.

METH_KEYWORDS 없이 METH_FASTCALL 플래그가 설정되면, ml_meth 필드는 (PyObject *self, PyObject *const *args, Py_ssize_t nargs) 인자를 받는 PyCFunctionFast 타입입니다. 이러한 함수는 위치 인자만 받으며, 위치 인자는 길이가 nargs인 일반 C 배열 args로 전달됩니다.

METH_FASTCALL | METH_KEYWORDS 플래그가 설정되면, ml_meth 필드는 (PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) 인자를 받는 PyCFunctionFastKeywords 타입입니다. 위치 인자는 길이가 nargs인 C 배열 args로 전달됩니다. 키워드 인자의 값은 nargs 위치부터 시작하여 그 배열에 이어서 나옵니다. 키워드 인자의 키(이름)는 kwnames에 tuple로 전달됩니다. 예를 들어, 위치 인자 3개와 키워드 인자 2개가 주어졌다고 가정합시다. 그러면 args는 길이가 3 + 2 = 5인 배열이고, nargs는 3이며, kwnames는 2-튜플입니다.

내장 함수의 자동 생성

Python은 확장 타입(PyTypeObject.tp_methods 필드 사용)과 모듈(PyModuleDef.m_methods 필드 사용)에 대해 cfunction의 인스턴스를 자동으로 생성합니다. PyTypeObject.tp_methods와 PyModuleDef.m_methods 배열은 PyMethodDef 구조체의 배열이어야 합니다.

확장 타입의 바인딩되지 않은 메서드

언바운드 메서드의 타입이 method_descriptor에서 cfunction으로 바뀝니다. 언바운드 메서드로 나타나는 객체는 클래스 __dict__에 나타나는 객체와 같은 객체입니다. Python은 __parent__ 속성을 정의하는 클래스로 자동 설정합니다.

모듈의 내장 함수

모듈의 함수인 경우, __parent__는 해당 모듈로 설정됩니다. METH_BINDING 플래그가 주어지지 않으면 __self__도 모듈로 설정됩니다(하위 호환성을 위해서입니다).

중요한 결과로, 이러한 함수는 속성으로 사용될 때 기본적으로 메서드가 되지 않습니다(base_function.__get__은 m_self가 NULL이었을 때만 그렇게 합니다). 이를 버그로 볼 수도 있지만, 하위 호환성 때문에 이렇게 했습니다: python-ideas에 올라온 최초 게시물 [6]에서 내장 함수의 이 잘못된 기능을 유지하자는 데 합의가 이루어졌습니다.

그러나 특정 내장 함수나 새로 구현되는 내장 함수에서는 그래도 이를 허용할 수 있도록, METH_BINDING 플래그는 __self__가 설정되지 않게 합니다.

추가 변경 사항

새로운 타입 플래그

새로운 PyTypeObject 플래그(tp_flags용)가 추가됩니다: Py_TPFLAGS_BASEFUNCTION은 이 타입의 인스턴스가 base_function처럼 호출될 수 있고 메서드로 바인딩될 수 있는 함수임을 나타냅니다.

이는 Py_TPFLAGS_LIST_SUBCLASS 같은 플래그와는 다른데, 단순히 서브클래스임을 나타내는 것 이상을 의미하기 때문입니다: __call__과 __get__의 기본 구현도 나타냅니다. 특히, base_function의 이러한 서브클래스는 Calling base_function instances 절의 구현을 따라야 합니다.

이 플래그는 base_function으로부터 tp_call과 tp_descr_get 구현을 상속하는 확장 타입에 대해 자동으로 설정됩니다. 확장 타입이 __call__이나 __get__을 호환되는 방식으로 재정의하는 경우에는 이 플래그를 명시적으로 지정할 수 있습니다. Py_TPFLAGS_BASEFUNCTION 플래그는 힙 타입에 절대 설정해서는 안 됩니다. 안전하지 않기 때문입니다(힙 타입은 동적으로 변경될 수 있습니다).

C API 함수

관련된 몇 가지 Python/C API 매크로와 함수를 나열합니다. 이 중 일부는 기존 함수(변경되었을 수 있음)이고, 일부는 새로운 함수입니다:

  • int PyBaseFunction_CheckFast(PyObject *op): op가 Py_TPFLAGS_BASEFUNCTION이 설정된 클래스의 인스턴스이면 참을 반환합니다. base_function 내부에 접근하는 것이 의미 있는지 판단하려면 이 함수를 사용해야 합니다.
  • int PyBaseFunction_Check(PyObject *op): op가 base_function의 인스턴스이면 참을 반환합니다.
  • PyObject *PyBaseFunction_New(PyTypeObject *cls, PyCFunctionDef *ml, PyObject *self, PyObject *module, PyObject *parent): 주어진 데이터로부터 cls의 새 인스턴스를 생성합니다(cls는 base_function의 서브클래스여야 합니다).
  • int PyCFunction_Check(PyObject *op): op가 cfunction의 인스턴스이면 참을 반환합니다.
  • int PyCFunction_NewEx(PyMethodDef* ml, PyObject *self, PyObject* module): cfunction의 새 인스턴스를 생성합니다. 특수한 경우로, self가 NULL이면 대신 self = Py_None으로 설정합니다(하위 호환성을 위해서입니다). self가 모듈이면 __parent__는 self로 설정됩니다. 그렇지 않으면 __parent__는 NULL입니다.
  • 기존의 여러 PyCFunction_... 및 PyMethod_ 함수에 대해, base_function 인스턴스에 작용하는 새 함수 PyBaseFunction_...을 정의합니다. 기존 함수는 새 함수의 별칭으로 유지됩니다.
  • int PyFunction_Check(PyObject *op): op가 METH_PYTHON 플래그가 설정된 base_function의 인스턴스이면 참을 반환합니다(이는 op가 function의 인스턴스인지 확인하는 것과 같습니다).
  • int PyFunction_CheckFast(PyObject *op): PyFunction_Check(op) && PyBaseFunction_CheckFast(op)와 동등합니다.
  • int PyFunction_CheckExact(PyObject *op): op의 타입이 function이면 참을 반환합니다.
  • PyObject *PyFunction_NewPython(PyTypeObject *cls, PyObject *code, PyObject *globals, PyObject *name, PyObject *qualname): 주어진 데이터로부터 cls(function의 서브클래스여야 합니다)의 새 인스턴스를 생성합니다.
  • PyObject *PyFunction_New(PyObject *code, PyObject *globals): function의 새 인스턴스를 생성합니다.
  • PyObject *PyFunction_NewWithQualName(PyObject *code, PyObject *globals, PyObject *qualname): function의 새 인스턴스를 생성합니다.
  • PyObject *PyFunction_Copy(PyTypeObject *cls, PyObject *func): 주어진 function을 복사하여 cls(function의 서브클래스여야 합니다)의 새 인스턴스를 생성합니다.

types 모듈의 변경 사항

두 개의 타입이 추가됩니다. base_function에 대응하는 types.BaseFunctionType과 defined_function에 대응하는 types.DefinedFunctionType입니다.

그 외에는 types 모듈에 아무런 변경도 없습니다. 특히 types.FunctionType은 function을 가리킵니다. 그러나 실제 타입은 바뀝니다. 특히 types.BuiltinFunctionType은 더 이상 types.BuiltinMethodType과 같지 않게 됩니다.

inspect 모듈의 변경 사항

새 함수 inspect.isbasefunction은 base_function의 인스턴스인지 검사합니다.

inspect.isfunction은 defined_function의 인스턴스인지 검사합니다.

inspect.isbuiltin은 cfunction의 인스턴스인지 검사합니다.

inspect.isroutine은 isbasefunction 또는 ismethoddescriptor를 검사합니다.

참고: bpo-33261 [3]을 먼저 수정해야 합니다.

프로파일링

현재 sys.setprofile은 내장 함수에 대해 c_call, c_return, c_exception 이벤트를 지원합니다. 이 이벤트들은 내장 함수를 호출하거나 내장 함수에서 반환될 때 생성됩니다. 반면 call과 return 이벤트는 함수 자체에 의해 생성됩니다. 따라서 call과 return 이벤트에 대해서는 아무것도 바꿀 필요가 없습니다.

더 이상 C 함수와 Python 함수를 구별하지 않으므로, Python 함수에 대해서는 c_* 이벤트가 발생하지 않도록 막아야 합니다. 이는 ml_flags에 METH_PYTHON 플래그가 설정되어 있으면 해당 이벤트를 생성하지 않는 방식으로 이루어집니다.

CPython 이외의 구현

이 PEP의 대부분은 CPython에만 해당합니다. 다른 Python 구현에 요구되는 변경 사항은 base_function 베이스 클래스와 function을 서브클래싱할 수 있다는 점, 이 두 가지입니다. cfunction과 defined_function 클래스는 필수가 아닙니다.

일관성을 위해 base_function을 요구하지만 이에 대해 어떠한 요구 사항도 두지 않습니다. 단순히 object의 복사본이어도 괜찮습니다. 새로운 __parent__(및 __objclass__) 속성의 지원은 필수가 아닙니다. defined_function 클래스가 없다면 types.DefinedFunctionType은 types.FunctionType의 별칭이어야 합니다.

근거

기존 클래스를 그냥 변경하지 않는 이유는 무엇입니까?

새로운 base_function 클래스를 도입하지 않고 기존 클래스를 유지한 채로 문제를 해결하려고 시도할 수도 있습니다.

그렇게 하는 편이 더 간단해 보일 수 있지만 실제로는 그렇지 않습니다. 이 방식에서는 function, builtin_function_or_method, method_descriptor라는 서로 다른 3개의 클래스에 대해 인트로스펙션을 지원해야 합니다. 뒤의 두 클래스의 경우, “인트로스펙션 지원”은 최소한 서브클래싱을 허용한다는 의미가 됩니다. 하지만 성능을 잃고 싶지는 않으므로 빠른 서브클래스 검사가 필요합니다. 이를 위해서는 tp_flags에 새로운 플래그 두 개가 필요합니다. 또한 내장 함수의 서브클래스가 __get__을 허용하기를 원하므로, 내장 함수에 대해서도 LOAD_METHOD 옵코드를 구현해야 합니다. 더 일반적으로 말하면, 많은 기능을 중복해서 구현해야 하며 최종 결과는 훨씬 더 복잡한 코드가 됩니다.

또한 내장 함수 서브클래스의 인트로스펙션이 __text_signature__와 어떻게 상호 작용할지도 명확하지 않습니다. 같은 클래스에 서로 독립적인 두 종류의 inspect.signature 지원을 두는 것은 문제를 자초하는 일로 보입니다.

그리고 이 방식으로는 Motivation에서 언급한 내장 함수와 Python 함수 사이의 다른 차이점 일부를 해결할 수 없습니다.

__text_signature__가 해결책이 아닌 이유

내장 함수에는 __text_signature__ 속성이 있으며, 이 속성은 함수의 시그니처를 일반 텍스트로 제공합니다. 기본값은 ast.literal_eval로 평가됩니다. 이 때문에 소수의 표준 Python 클래스만 지원하며 임의의 Python 객체는 지원하지 않습니다.

그리고 설령 __text_signature__가 어떻게든 임의의 시그니처를 허용하게 되더라도, 그것은 인트로스펙션의 한 부분에 불과합니다. 예를 들어 inspect.getsourcefile에는 도움이 되지 않습니다.

defined_function 대 function(함수)

여러 곳에서 기존 function 클래스를 defined_function으로 대체할지, 아니면 새로운 function 클래스로 대체할지 결정해야 합니다. 이 결정은 가장 가능성이 높은 사용 사례를 고려하여 내립니다.

  1. types.FunctionType은 function을 가리킵니다. 이 타입은 types.FunctionType(...)을 사용하여 인스턴스를 생성하는 데 쓰일 수 있기 때문입니다.
  2. inspect.isfunction()은 defined_function을 가리킵니다. 인트로스펙션이 지원되는 클래스가 바로 이 클래스이기 때문입니다.
  3. C API 함수는 function을 가리켜야 합니다. defined_function의 여러 속성이 어떻게 구현되는지는 명시하지 않기 때문입니다. 일반적으로 C 확장이 인트로스펙션을 수행할 이유가 없으므로, 이는 문제가 되지 않을 것으로 예상합니다.

이 PEP의 범위: 어떤 클래스가 관련됩니까?

이 PEP의 주된 동기는 함수 클래스를 바로잡는 것이므로, 기존 클래스인 builtin_function_or_method와 function은 당연히 통합하고자 합니다.

내장 함수와 메서드는 같은 클래스를 가지므로, 바운드 메서드도 포함하는 것이 자연스러워 보입니다. 그리고 Python 함수에는 “언바운드 메서드”가 없으므로, 확장 타입에서도 언바운드 메서드를 없애는 것이 합리적입니다.

현재로서는 staticmethod, classmethod, classmethod_descriptor 클래스는 변경하지 않습니다. 이들을 base_function 클래스 계층 구조에 넣고 classmethod와 classmethod_descriptor를 통합하는 것은 분명 합리적입니다. 그러나 이 PEP는 이미 충분히 크므로, 이는 향후 개선 가능 사항으로 남겨 둡니다.

__init__이나 __eq__ 같은 확장 타입의 슬롯 래퍼는 일반 메서드와 상당히 다릅니다. 또한 보통은 foo.__getitem__(i) 대신 foo[i]라고 작성하므로 이들을 직접 호출하는 경우는 드뭅니다. 따라서 이 PEP에서는 이들을 다루지 않습니다.

Python에는 instancemethod 클래스도 있는데, 이는 바운드 메서드와 언바운드 메서드에 사용되던 Python 2 시절의 유물로 보입니다. 이 클래스에 아직 사용 사례가 있는지는 분명하지 않습니다. 어떤 경우든 이 PEP에서 이 클래스를 다룰 이유는 없습니다.

TODO: instancemethod를 폐지(deprecate)해야 합니까? CPython 3.7 내부에서는 전혀 사용되지 않는 것으로 보이지만, 외부 패키지에서는 사용하고 있을 수도 있지 않겠습니까?

METH_STATIC과 METH_CLASS를 다루지 않음

이 PEP에서 METH_STATIC과 METH_CLASS 플래그를 언급하는 부분은 거의 없습니다. 이 플래그들은 Automatic creation of built-in functions에서만 검사됩니다. staticmethod, classmethod 또는 classmethod_descriptor가 바인딩되면(즉, __get__이 호출되면) m_self != NULL인 base_function 인스턴스가 생성됩니다. classmethod의 경우에는 m_self가 메서드가 바인딩된 클래스이므로 이는 당연합니다. staticmethod의 경우 m_self로 임의의 Python 객체를 사용할 수 있습니다. 하위 호환성을 위해 확장 타입의 정적 메서드에는 m_self = __parent__를 택합니다.

base_function의 __self__

__self__ 슬롯을 bound_method가 아니라 base_function에 추가하는 것이 처음에는 이상해 보일 수 있습니다. 이 아이디어는 기존의 builtin_function_or_method 클래스에서 가져왔습니다. 이렇게 하면 이 PEP에서 다루는 여러 함수 클래스에 대해 __call__과 __get__을 하나의 일반적인 구현으로 제공할 수 있습니다.

또한 __self__를 모듈로 설정하는 기존 내장 함수도 쉽게 지원할 수 있습니다(예를 들어 sys.exit.__self__는 sys입니다).

__doc__의 두 가지 구현

base_function은 함수 독스트링을 지원하지 않습니다. 그 대신 cfunction과 function 클래스가 각자 고유한 방식으로 독스트링을 처리합니다(그리고 bound_method는 감싼 함수의 __doc__를 그대로 가져옵니다).

cfunction의 경우 독스트링은 (텍스트 시그니처와 함께) PyMethodDef의 읽기 전용 ml_doc 필드에 C 문자열로 저장됩니다. function의 경우 독스트링은 쓰기 가능한 Python 객체로 저장되며, 실제로 문자열일 필요도 없습니다. __doc__를 다루는 이 두 가지 매우 다른 방식을 통합하기는 어려워 보입니다. 하위 호환성을 위해 기존 구현을 유지합니다.

defined_function에 대해서는 __doc__를 구현하도록 요구하지만, 그 방식은 규정하지 않습니다. 서브클래스는 cfunction과 같은 방식으로, 또는 구조체 멤버를 사용하거나 다른 방식으로 __doc__를 구현할 수 있습니다.

서브클래싱

PyCFunction_Check와 PyMethod_Check에서 빠른 타입 검사가 가능하도록 cfunction과 bound_method의 서브클래싱을 허용하지 않습니다.

다른 클래스의 서브클래싱도 금지할 이유가 없으므로 허용합니다. Python 모듈에서 서브클래스를 만들 대상으로 의미 있는 유일한 클래스는 function입니다. 나머지 클래스는 어차피 인스턴스화할 수 없기 때문입니다.

tp_call 대체: METH_PASS_FUNCTION과 METH_CALL_UNBOUND

새 플래그 METH_PASS_FUNCTION과 METH_CALL_UNBOUND는 이전에 사용자 정의 tp_call을 사용하던 경우를 지원하기 위한 것입니다. 이 플래그들은 Python/ceval.c에서 객체를 호출하기 위한 특수한 빠른 경로의 수를 줄여 줍니다. Python 함수, 내장 함수, 메서드 디스크립터를 따로 처리하는 대신 단 한 번만 검사하면 됩니다.

tp_call의 시그니처는 본질적으로 METH_VARARGS | METH_KEYWORDS | METH_PASS_FUNCTION | METH_CALL_UNBOUND 플래그를 가진 PyBaseFunctionObject.m_ml.ml_meth의 시그니처와 같습니다(유일한 차이는 self 인자가 추가된다는 점입니다). 따라서 기존 tp_call 슬롯이 대신 base_function 구현을 사용하도록 바꾸는 일은 쉬울 것입니다.

C 함수가 __parent__와 같은 함수의 추가 메타데이터에 접근하기만 하면 되는 경우에는 METH_CALL_UNBOUND 없이 METH_PASS_FUNCTION을 사용하는 것도 합리적입니다. 예를 들어 PEP 573을 지원하려면 이것이 필요합니다. 기존 메서드가 METH_PASS_FUNCTION을 사용하도록 변환하는 일은 간단합니다. C 함수에 인자를 하나 추가하기만 하면 됩니다.

하위 호환성

이 PEP를 설계하면서 하위 호환성을 지나치게 깨뜨리지 않도록 세심한 주의를 기울였습니다. 호환되지 않을 가능성이 있는 변경 사항 대부분은 CPython 구현 세부 사항에 대한 변경이며, 이러한 세부 사항은 다른 Python 인터프리터에서는 어차피 다릅니다. 특히 PyPy에서 올바르게 실행되는 Python 코드는 이 PEP가 적용되어도 계속 동작할 가능성이 매우 높습니다.

staticmethod, functools.partial, operator.methodcaller와 같은 표준 클래스와 함수는 전혀 바꿀 필요가 없습니다.

types와 inspect의 변경 사항

types와 inspect에 대해 제안된 변경 사항은 동작 변화를 최소화하기 위한 것입니다. 그러나 일부 변화는 피할 수 없으며, 이로 인해 types나 inspect를 사용하는 코드가 깨질 수 있습니다. 예를 들어 Python 표준 라이브러리에서는 이 때문에 doctest 모듈을 변경해야 합니다.

또한 다양한 종류의 함수를 입력으로 받는 도구는 새로운 함수 계층 구조와 사용자 정의 함수 클래스가 있을 수 있다는 점에 대응해야 합니다.

Python 함수

Python 함수는 본질적으로 아무것도 바뀌지 않습니다. 기존에 있던 속성은 여전히 존재하며, Python 함수는 이전과 마찬가지로 초기화하고, 호출하고, 메서드로 변환할 수 있습니다.

하위 호환성을 위해 function이라는 이름을 유지합니다. 이름을 python_function처럼 더 구체적인 이름으로 바꾸는 것이 합리적일 수도 있지만, 그렇게 하면 문서와 테스트 스위트를 번거롭게 많이 고쳐야 합니다.

모듈의 내장 함수

내장 함수도 아무것도 바뀌지 않습니다. 이러한 함수는 메서드로 바인딩되지 않는다는 기존 동작을 유지합니다. 이는 __self__가 모듈로 설정된다는 사실에서 비롯된 결과입니다.

내장 바운드 메서드와 언바운드 메서드

내장 바인딩된 메서드와 바인딩되지 않은 메서드의 타입이 변경됩니다. 그러나 base_function.__call__의 프로토콜(특히 __objclass__ 처리와 self 슬라이싱)은 하위 호환성을 유지하도록 특별히 설계되었으므로, 이러한 메서드를 호출하는 데에는 영향이 없습니다. 기존에 있던 모든 속성(__objclass__와 __self__ 등)은 여전히 존재합니다.

새로운 속성

일부 객체에는 새로운 특수 이중 밑줄 속성이 추가됩니다. 예를 들어, 새로운 속성 __parent__가 모든 내장 함수에 나타나며, 모든 메서드에는 __func__ 속성이 추가됩니다. 이제 __self__가 Python 함수의 특수 읽기 전용 속성이 되었다는 점이 [4]에서 문제를 일으켰습니다. 그렇지만 일반적으로 깨지는 부분은 많지 않을 것으로 예상합니다.

method_descriptor와 PyDescr_NewMethod

method_descriptor 클래스와 PyDescr_NewMethod 생성자는 폐지 예정으로 지정되어야 합니다. 이들은 CPython 자체에서는 더 이상 사용되지 않지만 여전히 지원됩니다.

2단계 구현

TODO: 이 절은 선택 사항입니다. 이 PEP가 수락되면, 이 2단계 구현을 적용할지 여부를 결정해야 합니다.

위에서 언급했듯이, Changes to types and inspect는 일부 기존 코드를 깨뜨릴 수 있습니다. 깨지는 부분을 더욱 줄이기 위해, 이 PEP를 두 단계로 나누어 구현할 수 있습니다.

1단계: 기존 클래스를 유지하되 베이스 클래스를 추가

먼저 base_function 클래스를 구현하여 공통 베이스 클래스로 사용하고, 그 밖에는 기존 클래스를 유지합니다(단, 그 구현은 유지하지 않습니다).

이 제안에서 클래스 계층 구조는 다음과 같이 됩니다:

                      object
                         |
                         |
                  base_function
                 /       |     \
                /        |      \
               /         |       \
      cfunction          |     defined_function
       |     |           |         \
       |     |      bound_method    \
       |     |                       \
       |  method_descriptor       function
       |
builtin_function_or_method

말단 클래스인 builtin_function_or_method, method_descriptor, bound_method, function은 기존 클래스에 대응합니다(method는 bound_method로 이름이 바뀝니다).

모듈에서 자동으로 생성된 함수는 builtin_function_or_method의 인스턴스가 됩니다. 확장 타입의 바인딩되지 않은 메서드는 method_descriptor의 인스턴스가 됩니다.

method_descriptor 클래스는 __get__이 bound_method 대신 builtin_function_or_method를 반환한다는 점을 제외하면 cfunction의 복사본입니다.

builtin_function_or_method 클래스는 bound_method와 같은 C 구조체를 가지지만, cfunction을 상속합니다. __func__ 속성은 필수가 아닙니다. 이 속성은 method_descriptor를 바인딩할 때만 정의됩니다.

inspect 함수의 구현은 그대로 유지합니다. 이 때문에, 그리고 기존 클래스가 유지되기 때문에, 타입 검사를 하는 코드의 하위 호환성이 보장됩니다.

실제로 DeprecationWarning을 표시하면 올바르게 동작하는 많은 코드에 영향을 미치므로, 폐지 예정 사항은 모두 문서에만 표시됩니다. 또 다른 이유는 isinstance(x, t) 호출에 대해 경고를 표시하기가 어렵고(__instancecheck__를 조작하는 방식으로 가능하기는 합니다), type(x) is t에 대해서는 불가능하기 때문입니다.

2단계

2단계는 이 PEP의 나머지 부분에서 실제로 설명하는 내용입니다. 구현 측면에서 보면, 1단계에 비해 상대적으로 작은 변경이 될 것입니다.

참조 구현

이 PEP의 대부분은 CPython용으로 구현되어 있으며, 다음 주소에서 확인할 수 있습니다: https://github.com/jdemeyer/cpython/tree/pep575

구현은 네 단계로 이루어지며, 각 단계는 해당 브랜치의 커밋에 대응합니다. 각 단계가 끝난 뒤에도 CPython은 대체로 정상 작동하는 상태입니다.

  1. base_function 클래스를 추가하고 cfunction의 서브클래스로 만듭니다. 이 단계에서 __call__ 프로토콜 전체가 구현되므로 단연 가장 큰 단계입니다.
  2. method의 이름을 bound_method로 바꾸고 base_function의 서브클래스로 만듭니다. 확장 타입의 언바운드 메서드를 cfunction의 인스턴스로 변경하여, 확장 타입의 바운드 메서드도 bound_method의 인스턴스가 되도록 합니다.
  3. defined_function과 function을 구현합니다.
  4. 표준 라이브러리와 테스트 스위트 등 Python의 다른 부분을 변경합니다.

부록: 현재 상황

NOTE: 이 절은 PEP의 초안 기간에 더 유용하므로, PEP가 수락된 후에는 삭제하셔도 됩니다.

참고를 위해 CPython 3.7에 존재하는 관련 클래스를 자세히 설명합니다.

관련된 각 클래스는 “고아” 클래스입니다(자명하지 않은 서브클래스도 슈퍼클래스도 없습니다).

builtin_function_or_method: 내장 함수와 바운드 메서드

이들의 타입은 PyCFunction_Type 이며 구조체는 PyCFunctionObject입니다.:

typedef struct {
    PyObject_HEAD
    PyMethodDef *m_ml; /* Description of the C function to call */
    PyObject    *m_self; /* Passed as 'self' arg to the C func, can be NULL */
    PyObject    *m_module; /* The __module__ attribute, can be anything */
    PyObject    *m_weakreflist; /* List of weak references */
} PyCFunctionObject;

struct PyMethodDef {
    const char  *ml_name;   /* The name of the built-in function/method */
    PyCFunction ml_meth;    /* The C function that implements it */
    int         ml_flags;   /* Combination of METH_xxx flags, which mostly
                               describe the args expected by the C func */
    const char  *ml_doc;    /* The __doc__ attribute, or NULL */
};

여기서 PyCFunction은 C 함수 포인터입니다(여러 형태가 있으며, 가장 기본적인 형태는 self와 *args에 해당하는 두 인자를 받습니다).

이 클래스는 함수와 바운드 메서드 모두에 사용됩니다. 메서드의 경우 m_self 슬롯이 해당 객체를 가리킵니다.:

>>> dict(foo=42).get
<built-in method get of dict object at 0x...>
>>> dict(foo=42).get.__self__
{'foo': 42}

어떤 경우에는 함수가 그 함수를 정의하는 모듈의 “메서드”로 간주됩니다.:

>>> import os
>>> os.kill
<built-in function kill>
>>> os.kill.__self__
<module 'posix' (built-in)>

method_descriptor: 내장 언바운드 메서드

이들의 타입은 PyMethodDescr_Type 이며 구조체는 PyMethodDescrObject입니다.:

typedef struct {
    PyDescrObject d_common;
    PyMethodDef *d_method;
} PyMethodDescrObject;

typedef struct {
    PyObject_HEAD
    PyTypeObject *d_type;
    PyObject *d_name;
    PyObject *d_qualname;
} PyDescrObject;

function: Python 함수

이들의 타입은 PyFunction_Type 이며 구조체는 PyFunctionObject입니다.:

typedef struct {
    PyObject_HEAD
    PyObject *func_code;        /* A code object, the __code__ attribute */
    PyObject *func_globals;     /* A dictionary (other mappings won't do) */
    PyObject *func_defaults;    /* NULL or a tuple */
    PyObject *func_kwdefaults;  /* NULL or a dict */
    PyObject *func_closure;     /* NULL or a tuple of cell objects */
    PyObject *func_doc;         /* The __doc__ attribute, can be anything */
    PyObject *func_name;        /* The __name__ attribute, a string object */
    PyObject *func_dict;        /* The __dict__ attribute, a dict or NULL */
    PyObject *func_weakreflist; /* List of weak references */
    PyObject *func_module;      /* The __module__ attribute, can be anything */
    PyObject *func_annotations; /* Annotations, a dict or NULL */
    PyObject *func_qualname;    /* The qualified name */

    /* Invariant:
     *     func_closure contains the bindings for func_code->co_freevars, so
     *     PyTuple_Size(func_closure) == PyCode_GetNumFree(func_code)
     *     (func_closure may be NULL if PyCode_GetNumFree(func_code) == 0).
     */
} PyFunctionObject;

Python 3에는 “언바운드 메서드” 클래스가 없습니다. 언바운드 메서드는 그저 평범한 함수입니다.

method: Python 바운드 메서드

이들의 타입은 PyMethod_Type 이며 구조체는 PyMethodObject입니다.:

typedef struct {
    PyObject_HEAD
    PyObject *im_func;   /* The callable object implementing the method */
    PyObject *im_self;   /* The instance it is bound to */
    PyObject *im_weakreflist; /* List of weak references */
} PyMethodObject;

참고 문헌