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

Python 개선 제안 한국어 번역

PEP 489 – 다단계 확장 모듈 초기화

Author:
Petr Viktorin <encukou at gmail.com>, Stefan Behnel <stefan_ml at behnel.de>, Alyssa Coghlan <ncoghlan at gmail.com>
BDFL-Delegate:
Eric Snow <ericsnowcurrently at gmail.com>
Discussions-To:
Import-SIG list
Status:
Final
Type:
Standards Track
Created:
11-Aug-2013
Python-Version:
3.5
Post-History:
23-Aug-2013, 20-Feb-2015, 16-Apr-2015, 07-May-2015, 18-May-2015
Resolution:
Python-Dev message

Table of Contents

번역·라이선스 안내

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

Important

This PEP is a historical document. The up-to-date, canonical documentation can now be found at Initializing C modules. Python 3.14 이상에서는 Defining extension modules와 Module definition struct를 참조하십시오..

×

See PEP 1 for how to propose changes.

초록

이 PEP는 내장 모듈과 확장 모듈이 임포트 메커니즘과 상호 작용하는 방식을 재설계할 것을 제안합니다. 이 방식은 Python 3.0을 위해 PEP 3121에서 마지막으로 개정되었지만, 당시 모든 문제를 해결하지는 못했습니다. 목표는 확장 모듈이 Python 모듈의 동작 방식에 더 가깝게 동작하도록 하여 임포트 관련 문제를 해결하는 것이며, 구체적으로는 PEP 451에서 도입된 ModuleSpec 기반 로딩 메커니즘에 훅으로 연결하는 것입니다.

이 제안은 PEP 384의 PyType_Spec에서 영감을 얻어, 확장 모듈 작성자가 필요한 기능만 정의할 수 있게 하고 향후 확장 모듈 선언에 기능을 추가할 수 있게 합니다.

확장 모듈은 두 단계 과정을 거쳐 생성되며, 이는 ModuleSpec 아키텍처에 더 잘 들어맞고 클래스의 __new__와 __init__에 대응합니다.

확장 모듈은 임의의 C 수준 모듈별 상태를 모듈 안에 안전하게 저장할 수 있으며, 이 상태는 일반적인 가비지 컬렉션의 대상이 되고 다시 로드와 서브 인터프리터를 지원합니다. 확장 모듈 작성자는 새 API를 사용할 때 이러한 문제를 고려하는 것이 좋습니다.

또한 이 제안은 ASCII가 아닌 이름을 가진 확장 모듈도 허용합니다.

PEP 3121에서 다룬 문제가 이 제안에서 모두 해결되지는 않습니다. 특히 런타임 모듈 조회(PyState_FindModule)와 관련된 문제는 향후 PEP의 과제로 남겨 둡니다.

동기

Python 모듈과 확장 모듈은 같은 방식으로 설정되지 않습니다. Python 모듈의 경우, 모듈 객체가 먼저 생성되고 설정된 다음 모듈 코드가 실행됩니다(PEP 302). ModuleSpec 객체(PEP 451)는 모듈에 관한 정보를 담는 데 사용되며, 관련 훅에 전달됩니다.

확장 모듈(즉, 공유 라이브러리)과 내장 모듈의 경우, 모듈 초기화 함수가 곧바로 실행되어 생성과 초기화를 모두 수행합니다. 초기화 함수에는 ModuleSpec도, __file__이나 완전히 정규화된 이름처럼 ModuleSpec에 담긴 정보도 전혀 전달되지 않습니다. 이 때문에 상대 임포트와 리소스 로딩이 어려워집니다.

또한 Py3에서는 모듈이 sys.modules에 추가되지 않으므로, 모듈을 (전이적으로 일어날 수도 있는) 다시 임포트하면 실제로 모듈을 다시 임포트하려고 시도하게 되고, 결국 모듈 초기화 함수를 다시 실행하면서 무한 루프에 빠집니다. 완전한 모듈 이름에 접근할 수 없으면 모듈을 sys.modules에 올바르게 추가하는 것도 간단하지 않습니다. 이는 특히 Cython으로 생성된 모듈에서 문제가 되는데, 이런 모듈에서는 모듈 초기화 코드가 여느 ‘일반’ Python 모듈의 코드와 같은 수준으로 복잡한 경우가 드물지 않기 때문입니다. 또한 __file__과 __name__ 정보가 없으면 “__init__.py” 모듈, 즉 패키지를 컴파일하기가 어려워지며, 모듈 초기화 시점에 상대 임포트를 사용하는 경우에는 특히 그렇습니다.

