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

Python 개선 제안 한국어 번역

PEP 573 – C 확장 메서드에서 모듈 상태에 접근하기

Author:
Petr Viktorin <encukou at gmail.com>, Alyssa Coghlan <ncoghlan at gmail.com>, Eric Snow <ericsnowcurrently at gmail.com>, Marcel Plch <gmarcel.plch at gmail.com>
BDFL-Delegate:
Stefan Behnel
Discussions-To:
Import-SIG list
Status:
Final
Type:
Standards Track
Created:
02-Jun-2016
Python-Version:
3.9
Post-History:


Table of Contents

번역·라이선스 안내

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

초록

이 PEP는 CPython 확장 메서드가 자신이 정의된 모듈의 상태와 같은 컨텍스트에 접근할 수 있는 방법을 추가할 것을 제안합니다.

이를 통해 확장 메서드는 모듈 상태를 조회할 때 PyState_FindModule 대신 직접 포인터 역참조를 사용할 수 있게 되어, 프로세스 전역 상태 대신 모듈 범위 상태를 사용할 때 드는 성능 비용을 줄이거나 없앨 수 있습니다.

이것은 PEP 3121(확장 모듈 초기화 및 종료 처리)과 PEP 489(다단계 확장 모듈 초기화)의 도입을 가로막던 남은 걸림돌 중 하나를 해결합니다.

이 PEP는 PEP 3121과 PEP 489가 다루기 시작한 문제를 완전히 해결하기 위해 한 걸음 더 나아가지만, 남아 있는 모든 문제를 해결하려 하지는 않습니다. 특히, 슬롯 메서드(nb_add 등)에서 모듈 상태에 접근하는 문제는 해결하지 않습니다.

용어

프로세스 전역 상태

C 수준의 정적 변수입니다. 이는 매우 저수준의 메모리 저장소이므로 신중하게 관리해야 합니다.

모듈별 상태

모듈 객체에 국한된 상태로, 모듈 객체 초기화 과정의 일부로 동적으로 할당됩니다. 이를 통해 해당 상태는 모듈의 다른 인스턴스(다른 서브인터프리터에 있는 인스턴스 포함)로부터 격리됩니다.

PyModule_GetState()로 접근합니다.

정적 타입

C 수준의 정적 변수로 정의된 타입 객체, 즉 컴파일 시에 포함되는 타입 객체입니다.

정적 타입은 모듈 인스턴스 간에 공유되어야 하며, 자신이 어느 모듈에 속하는지에 대한 정보를 갖고 있지 않습니다. 정적 타입에는 __dict__가 없습니다(그 인스턴스에는 있을 수 있습니다).

힙 타입

실행 시간에 생성되는 타입 객체입니다.

정의 클래스

메서드(바인딩되었든 바인딩되지 않았든)의 정의 클래스는 해당 메서드가 정의된 클래스입니다. 단지 베이스 클래스로부터 메서드를 상속받기만 한 클래스는 정의 클래스가 아닙니다.

예를 들어, int는 True.to_bytes, True.__floor__, int.__repr__의 정의 클래스입니다.

C에서 정의 클래스는 해당 tp_methods 또는 “tp 슬롯” [1] 항목으로 정의된 클래스입니다. Python에서 정의된 메서드의 경우, 정의 클래스는 __class__ 클로저 셀에 저장됩니다.

C-API

Python 문서에 설명된 “Python/C API”입니다. CPython은 C-API를 구현하지만, 다른 구현도 존재합니다.

근거

PEP 489는 확장 모듈을 초기화하는 새로운 방법을 도입했으며, 이 방법은 이를 구현하는 확장 모듈에 다음과 같은 여러 이점을 제공합니다:

  • 확장 모듈이 대응하는 Python 모듈과 더 비슷하게 동작합니다.
  • 확장 모듈이 기존 모듈 객체에 로드되는 것을 쉽게 지원할 수 있으므로, runpy의 확장 모듈 지원이나 확장 모듈 다시 로드를 가능하게 하는 시스템으로 가는 길이 열립니다.
  • 같은 확장에서 여러 모듈을 로드할 수 있으므로, 단일 인터프리터에서 모듈 격리(올바른 서브인터프리터 지원의 핵심 기능)를 테스트할 수 있습니다.

