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

Python 개선 제안 한국어 번역

PEP 579 – C 함수 및 메서드 리팩터링

Author:
Jeroen Demeyer <J.Demeyer at UGent.be>
BDFL-Delegate:
Petr Viktorin
Status:
Final
Type:
Informational
Created:
04-Jun-2018
Post-History:
20-Jun-2018

Table of Contents

번역·라이선스 안내

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

승인 공지

이 PEP는 PEP 575, PEP 580, PEP 590(그리고 이후 제안에서 다뤄질 수도 있음)에서 다룬 설계 문제를 설명합니다.

앞서 PEP 1에서 언급했듯이:

정보 제공용 PEP는 반드시 Python 커뮤니티의 합의나 권고를 나타내는 것은 아니므로, 사용자와 구현자는 정보 제공용 PEP를 무시하거나 그 조언을 따를 수 있습니다.

이 PEP의 문제나 해결책이 타당한지에 대한 합의는 없지만, 이 목록은 추가 설계를 이끄는 데 여전히 유용합니다.

초록

이 메타 PEP는 CPython의 기존 내장 함수(C로 구현된 함수) 및 메서드 구현에 관한 다양한 문제를 정리합니다.

이러한 문제를 모두 하나의 PEP에서 해결하기에는 범위가 너무 크므로, 이는 다른 표준화 트랙 PEP에 위임합니다. 그러나 이 PEP는 가능한 해결책에 관한 간략한 아이디어를 제시합니다. 이는 주로 전반적인 전략을 조정하기 위한 것입니다. 예를 들어 제안된 해결책이 어느 한 가지 문제만 해결하기에는 너무 복잡해 보일 수 있지만, 여러 문제를 해결하는 전반적인 해결책으로는 가장 적합할 수 있습니다.

이 PEP는 순전히 정보 제공을 위한 것으로, 모든 문제가 결국 해결되거나 여기에서 제안한 해결책을 사용하여 해결된다는 의미는 아닙니다.

또한 가능한 요청 기능의 체크리스트 역할을 하여, 특정 수정 사항이 그러한 다른 기능을 구현하기 더 어렵게 만들지 않는지 확인하는 데 사용됩니다.

주요 제안 변경 사항은 PyMethodDef를 함수/메서드 호출에 필요한 모든 것을 수집하는 새로운 구조체 PyCCallDef로 교체하는 것입니다. PyTypeObject 구조체에 객체 구조체 내의 PyCCallDef *로 가는 오프셋을 제공하는 새 필드 tp_ccalloffset를 추가합니다.

NOTE: 이 PEP는 CPython 구현 세부 사항만 다루며, Python 언어나 표준 라이브러리에는 영향을 주지 않습니다.

문제

이 절에서는 내장 함수 및 메서드와 관련된 다양한 문제를 해결책에 대한 계획 및 (해당하는 경우) 세부 사항을 논의하는 표준화 트랙 PEP에 대한 참조와 함께 나열합니다.

1. 명명

Python에서 “내장”이라는 단어는 과도하게 사용됩니다. Python 문서를 빠르게 훑어보면, 이 단어는 대부분 builtins 모듈의 항목을 가리킵니다. 다시 말해, 가져올 필요 없이 전역 네임스페이스에서 사용할 수 있는 항목을 의미합니다. 이는 “C로 구현된”이라는 의미로 “내장”이라는 단어를 사용하는 것과 충돌합니다.

해결책: 내장 함수와 메서드의 C 구조체가 이미 PyCFunctionObject라고 불리므로, “내장 함수”와 “내장 메서드” 대신 “cfunction”과 “cmethod”라는 이름을 사용합시다.

2. 확장할 수 없음

관련된 다양한 클래스(예: builtin_function_or_method)는 서브클래스로 만들 수 없습니다.:

>>> from types import BuiltinFunctionType
>>> class X(BuiltinFunctionType):
...     pass
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: type 'builtin_function_or_method' is not an acceptable base type

이는 이러한 클래스에 인트로스펙션 지원과 같은 기능을 추가할 수 없게 만든다는 문제가 있습니다.

추가 기능을 갖춘 함수를 C로 구현하려면 완전히 새로운 클래스를 처음부터 구현해야 합니다. 문제는 builtin_function_or_method와 같은 기존 클래스가 더 빠른 호출을 허용하도록 Python 인터프리터에서 특수 처리된다는 점입니다(예를 들어 METH_FASTCALL을 사용합니다). 현재로서는 동일한 최적화를 적용하는 사용자 정의 클래스를 만들 수 없습니다.