게다가 현재 존재하는 확장 모듈의 대다수는 서브 인터프리터 지원이나 인터프리터 다시 로드, 또는 두 가지 모두에 문제가 있으며, 현재 인프라로도 이러한 기능을 지원할 수는 있지만 쉽지도 효율적이지도 않습니다. 이러한 문제를 해결하는 것이 PEP 3121의 목표였지만, 표준 라이브러리의 일부 확장 모듈을 포함한 많은 확장 모듈이 Python 3으로 이식할 때 가장 적은 노력이 드는 방식을 택하여 이러한 문제를 해결하지 않은 채로 남겨 두었습니다. 이 PEP는 하위 호환성을 유지하므로 부담을 줄이고, 확장 모듈 작성자가 이식할 때 이러한 문제를 고려할 충분한 시간을 줄 것입니다.

현재 과정

현재 확장 모듈과 내장 모듈은 공유 라이브러리의 파일 이름을 따서 이름 붙인 “PyInit_modulename”이라는 초기화 함수를 내보냅니다. 이 함수는 임포트 메커니즘에 의해 실행되며, 완전히 초기화된 모듈 객체를 반환해야 합니다. 이 함수는 인자를 받지 않으므로 자신의 임포트 컨텍스트를 알 방법이 없습니다.

모듈 초기화 함수는 실행 중에 PyModuleDef 객체를 바탕으로 모듈 객체를 생성합니다. 그런 다음 모듈 dict에 속성을 추가하고 타입을 생성하는 등의 방식으로 모듈 초기화를 이어 갑니다.

내부적으로 공유 라이브러리 로더는 마지막으로 로드한 모듈의 완전히 정규화된 모듈 이름을 기록해 둡니다. 이름이 일치하는 모듈이 생성되면 이 전역 변수를 사용하여 모듈 객체의 완전히 정규화된 이름을 결정합니다. 이 방식은 모듈 초기화 함수가 자신의 모듈 객체를 가장 먼저 생성한다는 가정에 의존하므로 완전히 안전하지는 않습니다. 하지만 실제로는 이 가정이 대체로 성립합니다.

제안

초기화 함수(PyInit_modulename)가 PyModuleDef 객체에 대한 포인터를 반환할 수 있게 됩니다. 임포트 절차가 모듈 객체를 생성하는 일을 맡습니다. 또한 초기화의 해당 단계마다 PyModuleDef에 제공된 훅을 호출하는 일도 맡습니다(아래에서 설명합니다).

이 다단계 초기화는 추가로 제공되는 선택지입니다. 완전히 초기화된 모듈 객체를 반환하는 현재 방식인 단일 단계 초기화도 계속 허용됩니다. 따라서 기존 코드는 바이너리 호환성을 포함하여 변경 없이 동작합니다.

PyModuleDef 구조체는 타입을 위한 PEP 384의 PyType_Spec과 유사하게 슬롯 목록을 포함하도록 변경됩니다. 바이너리 호환성을 유지하고 새로운 구조체를 도입하지 않아도 되도록, 현재 사용되지 않는 PyModuleDef의 m_reload 포인터를 슬롯을 담도록 변경합니다(새 구조체를 도입하면 지원 함수와 모듈별 저장 공간이 추가로 필요해집니다). 구조체는 다음과 같이 정의됩니다.:

typedef struct {
    int slot;
    void *value;
} PyModuleDef_Slot;

typedef struct PyModuleDef {
    PyModuleDef_Base m_base;
    const char* m_name;
    const char* m_doc;
    Py_ssize_t m_size;
    PyMethodDef *m_methods;
    PyModuleDef_Slot *m_slots;  /* changed from `inquiry m_reload;` */
    traverseproc m_traverse;
    inquiry m_clear;
    freefunc m_free;
} PyModuleDef;

m_slots 멤버는 NULL이거나, id가 0으로 설정된 슬롯(즉, {0, NULL})으로 끝나는 PyModuleDef_Slot 구조체 배열을 가리켜야 합니다.

슬롯을 지정하려면 고유한 슬롯 ID를 제공해야 합니다. 새로운 Python 버전에서 새로운 슬롯 ID가 도입될 수 있지만, 슬롯 ID는 절대 재사용되지 않습니다. 슬롯이 폐지될 수는 있지만, Python 3.x 전반에 걸쳐 계속 지원됩니다.

슬롯의 값 포인터는 해당 슬롯의 문서에서 달리 명시하지 않는 한 NULL일 수 없습니다.

현재 사용할 수 있는 슬롯은 다음과 같으며, 뒤에서 설명합니다:

  • Py_mod_create
  • Py_mod_exec