PEP 489 채택의 가장 큰 걸림돌은 확장 타입의 메서드에서 모듈 상태에 접근할 수 있게 하는 것입니다. 현재 확장 메서드에서 이 상태에 접근하려면 PyState_FindModule로 모듈을 조회해야 합니다(모듈 참조를 인자로 받는 확장 모듈의 모듈 수준 함수와는 대조적입니다). 그러나 PyState_FindModule은 스레드 로컬 상태를 조회하므로 C 수준의 프로세스 전역 접근에 비해 비용이 상대적으로 크며, 그 결과 모듈 작성자들이 이를 사용하기를 꺼리게 됩니다.

또한 PyState_FindModule은 각 서브인터프리터에 주어진 PyModuleDef에 대응하는 모듈이 최대 하나만 존재한다는 가정에 의존합니다. 이 가정은 PEP 489의 다단계 초기화를 사용하는 모듈에서는 성립하지 않으므로, 이러한 모듈에서는 PyState_FindModule을 사용할 수 없습니다.

확장 메서드에서 모듈 수준 상태에 접근하는 더 빠르고 안전한 방법이 필요합니다.

배경

Python 메서드를 구현할 때 다음 정보 중 하나 이상에 접근해야 할 수 있습니다:

  • 메서드가 호출된 인스턴스(self)
  • 기반 함수
  • 정의 클래스, 즉 메서드가 정의된 클래스
  • 대응하는 모듈
  • 모듈 상태

Python 코드에서는 이에 해당하는 Python 수준의 값을 다음과 같이 얻을 수 있습니다:

import sys

class Foo:
    def meth(self):
        instance = self
        module_globals = globals()
        module_object = sys.modules[__name__]  # (1)
        underlying_function = Foo.meth         # (1)
        defining_class = Foo                   # (1)
        defining_class = __class__             # (2)

Note

type(self)는 Foo의 서브클래스일 수 있으므로, 정의 클래스는 type(self)가 아닙니다.

(1)로 표시된 문은 함수의 __globals__를 통한 이름 기반 조회에 암묵적으로 의존합니다. 정의 클래스와 Python 함수 객체에 접근하기 위해 Foo 속성을 사용하거나, sys.modules에서 모듈 객체를 찾기 위해 __name__을 사용합니다.

Python 코드에서는 함수 정의가 실행될 때 __globals__가 적절히 설정되고, 네임스페이스가 조작되어 다른 객체를 반환하더라도 최악의 경우 예외가 발생할 뿐이므로 이 방법을 쓸 수 있습니다.

__class__ 클로저 (2)는 정의 클래스를 얻는 더 안전한 방법이지만, 여전히 __closure__가 적절히 설정되어 있어야 합니다.

반면 확장 메서드는 일반적으로 보통의 C 함수로 구현됩니다. 이는 확장 메서드가 자신의 인자와 C 수준의 스레드 로컬 상태 및 프로세스 전역 상태에만 접근할 수 있다는 뜻입니다. 전통적으로 많은 확장 모듈이 공유 상태를 C 수준의 프로세스 전역 변수에 저장해 왔으며, 이는 다음과 같은 경우에 문제를 일으킵니다:

  • 같은 프로세스에서 초기화/종료 주기를 여러 번 실행하는 경우
  • 모듈을 다시 로드하는 경우(예: 조건부 임포트를 테스트하기 위해)
  • 서브인터프리터에서 확장 모듈을 로드하는 경우

PEP 3121은 PyState_FindModule API를 제공하여 이를 해결하려 했지만, (모듈 수준 함수가 아닌) 확장 메서드의 경우에는 여전히 중대한 문제가 있습니다:

  • C 수준의 프로세스 전역 상태에 직접 접근하는 것보다 현저히 느립니다
  • 프로세스 전역 상태에 대한 본질적인 의존이 여전히 일부 남아 있어, 모듈 다시 로드를 여전히 안정적으로 처리하지 못합니다