해결책: 기존 최적화를 임의의 클래스에서 사용할 수 있게 합니다. 새로운 PyTypeObject 필드 tp_ccalloffset를 추가하여 이를 수행합니다(또는 이를 위해 tp_print을 재사용할 수 있습니까?). 이 필드는 PyCCallDef 포인터의 오프셋을 지정합니다. 이는 C 함수를 호출하는 데 필요한 모든 정보를 보유하는 새로운 구조체이며, PyMethodDef대신 사용됩니다. 이는 새로운 “C 호출” 프로토콜을 구현합니다.

C 함수와 C 메서드를 구성할 때는 PyMethodDef 배열을 계속 사용하지만(예를 들어 tp_methods에서), 이것이 PyMethodDef 구조체에 남는 유일한 용도입니다.

또한 일부 함수 클래스를 서브클래싱할 수 있게 만들 수도 있습니다. 그러나 tp_ccalloffset이 있으면 이는 덜 중요해 보입니다.

참조: PEP 580

3. C 함수는 메서드가 되지 않습니다.

repr와 같은 C 함수는 메서드로 바인딩하기 위한 __get__을 구현하지 않습니다.:

>>> class X:
...     meth = repr
>>> x = X()
>>> x.meth()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: repr() takes exactly one argument (0 given)

이 예제에서는 메서드의 일반적인 규칙을 적용하여 x.meth()repr(x)를 반환할 것으로 예상했을 것입니다.

이는 놀라운 일이며 C 함수와 Python 함수 사이에 불필요한 차이를 만듭니다. 표준 내장 함수의 경우에는 이것이 실제로 문제가 되지 않습니다. 이러한 함수는 메서드로 사용하도록 의도된 것이 아니기 때문입니다. 그러나 메서드로 사용할 수 있는 새로운 C 함수를 구현하려는 경우에는 문제가 됩니다.

다시 말해, C 함수처럼 동작하지만 메서드로 바인딩되는 새로운 클래스를 만드는 것이 해결책이 될 수 있습니다. 그러나 그렇게 하면 LOAD_METHOD/CALL_METHOD 연산 코드와 같은 메서드의 기존 최적화 일부를 잃게 됩니다.

해결책: 앞선 문제와 동일합니다. 이는 self__get__을 처리하는 것이 새로운 C 호출 프로토콜의 일부여야 한다는 점을 보여 줄 뿐입니다.

하위 호환성을 위해 C 함수의 기존 비바인딩 동작은 유지합니다. 다만 사용자 정의 클래스에서 이를 허용합니다.

참조: PEP 580

4. inspect.isfunction의 의미

현재 inspect.isfunctiontypes.FunctionType의 인스턴스에 대해서만 True를 반환합니다. 즉, 진정한 Python 함수입니다.

inspect.isfunction의 일반적인 사용 사례는 인트로스펙션을 확인하는 것입니다. 예를 들어 inspect.getfile()이 작동한다는 것을 보장합니다. 이상적으로는 다른 클래스도 함수로 취급될 수 있어야 합니다.

해결책: 새로운 InspectFunction추상 베이스 클래스를 도입하고 이를 사용하여 inspect.isfunction을 구현합니다. 또는 [2]에서 제안된 것처럼 inspect.isfunction에 덕 타이핑을 사용합니다.:

def isfunction(obj):
    return hasattr(type(obj), "__code__")

5. C 함수는 함수 객체에 접근할 수 있어야 합니다.

현재 C 함수의 기반이 되는 C 함수는 self인자(바인딩된 메서드의 경우)를 받은 다음, 필요에 따라 여러 인자를 받습니다. C 함수가 실제로 Python C 함수 객체에 접근할 방법은 없습니다(__call__ 또는 tp_call 에서의 self). 예를 들어 이를 통해 Python 함수(types.FunctionType)에 대한 C 호출 프로토콜을 구현할 수 있습니다. Python 함수 호출을 구현하는 C 함수는 함수의 __code__속성에 접근해야 합니다.

이는 PEP 573에서도 필요합니다. 여기서는 모든 cfunction이 해당 “부모”(모듈 함수의 경우 모듈, 메서드의 경우 이를 정의하는 클래스)에 접근해야 합니다.

해결책: C 함수가 추가 인자를 (첫 번째 인자로) 받는다는 것을 지정하는 새 PyMethodDef 플래그를 추가합니다. 이 인자는 함수 객체입니다.

참조: PEP 580, PEP 573

6. METH_FASTCALL은 비공개이며 문서화되지 않았습니다.