알 수 없는 슬롯 ID가 있으면 임포트가 SystemError와 함께 실패합니다.

다단계 초기화를 사용할 때는 임포트 중에 PyModuleDef의 m_name 필드가 사용되지 않습니다. 모듈 이름은 ModuleSpec에서 가져옵니다.

PyModuleDef 객체는 PyInit_*에서 반환되기 전에 새로 추가된 PyModuleDef_Init 함수를 사용하여 초기화되어야 합니다. 이 함수는 객체 타입(일부 컴파일러에서는 정적으로 설정할 수 없습니다), 참조 횟수, 내부 관리용 데이터(m_index)를 설정합니다. 예를 들어, 확장 모듈 “example”은 다음과 같이 익스포트됩니다.:

static PyModuleDef example_def = {...}

PyMODINIT_FUNC
PyInit_example(void)
{
    return PyModuleDef_Init(&example_def);
}

PyModuleDef 객체는 이로부터 생성된 모듈의 수명 동안 사용 가능해야 합니다. 보통은 정적으로 선언됩니다.

의사 코드 개요

다음은 수정된 임포터가 동작하는 방식에 대한 개요입니다. 로깅이나 오류 및 유효하지 않은 상태의 처리 같은 세부 사항은 생략하며, C 코드는 간결한 Python 스타일의 문법으로 나타냅니다.

임포터를 호출하는 프레임워크는 PEP 451에 설명되어 있습니다.

importlib/_bootstrap.py:

class BuiltinImporter:
    def create_module(self, spec):
        module = _imp.create_builtin(spec)

    def exec_module(self, module):
        _imp.exec_dynamic(module)

    def load_module(self, name):
        # use a backwards compatibility shim
        _load_module_shim(self, name)

importlib/_bootstrap_external.py:

class ExtensionFileLoader:
    def create_module(self, spec):
        module = _imp.create_dynamic(spec)

    def exec_module(self, module):
        _imp.exec_dynamic(module)

    def load_module(self, name):
        # use a backwards compatibility shim
        _load_module_shim(self, name)

Python/import.c (_imp 모듈):

def create_dynamic(spec):
    name = spec.name
    path = spec.origin

    # Find an already loaded module that used single-phase init.
    # For multi-phase initialization, mod is NULL, so a new module
    # is always created.
    mod = _PyImport_FindExtensionObject(name, name)
    if mod:
        return mod

    return _PyImport_LoadDynamicModuleWithSpec(spec)

def exec_dynamic(module):
    if not isinstance(module, types.ModuleType):
        # non-modules are skipped -- PyModule_GetDef fails on them
        return

    def = PyModule_GetDef(module)
    state = PyModule_GetState(module)
    if state is NULL:
        PyModule_ExecDef(module, def)

def create_builtin(spec):
    name = spec.name

    # Find an already loaded module that used single-phase init.
    # For multi-phase initialization, mod is NULL, so a new module
    # is always created.
    mod = _PyImport_FindExtensionObject(name, name)
    if mod:
        return mod

    for initname, initfunc in PyImport_Inittab:
        if name == initname:
            m = initfunc()
            if isinstance(m, PyModuleDef):
                def = m
                return PyModule_FromDefAndSpec(def, spec)
            else:
                # fall back to single-phase initialization
                module = m
                _PyImport_FixupExtensionObject(module, name, name)
                return module

Python/importdl.c:

def _PyImport_LoadDynamicModuleWithSpec(spec):
    path = spec.origin
    package, dot, name = spec.name.rpartition('.')

    # see the "Non-ASCII module names" section for export_hook_name
    hook_name = export_hook_name(name)

    # call platform-specific function for loading exported function
    # from shared library
    exportfunc = _find_shared_funcptr(hook_name, path)

    m = exportfunc()
    if isinstance(m, PyModuleDef):
        def = m
        return PyModule_FromDefAndSpec(def, spec)

    module = m

    # fall back to single-phase initialization
    ....

Objects/moduleobject.c:

def PyModule_FromDefAndSpec(def, spec):
    name = spec.name
    create = None
    for slot, value in def.m_slots:
        if slot == Py_mod_create:
            create = value
    if create:
        m = create(spec, def)
    else:
        m = PyModule_New(name)

    if isinstance(m, types.ModuleType):
        m.md_state = None
        m.md_def = def

    if def.m_methods:
        PyModule_AddFunctions(m, def.m_methods)
    if def.m_doc:
        PyModule_SetDocString(m, def.m_doc)