또한 모듈 상태와 같은 C 수준 구조체를 조회할 때 예상치 못한 객체 레이아웃이 주어지면 인터프리터가 충돌할 수 있으므로, 확장 메서드가 기대하는 종류의 객체를 받도록 보장하는 것이 훨씬 더 중요합니다.

제안

현재 바인딩된 확장 메서드(PyCFunction 또는 PyCFunctionWithKeywords)는 self와, (해당하는 경우) 전달된 위치 인자와 키워드 인자만 받습니다.

모듈 수준 확장 함수는 이미 self 인자를 통해 자신을 정의한 모듈 객체에 접근할 수 있지만, 확장 타입의 메서드에는 그런 여유가 없습니다. 이 메서드는 self를 통해 바인딩된 인스턴스를 받으므로 자신을 정의한 클래스나 모듈 수준 상태에 직접 접근할 수 없습니다.

위에서 설명한 추가적인 모듈 수준 컨텍스트는 두 가지를 변경하면 제공할 수 있습니다. 두 가지 추가 사항은 모두 선택 사항이며, 확장 작성자가 이를 사용하려면 명시적으로 옵트인해야 합니다:

  • 힙 타입 객체에 모듈에 대한 포인터를 추가합니다.
  • 정의한 클래스를 하부 C 함수에 전달합니다.

    CPython에서는 내장 메서드 객체(PyCFunctionObject)가 생성될 때 정의한 클래스를 바로 얻을 수 있으므로, PyCFunctionObject를 확장하는 새 구조체에 이를 저장할 수 있습니다.

그러면 PyModule_GetState를 통해 모듈 객체에서 모듈 상태를 가져올 수 있습니다.

이 제안에 따르면, 메서드가 Per-module State에 접근해야 하는 타입은 모두 정적 타입이 아닌 힙 타입이어야 한다는 점에 유의하십시오. 이는 하나의 확장에서 여러 모듈 객체를 로드하는 것을 지원하기 위해 필요합니다. C 수준 전역 변수인 정적 타입은 자신이 어느 모듈 객체에 속하는지에 대한 정보를 갖고 있지 않기 때문입니다.

슬롯 메서드

위의 변경 사항은 tp_iter나 nb_add 같은 슬롯 메서드는 다루지 않습니다.

슬롯 메서드의 문제는 C API가 고정되어 있어서, 정의한 클래스를 전달하기 위한 새 인자를 단순히 추가할 수 없다는 점입니다. 이 문제에 대해 두 가지 해결책이 제안되었습니다:

  • MRO를 순회하여 클래스를 조회합니다. 이 방법은 비용이 클 수 있지만, 성능이 문제가 되지 않는 경우(예를 들어 모듈 수준 예외를 발생시킬 때)에는 사용할 수 있습니다.
  • 각 슬롯을 정의한 클래스에 대한 포인터를 별도의 테이블인 __typeslots__ [2]에 저장합니다. 이 방법은 기술적으로 구현할 수 있고 빠르지만, 변경 범위가 상당히 큽니다.

이 문제의 영향을 받는 모듈은 thread-local state나 PEP 567 context variables를 캐싱 메커니즘으로 사용하거나, 재로드에 적합한 자체 조회 캐싱 방식을 정의하는 방법도 선택할 수 있습니다.

이 문제의 일반적인 해결은 향후 PEP로 미룹니다.

명세

힙 타입에 모듈 참조 추가하기

모듈을 생성하기 위한 새 팩토리 메서드가 C-API에 추가됩니다.:

PyObject* PyType_FromModuleAndSpec(PyObject *module,
                                   PyType_Spec *spec,
                                   PyObject *bases)

이 메서드는 PyType_FromSpecWithBases와 동일하게 동작하며, 추가로 전달된 모듈 객체를 새 타입과 연결합니다. (CPython에서는 아래에 설명하는 ht_module을 설정합니다.)