METH_FASTCALL 메커니즘을 사용하면 tuple 대신 파이썬 객체의 C 배열을 사용하여 cfunction과 cmethod를 호출할 수 있습니다. 이는 Python 3.6에서 위치 인자만 지원하도록 도입되었으며, Python 3.7에서는 키워드 인자 지원과 함께 확장되었습니다.

그러나 문서화되지 않았으므로 CPython 자체에서만 사용하도록 의도된 것으로 추정됩니다.

해결책: 이는 중요한 최적화이므로 모두가 이를 사용하도록 권장해야 합니다. 이제 METH_FASTCALL의 구현이 안정적이므로 이를 문서화하십시오!

C 호출 프로토콜의 일부로 C API 함수도 추가해야 합니다.

PyObject *PyCCall_FastCall(PyObject *func, PyObject *const *args, Py_ssize_t nargs, PyObject *keywords)

참조: PEP 580

7. 네이티브 C 인자 허용

cfunction은 항상 인자를 파이썬 객체로 받습니다(예를 들어 PyObject 포인터의 배열로 받습니다). cfunction이 실제로 네이티브 C 함수를 감싸는 경우(예를 들어 ctypes에서 가져오거나 Cython과 같은 컴파일러에서 가져오는 경우)에는 비효율적입니다. C 코드에서 C 코드로 호출할 때 인자를 전달하려면 파이썬 객체를 사용해야 합니다.

C 데이터에 대한 접근을 허용하는 버퍼 프로토콜과 마찬가지로, 기반 C 호출 가능 객체에 대한 접근도 허용해야 합니다.

해결책: 네이티브 인자를 사용하는 C 함수(예를 들어 C long)를 cfunction 내부에서 감쌀 때, 해당 C 시그니처와 함께 기반 C 함수에 대한 함수 포인터도 저장해야 합니다.

Argument Clinic은 “impl” 함수에 대한 포인터를 저장하여 이를 자동으로 수행할 수 있습니다.

8. 복잡성

모든 메서드 변형을 구현하는 데 관여하는 클래스가 매우 많습니다. 그 자체로는 문제가 아니지만, 문제가 누적되는 요인입니다.

일반적인 Python 클래스의 경우, 아래 표는 다양한 종류의 메서드에 해당하는 클래스를 보여 줍니다. 열은 클래스 __dict__에 있는 클래스, 비바운드 메서드(클래스에 바인딩됨)의 클래스, 바운드 메서드(인스턴스에 바인딩됨)의 클래스를 나타냅니다.

종류 __dict__ 비바운드 바운드
일반 메서드 function function method
정적 메서드 staticmethod function function
클래스 메서드 classmethod method method
슬롯 메서드 function function method

다음은 확장 형식(C 클래스)에 해당하는 표입니다:

종류 __dict__ 바인딩되지 않음 바인딩됨
일반 메서드 method_descriptor method_descriptor builtin_function_or_method
정적 메서드 staticmethod builtin_function_or_method builtin_function_or_method
클래스 메서드 classmethod_descriptor builtin_function_or_method builtin_function_or_method
슬롯 메서드 wrapper_descriptor wrapper_descriptor method-wrapper

많은 클래스가 관련되어 있으며, 이 두 테이블은 매우 다르게 보입니다. Python 메서드가 C 메서드와 근본적으로 다르게 취급되어야 할 타당한 이유는 없습니다. 또한 기능도 약간 다릅니다. 예를 들어 method__func__를 지원하지만 builtin_function_or_method는 지원하지 않습니다.

CPython은 이러한 객체 대부분에 대한 호출을 최적화하므로, 이를 처리하는 코드도 복잡해질 수 있습니다. 이에 대한 좋은 예가 Python/ceval.ccall_function함수입니다.

해결책: 이러한 모든 클래스는 C 호출 프로토콜을 구현해야 합니다. 그러면 타입 검사를 수행하는 대신 C 호출 프로토콜(tp_ccalloffset != 0)을 확인하여 코드의 복잡성을 대부분 해결할 수 있습니다.

또한 이러한 클래스 중 일부를 병합할 수 있는지, 그리고 method를 확장 타입의 바운드 메서드에도 재사용할 수 있는지 조사해야 합니다(후자의 경우는 PEP 576을 참조하십시오. 여기에는 약간의 하위 호환성 문제가 있을 수 있다는 점을 염두에 두어야 합니다). 이는 그 자체가 목표라기보다는 이러한 클래스를 작업할 때 염두에 둘 사항입니다.

9. PyMethodDef는 지나치게 제한적입니다.