def PyModule_ExecDef(module, def):
    if isinstance(module, types.module_type):
        if module.md_state is NULL:
            # allocate a block of zeroed-out memory
            module.md_state = _alloc(module.md_size)

    if def.m_slots is NULL:
        return

    for slot, value in def.m_slots:
        if slot == Py_mod_exec:
            value(module)

모듈 생성 단계

모듈 객체의 생성, 즉 ExecutionLoader.create_module의 구현은 Py_mod_create 슬롯이 결정합니다.

Py_mod_create 슬롯

Py_mod_create 슬롯은 사용자 정의 모듈 서브클래스를 지원하는 데 사용됩니다. 값 포인터는 다음 시그니처를 갖는 함수를 가리켜야 합니다.:

PyObject* (*PyModuleCreateFunction)(PyObject *spec, PyModuleDef *def)

이 함수는 PEP 451에 정의된 ModuleSpec 인스턴스와 PyModuleDef 구조체를 받습니다. 이 함수는 새 모듈 객체를 반환하거나, 오류를 설정하고 NULL을 반환해야 합니다.

이 함수는 PEP 451에 명시된 임포트 관련 속성(__name__이나 __loader__ 등)을 새 모듈에 설정할 책임이 없습니다.

반환되는 객체가 types.ModuleType의 인스턴스일 필요는 없습니다. 속성의 설정과 조회를 지원한다면 어떤 타입이든 사용할 수 있으며, 이때 적어도 임포트 관련 속성은 설정하고 조회할 수 있어야 합니다. 그러나 모듈별 상태나 실행 슬롯 처리와 같은 모듈 고유의 기능은 ModuleType 인스턴스만 지원합니다. ModuleType 서브클래스가 아닌 다른 것이 반환되면 실행 슬롯을 정의할 수 없습니다. 실행 슬롯이 하나라도 정의되어 있으면 SystemError가 발생합니다.

이 함수가 호출될 때에는 sys.modules에 모듈 항목이 아직 채워지지 않았다는 점에 유의하십시오. 같은 모듈을 다시 임포트하려고 하면 (간접적으로 임포트하는 경우도 포함하여) 무한 루프에 빠질 수 있습니다. 확장 모듈 작성자는 Py_mod_create를 최소한으로 유지하고, 특히 그 안에서 사용자 코드를 호출하지 않는 것이 좋습니다.

Py_mod_create 슬롯은 여러 개 지정할 수 없습니다. 여러 개를 지정하면 임포트가 SystemError로 실패합니다.

Py_mod_create가 지정되지 않으면 임포트 절차는 PyModule_New를 사용하여 일반 모듈 객체를 만듭니다. 이름은 spec에서 가져옵니다.

생성 후 단계

Py_mod_create 함수가 types.ModuleType이나 그 서브클래스의 인스턴스를 반환하면 (또는 Py_mod_create 슬롯이 없으면), 임포트 절차는 PyModuleDef를 모듈과 연결합니다. 이를 통해 실행 단계, PyModule_GetDef 함수, 가비지 컬렉션 루틴(traverse, clear, free)에서도 PyModuleDef에 접근할 수 있게 됩니다.

Py_mod_create 함수가 모듈 서브클래스를 반환하지 않으면 m_size는 0이어야 하고, m_traverse, m_clear, m_free는 모두 NULL이어야 합니다. 그렇지 않으면 SystemError가 발생합니다.

또한 PyModuleDef에 명시된 초기 속성은 모듈 객체의 타입과 관계없이 모듈 객체에 설정됩니다:

  • m_doc이 NULL이 아니면 이 값으로 독스트링을 설정합니다.
  • m_methods가 있으면 이 값으로 모듈의 함수를 초기화합니다.

모듈 실행 단계

모듈 실행, 즉 ExecutionLoader.exec_module의 구현은 “실행 슬롯(execution slots)”이 관장합니다. 이 PEP는 Py_mod_exec 하나만 추가하지만, 앞으로 다른 슬롯이 추가될 수 있습니다.

실행 단계는 모듈 객체와 연관된 PyModuleDef에 대해 수행됩니다. PyModule_Type의 서브클래스가 아닌 객체(PyModule_GetDef가 실패하는 객체)의 경우 실행 단계를 건너뜁니다.

실행 슬롯은 여러 번 지정할 수 있으며, 슬롯 배열에 나타나는 순서대로 처리됩니다. 기본 임포트 메커니즘을 사용할 때, 이 슬롯들은 PEP 451에 명시된 임포트 관련 속성(__name__이나 __loader__ 등)이 설정되고 모듈이 sys.modules에 추가된 후에 처리됩니다.