또한 접근자 PyObject * PyType_GetModule(PyTypeObject *)가 제공됩니다. 이 함수는 타입에 연결된 모듈이 설정되어 있으면 그 모듈을 반환하고, 그렇지 않으면 TypeError를 설정하고 NULL을 반환합니다. 정적 타입이 주어지면 항상 TypeError를 설정하고 NULL을 반환합니다.

CPython에서 이를 구현하기 위해 PyHeapTypeObject 구조체에 새 멤버 PyObject *ht_module이 추가되며, 이 멤버는 연결된 모듈에 대한 포인터를 저장합니다. 이 멤버의 기본값은 NULL이며, 타입 객체가 생성된 후에는 수정하지 않아야 합니다.

ht_module 멤버는 서브클래스에 상속되지 않습니다. 이 멤버가 필요한 각 타입마다 PyType_FromSpecWithBases를 사용하여 설정해야 합니다.

일반적으로 ht_module이 설정된 클래스를 생성하면 해당 클래스와 모듈이 얽힌 참조 순환이 생깁니다. 모듈 해체는 성능에 민감한 작업이 아니고, 모듈 수준 함수도 대개 참조 순환을 만들기 때문에 이는 문제가 되지 않습니다. f_globals를 통한 함수 순환을 끊는 기존의 “모든 모듈 전역 변수를 None으로 설정”하는 코드가 ht_module을 통한 새로운 순환도 끊습니다.

확장 메서드에 정의 클래스 전달하기

PyMethodDef.ml_flags에서 사용할 새 시그니처 플래그 METH_METHOD가 추가됩니다. 개념적으로, 이 플래그는 함수 시그니처에 defining_class를 추가합니다. 초기 구현을 쉽게 하기 위해, 이 플래그는 (METH_FASTCALL | METH_KEYWORDS | METH_METHOD) 형태로만 사용할 수 있습니다. (METH_O나 단독 METH_FASTCALL 같은 다른 플래그와는 함께 사용할 수 없지만, METH_CLASS나 METH_STATIC과는 조합할 수 있습니다.)

이 플래그 조합을 사용해 정의된 메서드의 C 함수는 PyCMethod라는 새로운 C 시그니처로 호출됩니다.:

PyObject *PyCMethod(PyObject *self,
                    PyTypeObject *defining_class,
                    PyObject *const *args,
                    size_t nargsf,
                    PyObject *kwnames)

(METH_VARARGS | METH_METHOD) 같은 추가 조합은 향후에 (또는 이 PEP의 초기 구현에서도) 추가될 수 있습니다. 그러나 METH_METHOD는 항상 추가적인 플래그여야 합니다. 즉, 정의 클래스는 필요한 경우에만 전달되어야 합니다.

CPython에는 추가 정보를 담기 위해 PyCFunctionObject를 확장하는 새 구조체가 추가됩니다.:

typedef struct {
    PyCFunctionObject func;
    PyTypeObject *mm_class; /* Passed as 'defining_class' arg to the C func */
} PyCMethodObject;

PyCFunction 구현은 METH_METHOD 플래그가 설정된 것을 발견하면 mm_class를 PyCMethod C 함수에 전달합니다. mm_class에 더 쉽게 접근할 수 있도록 새 매크로 PyCFunction_GET_CLASS(cls)가 추가됩니다.

자신을 정의한 클래스/모듈에 접근할 필요가 없는 C 메서드는 다른 METH_* 시그니처를 계속 사용할 수 있습니다. METH_METHOD가 설정되지 않은 경우, PyCMethodObject로 캐스팅하는 것은 유효하지 않습니다.

Argument Clinic(인자 클리닉)

Argument Clinic(인자 클리닉)을 사용하는 메서드에 정의 클래스를 전달할 수 있도록, CPython의 Argument Clinic 도구에 defining_class라는 새 변환기가 추가됩니다.