확장 모듈에서 cfunction 또는 cmethod를 생성하는 일반적인 방법은 PyMethodDef를 사용하여 정의하는 것입니다. 그런 다음 이는 cfunction의 경우 PyModuleDef.m_methods 배열에, cmethod의 경우 PyTypeObject.tp_methods 배열에 저장됩니다. 그러나 안정 ABI(PEP 384) 때문에 PyMethodDef 구조체를 변경할 수 없습니다.

따라서 이 방식으로 cfunction/cmethod를 생성하기 위한 새 필드를 추가할 수 없습니다. 이것이 아마도 __doc____text_signature__ 가 동일한 C 문자열에 저장되는 해킹의 이유일 것입니다(__doc____text_signature__ 디스크립터가 관련 부분을 추출합니다).

해결책: 단일 PyMethodDef항목으로 cfunction/cmethod를 설명하기에 충분하다고 가정하지 않는 것입니다. 대신 PyMethodDef 필드 중 하나가 추가 구조체를 가리키는 포인터임을 나타내는 플래그를 추가할 수 있습니다. 또는 배열에서 두 개 이상의 연속된 PyMethodDef 항목을 사용하여 더 많은 데이터를 저장하도록 플래그를 추가할 수도 있습니다. 그러면 PyMethodDef 배열은 cfunction/cmethod를 구성하는 데만 사용되고, 그 이후에는 더 이상 사용되지 않습니다.

10. 슬롯 래퍼에는 사용자 지정 문서가 없습니다.

현재 __init__이나 __lt__와 같은 슬롯 래퍼에는 매우 일반적인 문서만 있으며, 클래스에 전혀 특화되어 있지 않습니다.:

>>> list.__init__.__doc__
'Initialize self.  See help(type(self)) for accurate signature.'
>>> list.__lt__.__doc__
'Return self<value.'

시그니처에도 동일한 문제가 발생합니다.:

>>> list.__init__.__text_signature__
'($self, /, *args, **kwargs)'

보시다시피 슬롯 래퍼는 __doc____text_signature__를 지원합니다. 문제는 이것들이 특정 슬롯의 모든 래퍼에 공통인 struct wrapperbase에 저장된다는 점입니다(예를 들어 str.__eq__int.__eq__에 동일한 wrapperbase가 사용됩니다).

해결책: 각 인스턴스에 대해 문서 문자열(및 텍스트 시그니처)을 별도로 허용하도록 슬롯 래퍼 클래스를 재고하는 것입니다.

그러면 확장 모듈이 문서를 어떻게 지정해야 하는지에 대한 문제가 여전히 남습니다. PyTypeObjecttp_init과 같은 항목은 단순한 함수 포인터이므로, 이를 사용하여 할 수 있는 일은 없습니다. 한 가지 해결책은 문서 문자열을 추가하기 위한 항목만 tp_methods배열에 추가하는 것입니다. 이러한 항목은 다음과 같은 형태일 수 있습니다.

{"__init__", NULL, METH_SLOTDOC, "pointer to __init__ doc goes here"}

11. 정적 메서드와 클래스 메서드는 호출 가능 객체여야 합니다.

staticmethodclassmethod의 인스턴스는 호출 가능 객체여야 합니다. 인정하건대 이에 대한 강력한 사용 사례는 없지만, 종종 요청되어 왔습니다(예를 들어 [1] 참조).

정적/클래스 메서드를 호출 가능 객체로 만들면 일관성이 높아질 것입니다. 우선, 함수 데코레이터는 일반적으로 기능을 추가하거나 함수를 수정하지만, 결과는 여전히 호출 가능 객체로 남습니다. 이는 @staticmethod@classmethod에는 해당하지 않습니다.

둘째, 확장 타입의 클래스 메서드는 이미 호출 가능 객체입니다:

>>> fromhex = float.__dict__["fromhex"]
>>> type(fromhex)
<class 'classmethod_descriptor'>
>>> fromhex(float, "0xff")
255.0

셋째로, function, staticmethod, classmethod를 서로 다른 종류의 언바운드 메서드로 볼 수 있습니다: 이들 모두 바인딩되면 method가 되지만, __get__의 구현은 약간 다릅니다. 이 관점에서 보면, function은 호출 가능 객체인데 다른 것들은 그렇지 않다는 점이 이상해 보입니다.

해결책: staticmethod, classmethod의 구현을 변경할 때는 인스턴스를 호출 가능 객체로 만드는 것을 고려해야 합니다. 이것이 그 자체로 목표는 아니더라도, 구현으로 인해 자연스럽게 일어날 수 있습니다.

참고 문헌