실행 전 단계

실행 슬롯을 처리하기 전에 모듈에 대한 모듈별 상태(per-module state)가 할당됩니다. 이 시점부터 모듈별 상태는 PyModule_GetState를 통해 접근할 수 있습니다.

Py_mod_exec 슬롯

이 슬롯의 항목은 다음 시그니처를 가진 함수를 가리켜야 합니다:

int (*PyModuleExecFunction)(PyObject* module)

이 함수는 모듈을 초기화하기 위해 호출됩니다. 일반적으로 이는 모듈의 초기 속성을 설정하는 일에 해당합니다. “module” 인자는 초기화할 모듈 객체를 받습니다.

이 함수는 성공 시 0을 반환해야 하며, 오류 시에는 예외를 설정하고 -1을 반환해야 합니다.

PyModuleExec가 sys.modules에 있는 모듈의 항목을 교체하면, 모든 실행 슬롯이 처리된 후 importlib 메커니즘이 새 객체를 사용하고 반환합니다. 이는 임포트 메커니즘 자체의 기능입니다. 슬롯 자체는 모두 생성 단계에서 반환된 모듈을 사용하여 처리되며, 실행 단계 동안에는 sys.modules를 참조하지 않습니다. (확장 모듈의 경우, 사용자 정의 모듈 객체를 사용하려면 대개 Py_mod_create를 구현하는 편이 더 나은 해결책이라는 점에 유의하십시오.)

레거시 초기화

하위 호환성을 유지하는 단일 단계 초기화는 계속 지원됩니다. 이 방식에서는 PyInit 함수가 PyModuleDef 객체 대신 완전히 초기화된 모듈을 반환합니다. 이 경우 PyInit 훅이 생성 단계를 구현하며, 실행 단계는 아무 작업도 하지 않습니다.

이전 버전의 Python에서 변경 없이 동작해야 하는 모듈은 단일 단계 초기화를 유지해야 합니다. 다단계 초기화가 주는 이점은 백포트할 수 없기 때문입니다. 다음은 다단계 초기화를 지원하면서, 이전 버전의 CPython용으로 컴파일될 때는 단일 단계 초기화로 대체하는 모듈의 예입니다. 이 예는 주로 다단계 초기화를 활성화하는 데 필요한 변경 사항을 보여 주기 위해 포함되었습니다:

#include <Python.h>

static int spam_exec(PyObject *module) {
    PyModule_AddStringConstant(module, "food", "spam");
    return 0;
}

#ifdef Py_mod_exec
static PyModuleDef_Slot spam_slots[] = {
    {Py_mod_exec, spam_exec},
    {0, NULL}
};
#endif

static PyModuleDef spam_def = {
    PyModuleDef_HEAD_INIT,                      /* m_base */
    "spam",                                     /* m_name */
    PyDoc_STR("Utilities for cooking spam"),    /* m_doc */
    0,                                          /* m_size */
    NULL,                                       /* m_methods */
#ifdef Py_mod_exec
    spam_slots,                                 /* m_slots */
#else
    NULL,
#endif
    NULL,                                       /* m_traverse */
    NULL,                                       /* m_clear */
    NULL,                                       /* m_free */
};

PyMODINIT_FUNC
PyInit_spam(void) {
#ifdef Py_mod_exec
    return PyModuleDef_Init(&spam_def);
#else
    PyObject *module;
    module = PyModule_Create(&spam_def);
    if (module == NULL) return NULL;
    if (spam_exec(module) != 0) {
        Py_DECREF(module);
        return NULL;
    }
    return module;
#endif
}

내장 모듈

모든 확장 모듈은 실행 파일에 링크하고 inittab에 포함하면(런타임에 PyImport_AppendInittab을 사용하거나, 구성 시점에 freeze 같은 도구를 사용하여) 내장 모듈로 사용할 수 있습니다.

이 가능성을 유지하기 위해, 이 PEP에서 도입하는 확장 모듈 로딩에 대한 모든 변경 사항은 내장 모듈에도 적용됩니다. 유일한 예외는 아래에서 설명하는 비 ASCII 모듈 이름입니다.

서브인터프리터와 인터프리터 다시 로드