각 메서드는 이 변환기를 사용하는 인자를 하나만 가질 수 있으며, 그 인자는 self 다음에 와야 하고, self를 사용하지 않는 경우에는 첫 번째 인자여야 합니다. 이 인자의 타입은 PyTypeObject *입니다.

이 변환기를 사용하면 Argument Clinic(인자 클리닉)은 METH_FASTCALL | METH_KEYWORDS | METH_METHOD를 호출 규약으로 선택합니다. 이 인자는 __text_signature__에 나타나지 않습니다.

새 변환기는 처음에는 METH_METHOD 규약을 사용할 수 없는 __init__ 및 __new__ 메서드와 호환되지 않습니다.

도우미 함수

힙 타입에서 Per-module State에 접근하는 것은 매우 흔한 작업입니다. 이를 쉽게 하기 위해 다음 도우미 함수가 추가됩니다.:

void *PyType_GetModuleState(PyObject *type)

이 함수는 힙 타입을 받아, 성공하면 해당 힙 타입이 속한 모듈의 상태를 가리키는 포인터를 반환합니다.

실패 시에는 두 가지 경우가 발생할 수 있습니다. 타입이 아닌 객체나 모듈이 없는 타입이 전달되면 TypeError가 설정되고 NULL이 반환됩니다. 모듈을 찾은 경우에는 NULL일 수도 있는 상태 포인터가 예외 설정 없이 반환됩니다.

초기 구현에서 변환되는 모듈

이 접근 방식을 검증하기 위해, 초기 구현 과정에서 _elementtree 모듈을 수정합니다.

API 변경 및 추가 사항 요약

다음 항목이 Python C-API에 추가됩니다:

  • PyType_FromModuleAndSpec 함수
  • PyType_GetModule 함수
  • PyType_GetModuleState 함수
  • METH_METHOD 호출 플래그
  • PyCMethod 함수 시그니처

다음 항목은 CPython 구현 세부 사항으로 추가되며, 문서화되지 않습니다:

  • PyCFunction_GET_CLASS 매크로
  • PyCMethodObject 구조체
  • _heaptypeobject의 ht_module 멤버
  • Argument Clinic(인자 클리닉)의 defining_class 변환기

하위 호환성

모든 힙 타입에 새 포인터 하나가 추가됩니다. 그 밖의 모든 변경 사항은 새 함수와 구조체를 추가하거나 비공개 구현 세부 사항을 변경하는 것입니다.

구현

초기 구현은 Github 저장소 [3]에서 볼 수 있으며, 패치 세트는 [4]에 있습니다.

향후 가능한 확장

슬롯 메서드

향후 슬롯 메서드에 정의 클래스(또는 모듈 상태)를 전달하는 방법이 추가될 수 있습니다.

이 PEP의 이전 버전에서는 특정 함수에 대한 슬롯을 정의하는 클래스를 MRO에서 검색하여 정의 클래스를 결정하는 헬퍼 함수를 제안했습니다. 그러나 이 방식은 클래스가 변경되면 실패합니다(힙 타입의 경우 Python 코드에서 클래스를 변경할 수 있습니다). 이 문제의 해결은 향후 논의로 남겨 둡니다.

모듈 참조를 가진 타입의 손쉬운 생성

PEP 489 실행 슬롯 타입을 추가하여 PyType_FromModuleAndSpec을 호출하는 것보다 힙 타입을 훨씬 쉽게 생성할 수 있도록 만들 수 있습니다. 이는 향후 PEP로 남겨 둡니다.

제한된 API에서 정적 예외 타입을 생성하는 좋은 방법을 추가하는 것도 좋을 수 있습니다. 이러한 예외 타입은 서브인터프리터 간에 공유되면서도 특정 모듈 상태 없이 인스턴스화될 수 있습니다. 이 또한 향후 가능한 논의로 남겨 둡니다.

최적화

여기에서 제안한 대로, METH_METHOD 플래그로 정의된 메서드는 특정 시그니처 하나만 지원합니다.

성능상의 이유로 다른 시그니처가 필요한 것으로 밝혀지면, 해당 시그니처가 추가될 수 있습니다.

참고 문헌