새로운 초기화 방식을 사용하는 확장은 Python 문서 [6]에서 언급한 문제를 피하면서 서브인터프리터와 여러 번의 Py_Initialize/Py_Finalize 주기를 올바르게 지원해야 합니다. 이 메커니즘은 이를 쉽게 할 수 있도록 설계되었지만, 확장 작성자는 여전히 주의를 기울여야 합니다. 사용자 정의 함수, 메서드, 인스턴스는 어느 것도 다른 인터프리터로 누출되어서는 안 됩니다. 이를 위해 모든 모듈 수준 상태는 모듈 딕셔너리나, PyModule_GetState로 접근할 수 있는 모듈 객체의 저장 공간 중 하나에 보관해야 합니다. 간단한 경험 법칙은 다음과 같습니다. 변경 가능하거나 사용자가 설정할 수 있는 클래스 속성이 없는 내장 타입을 제외하고는 어떤 정적 데이터도 정의하지 마십시오.

다단계 초기화와 호환되지 않는 함수

PyModule_Create 함수는 NULL이 아닌 m_slots 포인터를 가진 PyModuleDef 구조체에 사용하면 실패합니다. 이 함수는 다단계 초기화에 필요한 ModuleSpec 객체에 접근할 수 없습니다.

PyState_FindModule 함수는 NULL을 반환하며, PyState_AddModule과 PyState_RemoveModule도 NULL이 아닌 m_slots를 가진 모듈에서는 실패합니다. 같은 PyModuleDef에서 여러 모듈 객체가 생성될 수 있으므로 PyState 등록은 비활성화됩니다.

모듈 상태와 C 수준 콜백

PyState_FindModule을 사용할 수 없으므로, 모듈 수준 상태(모듈 수준에서 정의된 함수, 클래스, 예외 포함)에 접근해야 하는 모든 함수는 직접적으로든 간접적으로든 모듈 객체(또는 필요한 특정 객체)에 대한 참조를 받아야 합니다. 이는 현재 다음 두 가지 상황에서 어렵습니다.

  • 클래스에 대한 참조는 받지만 클래스의 모듈에 대한 참조는 받지 않는 클래스의 메서드
  • C 수준 콜백을 사용하는 라이브러리(콜백 등록 시 설정한 사용자 정의 데이터를 콜백이 받을 수 있는 경우는 제외)

이러한 경우를 해결하는 것은 이 PEP의 범위를 벗어나지만, 새 메커니즘이 모든 모듈에 유용하려면 해결이 필요합니다. 적절한 해결책은 import-sig 메일링 리스트 [5]에서 논의되었습니다.

경험적으로 볼 때, PyState_FindModule에 의존하는 모듈은 현재로서는 새 메커니즘으로 이식하기에 좋은 후보가 아닙니다.

새 함수

모듈 생성 단계를 구현하는 새 함수와 매크로가 추가됩니다. 이들은 PyModule_Create 및 PyModule_Create2와 비슷하지만, 추가로 ModuleSpec 인자를 받고 NULL이 아닌 슬롯을 가진 모듈 정의를 처리한다는 점이 다릅니다:

PyObject * PyModule_FromDefAndSpec(PyModuleDef *def, PyObject *spec)
PyObject * PyModule_FromDefAndSpec2(PyModuleDef *def, PyObject *spec,
                                    int module_api_version)

모듈 실행 단계를 구현하는 새 함수가 추가됩니다. 이 함수는 모듈별 상태를 할당하고(아직 할당되지 않은 경우), 실행 슬롯을 항상 처리합니다. 임포트 메커니즘은 모듈이 실행될 때 이 메서드를 호출하며, 모듈이 다시 로드되는 경우는 예외입니다:

PyAPI_FUNC(int) PyModule_ExecDef(PyObject *module, PyModuleDef *def)

PyModuleDef 객체를 초기화하는 또 다른 함수가 도입됩니다. 이 멱등 함수는 타입, 참조 횟수, 모듈 인덱스를 채웁니다. 이 함수는 인자를 PyObject*로 캐스트하여 반환하므로, PyInit 함수에서 바로 반환할 수 있습니다:

PyObject * PyModuleDef_Init(PyModuleDef *);

또한 모듈에 독스트링과 메서드를 설정하기 위한 두 개의 도우미가 추가됩니다:

int PyModule_SetDocString(PyObject *, const char *)
int PyModule_AddFunctions(PyObject *, PyMethodDef *)

내보내기 훅 이름

이식 가능한 C 식별자는 ASCII로 제한되므로, PyInit 훅 이름을 만들려면 모듈 이름을 인코딩해야 합니다.

ASCII 모듈 이름의 경우 임포트 훅의 이름은 PyInit_<modulename>이며, 여기서 <modulename>는 모듈의 이름입니다.

ASCII가 아닌 문자를 포함하는 모듈 이름의 경우 임포트 훅의 이름은 PyInitU_<encodedname>이며, 여기서 이름은 CPython의 “punycode” 인코딩(소문자 접미사를 사용하는 Punycode)으로 인코딩되고 하이픈(“-“)은 밑줄(“_”)로 대체됩니다.

Python에서는 다음과 같습니다:

def export_hook_name(name):
    try:
        suffix = b'_' + name.encode('ascii')
    except UnicodeEncodeError:
        suffix = b'U_' + name.encode('punycode').replace(b'-', b'_')
    return b'PyInit' + suffix

예시:

모듈 이름 초기화 훅 이름
spam PyInit_spam
lančmít PyInitU_lanmt_2sa6t
スパム PyInitU_zck5b2b

이름에 ASCII가 아닌 문자가 들어 있는 모듈에서는 단일 단계 초기화를 지원하지 않습니다.

이 PEP의 초기 구현에서는 이름에 ASCII가 아닌 문자가 들어 있는 내장 모듈을 지원하지 않습니다.

모듈 다시 로드하기

importlib.reload()를 사용하여 확장 모듈을 다시 로드하면, 임포트 관련 속성을 다시 설정하는 것 외에는 계속해서 아무런 효과가 없습니다.

공유 라이브러리 로딩의 제약(POSIX의 dlopen과 Windows의 LoadModuleEx 모두 해당) 때문에 디스크에서 라이브러리가 바뀐 뒤에는 수정된 라이브러리를 로드하는 것이 일반적으로 불가능합니다.

모듈의 새 버전을 시험해 보는 것 외의 다시 로드 사용 사례는 너무 드물어서, 모든 모듈 작성자에게 다시 로드를 염두에 두도록 요구할 정도가 아닙니다. 다시 로드와 비슷한 기능이 필요하면 작성자가 그 기능을 하는 전용 함수를 내보내면 됩니다.

한 라이브러리에 여러 모듈 넣기

하나의 공유 라이브러리에서 여러 Python 모듈을 지원하기 위해, 라이브러리는 라이브러리 파일 이름에 대응하는 심볼 외에 추가 PyInit* 심볼을 내보낼 수 있습니다.

현재 이 메커니즘은 추가 모듈을 로드하는 데에만 쓸 수 있으며 찾는 데에는 쓸 수 없다는 점에 유의하십시오. (이것은 로더 메커니즘의 제약이며, 이 PEP는 로더 메커니즘을 수정하려 하지 않습니다.) 적절한 파인더가 없는 문제는 다음과 같은 코드로 우회할 수 있습니다:

import importlib.machinery
import importlib.util
loader = importlib.machinery.ExtensionFileLoader(name, path)
spec = importlib.util.spec_from_loader(name, loader)
module = importlib.util.module_from_spec(spec)
loader.exec_module(module)
return module

심볼릭 링크를 지원하는 플랫폼에서는 심볼릭 링크를 사용하여 한 라이브러리를 여러 이름으로 설치할 수 있습니다. 이렇게 하면 내보낸 모든 모듈을 일반 임포트 메커니즘으로 불러올 수 있습니다.

테스트와 초기 구현

테스트를 위해 새 내장 모듈 _testmultiphase를 만듭니다. 이 라이브러리는 “한 라이브러리에 여러 모듈 넣기”에서 설명한 메커니즘을 사용하여 추가 모듈 여러 개를 내보냅니다.

_testcapi 모듈은 바뀌지 않으며, 앞으로도 계속(또는 단일 단계 초기화가 더 이상 지원되지 않을 때까지) 단일 단계 초기화를 사용합니다.

초기 구현 작업의 일부로 array 모듈과 xx* 모듈을 다단계 초기화를 사용하도록 바꿉니다.

API 변경 및 추가 사항 요약

새 함수:

  • PyModule_FromDefAndSpec (매크로)
  • PyModule_FromDefAndSpec2
  • PyModule_ExecDef
  • PyModule_SetDocString
  • PyModule_AddFunctions
  • PyModuleDef_Init

새 매크로:

  • Py_mod_create
  • Py_mod_exec

새 타입:

  • PyModuleDef_Type이 공개됩니다.

새 구조체:

  • PyModuleDef_Slot

기타 변경 사항:

PyModuleDef.m_reload가 PyModuleDef.m_slots로 바뀝니다.

BuiltinImporter와 ExtensionFileLoader는 이제 create_module과 exec_module을 구현합니다.

내부 _imp 모듈에는 하위 호환되지 않는 변경이 있습니다. create_builtin, create_dynamic, exec_dynamic이 추가되고, init_builtin과 load_dynamic은 제거됩니다.

문서화되지 않은 함수 imp.load_dynamic과 imp.init_builtin은 하위 호환 심(shim)으로 대체됩니다.

하위 호환성

기존 모듈은 새로운 버전의 Python과 소스 및 바이너리 호환성을 계속 유지합니다. 다단계 초기화를 사용하는 모듈은 이 PEP를 구현하지 않은 버전의 Python과 호환되지 않습니다.

함수 init_builtin과 load_dynamic은 _imp 모듈에서 제거됩니다(단, imp 모듈에서는 제거되지 않습니다).

변경된 모든 로더(BuiltinImporter와 ExtensionFileLoader)는 하위 호환성을 유지합니다. load_module 메서드는 심으로 대체됩니다.

Python/import.c와 Python/importdl.c의 내부 함수들이 제거됩니다. (구체적으로는 _PyImport_GetDynLoadFunc, _PyImport_GetDynLoadWindows, _PyImport_LoadDynamicModule입니다.)

향후 가능한 확장

PEP 384의 PyType_Slot에서 영감을 받은 슬롯 메커니즘은 이후의 확장을 가능하게 합니다.

일부 확장 모듈은 많은 상수를 내보냅니다. 예를 들어 _ssl에는 다음과 같은 형태의 호출이 길게 나열되어 있습니다:

PyModule_AddIntConstant(m, "SSL_ERROR_ZERO_RETURN",
                        PY_SSL_ERROR_ZERO_RETURN);

이를 PyMethodDef와 유사한 선언적 목록으로 바꾸면 상용구 코드가 줄어들고, 흔히 누락되는 오류 검사를 추가 비용 없이 제공할 수 있습니다.

문자열 상수와 타입도 비슷하게 처리할 수 있습니다. (타입의 기본이 아닌 베이스는 이식 가능한 방식으로 정적으로 지정할 수 없다는 점에 유의하십시오. 이 경우에는 슬롯이 추가되기 전에 실행되는 Py_mod_exec 함수가 필요합니다. 그렇더라도 추가 비용 없는 오류 검사는 여전히 유익합니다.)

또 다른 가능성은 모듈이 Python의 -m 스위치에 주어졌을 때 실행될 “main” 함수를 제공하는 것입니다. 이것이 동작하려면 PEP 451에서 도입된 ModuleSpec 기반 로딩을 활용하도록 runpy 모듈을 수정해야 합니다. 또한 모듈을 원래 정의될 때 가지지 않았던 슬롯에 따라 설정하는 메커니즘을 추가해야 합니다.

구현

작업 중인 구현은 GitHub 저장소 [3]에서 볼 수 있으며, 패치셋은 [4]에 있습니다.

이전 접근 방식

Stefan Behnel의 초기 proto-PEP [1]에는 모듈 클래스를 생성하는 “PyInit_modulename” 훅이 있었고, 그 클래스의 __init__을 호출하여 모듈을 생성하는 방식이었습니다. 이 제안은 모듈 생성과 초기화를 별개의 단계로 나누는 PEP 451(당시에는 아직 존재하지 않았음)과 맞지 않았습니다. 또한 이미 존재하는 모듈 객체에 확장을 로드하는 것도 지원하지 않았습니다.

Alyssa (Nick) Coghlan은 “Create” 훅과 “Exec” 훅을 제안하고 프로토타입 구현 [2]을 작성했습니다. 당시에는 PEP 451이 아직 구현되지 않았으므로 이 프로토타입은 ModuleSpec을 사용하지 않습니다.

이 PEP의 최초 버전은 Create 훅과 Exec 훅을 사용했으며, Exec 훅으로 미리 생성된 임의의 객체에 로드하는 것을 허용했습니다. 이 제안은 확장 모듈의 초기화 방식을 Python 모듈의 초기화 방식에 더 가깝게 만들었지만, 나중에 이것이 중요한 목표가 아니라는 점이 인식되었습니다. 현재 PEP는 더 단순한 해결책을 설명합니다.

그다음 버전에서는 PyInit의 대안으로 “PyModuleExport” 훅을 사용하여, 기존 방식에는 PyInit을, 다단계 방식에는 PyModuleExport를 사용했습니다. 그러나 모듈 이름으로 훅 이름을 결정할 수 없어서 freeze 같은 도구가 PyImport_Inittab을 자동으로 생성하는 일이 복잡해졌습니다. 정의를 내보내는 용도에 완전히 적합하지는 않더라도 PyInit 훅 이름만 유지하니 훨씬 더 단순한 해결책이 되었습니다.

참고 문헌