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 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 PEP는 내장 모듈과 확장 모듈이 임포트 메커니즘과 상호 작용하는 방식을 재설계할 것을 제안합니다. 이는 PEP 3121에서 Python 3.0을 위해 마지막으로 개정되었지만, 당시에는 모든 문제를 해결하지 못했습니다. 이 제안의 목표는 확장 모듈을 Python 모듈의 동작 방식에 더 가깝게 만들어 임포트와 관련된 문제를 해결하는 것입니다. 구체적으로는 PEP 451에서 도입된 ModuleSpec 기반 로딩 메커니즘에 연결하는 것입니다.
이 제안은 PEP 384의 PyType_Spec에서 영감을 받아 확장 모듈 작성자가 필요한 기능만 정의할 수 있도록 하고, 향후 확장 모듈 선언에 추가될 기능도 수용할 수 있도록 합니다.
확장 모듈은 클래스의 __new__ 및 __init__과 유사한 2단계 프로세스로 생성되므로 ModuleSpec 아키텍처에 더 잘 부합합니다.
확장 모듈은 일반 가비지 컬렉션의 대상이 되고 다시 로드 및 서브 인터프리터를 지원하는 모듈 내에 임의의 C 수준 모듈별 상태를 안전하게 저장할 수 있습니다. 확장 모듈 작성자는 새 API를 사용할 때 이러한 문제를 고려하는 것이 좋습니다.
또한 이 제안은 ASCII가 아닌 이름을 가진 확장 모듈도 허용합니다.
이 제안에서는 PEP 3121에서 다룬 모든 문제가 해결되는 것은 아닙니다. 특히 런타임 모듈 조회(PyState_FindModule)와 관련된 문제는 향후 PEP의 과제로 남겨 둡니다.
동기
Python 모듈과 확장 모듈은 동일한 방식으로 설정되지 않습니다. Python 모듈의 경우 먼저 모듈 객체를 생성하고 설정한 다음, 모듈 코드가 실행됩니다(PEP 302). ModuleSpec 객체(PEP 451)는 모듈에 관한 정보를 보관하는 데 사용되며 관련 훅에 전달됩니다.
확장 모듈(즉, 공유 라이브러리)과 내장 모듈의 경우 모듈 초기화 함수가 즉시 실행되며 생성과 초기화를 모두 수행합니다. 초기화 함수에는 ModuleSpec이나 그 안에 포함된 __file__ 또는 정규화된 이름과 같은 어떠한 정보도 전달되지 않습니다. 이로 인해 상대 임포트와 리소스 로딩이 어려워집니다.
Py3에서는 모듈이 sys.modules에도 추가되지 않으므로, 모듈을 (잠재적으로 전이적으로) 다시 임포트하면 실제로 다시 임포트하려고 시도하게 되고, 그 결과 모듈 초기화 함수를 다시 실행할 때 무한 루프에 빠집니다. 정규화된 모듈 이름에 접근할 수 없으면 모듈을 sys.modules에 올바르게 추가하는 일도 간단하지 않습니다. 이는 특히 Cython으로 생성된 모듈에서 문제가 되는데, 이러한 모듈에서는 모듈 초기화 코드가 일반적인 Python 모듈의 코드와 동일한 수준의 복잡성을 갖는 일이 드물지 않습니다. 또한 __file__ 및 __name__ 정보가 없으면 “__init__.py” 모듈, 즉 패키지의 컴파일이 방해받으며, 특히 모듈 초기화 시점에 상대 임포트를 사용하는 경우에 그렇습니다.
더욱이 현재 존재하는 확장 모듈의 대다수는 서브 인터프리터 지원 및/또는 인터프리터 다시 로드에 문제가 있으며, 현재 인프라로 이러한 기능을 지원하는 것은 가능하지만 쉽지도 효율적이지도 않습니다. 이러한 문제를 해결하는 것이 PEP 3121의 목표였지만, 표준 라이브러리의 일부를 포함한 많은 확장 모듈은 Python 3으로 이식할 때 최소한의 노력만 들이는 방식을 택하여 이러한 문제를 해결하지 않은 채로 남겨 두었습니다. 이 PEP는 하위 호환성을 유지하므로 부담을 줄이고 확장 모듈 작성자가 이식할 때 이러한 문제를 고려할 충분한 시간을 제공할 수 있습니다.
현재 프로세스
현재 확장 모듈과 내장 모듈은 공유 라이브러리의 파일 이름을 따라 이름이 지정된 “PyInit_modulename”이라는 초기화 함수를 내보냅니다. 이 함수는 임포트 메커니즘에 의해 실행되며 완전히 초기화된 모듈 객체를 반환해야 합니다. 이 함수는 인자를 받지 않으므로 자신의 임포트 컨텍스트를 알 방법이 없습니다.
실행 중에 모듈 초기화 함수는 PyModuleDef 객체를 기반으로 모듈 객체를 생성합니다. 그런 다음 모듈 딕셔너리에 특성을 추가하고 타입을 생성하는 등의 작업을 수행하여 모듈을 계속 초기화합니다.
한편, 공유 라이브러리 로더는 마지막으로 로드한 모듈의 완전 수식 모듈 이름을 기록해 두며, 일치하는 이름의 모듈이 생성되면 이 전역 변수를 사용하여 모듈 객체의 완전 수식 이름을 결정합니다. 이는 모듈 초기화 함수가 먼저 자체 모듈 객체를 생성한다는 가정에 의존하므로 완전히 안전하지는 않지만, 실제로는 일반적으로 이 가정이 성립합니다.
제안
초기화 함수(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_createPy_mod_exec
알 수 없는 슬롯 ID가 있으면 가져오기가 SystemError와 함께 실패합니다.
다단계 초기화를 사용할 때는 가져오기 중에 PyModuleDef의 m_name 필드를 사용하지 않으며, 모듈 이름은 ModuleSpec에서 가져옵니다.
PyInit_*에서 반환되기 전에 PyModuleDef 객체는 새로 추가된 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의 구현은 “실행 슬롯”에 의해 관리됩니다. 이 PEP에서는 Py_mod_exec 하나만 추가하지만, 향후 다른 슬롯도 추가될 수 있습니다.
실행 단계는 모듈 객체에 연결된 PyModuleDef에서 수행됩니다. PyModule_Type의 서브클래스가 아닌 객체(PyModule_GetDef가 실패하는 객체)의 경우 실행 단계를 건너뜁니다.
실행 슬롯은 여러 번 지정할 수 있으며, 슬롯 배열에 나타나는 순서대로 처리됩니다. 기본 가져오기 메커니즘을 사용할 때는 PEP 451에 지정된 가져오기 관련 속성(예: __name__ 또는 __loader__)이 설정되고 모듈이 sys.modules에 추가된 후에 처리됩니다.
사전 실행 단계
실행 슬롯을 처리하기 전에 모듈별 상태가 모듈에 할당됩니다. 이 시점부터 모듈별 상태에 PyModule_GetState를 통해 접근할 수 있습니다.
Py_mod_exec 슬롯
이 슬롯의 항목은 다음 시그니처를 갖는 함수를 가리켜야 합니다.:
int (*PyModuleExecFunction)(PyObject* module)
이 함수는 모듈을 초기화하기 위해 호출됩니다. 일반적으로 이는 모듈의 초기 속성을 설정하는 작업입니다. “module” 인자는 초기화할 모듈 객체를 받습니다.
함수는 성공 시 0을 반환해야 하며, 오류 발생 시 예외를 설정하고 -1을 반환해야 합니다.
PyModuleExec가 sys.modules에 있는 모듈 항목을 교체하면, 모든 실행 슬롯이 처리된 후 가져오기 메커니즘에서 새 객체가 사용되고 반환됩니다. 이는 가져오기 메커니즘 자체의 기능입니다. 슬롯 자체는 모두 생성 단계에서 반환된 모듈을 사용하여 처리되며, 실행 단계에서는 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가 아닌 모듈 이름입니다.
서브인터프리터 및 인터프리터 재로딩
새로운 초기화 방식을 사용하는 확장은 서브인터프리터와 여러 Py_Initialize/Py_Finalize 주기를 올바르게 지원하여 Python 문서에 언급된 문제 [6]를 방지해야 합니다. 이 메커니즘은 이를 쉽게 수행하도록 설계되었지만, 확장 작성자는 여전히 주의를 기울여야 합니다. 사용자 정의 함수, 메서드 또는 인스턴스가 서로 다른 인터프리터로 유출되어서는 안 됩니다. 이를 위해 모든 모듈 수준 상태는 모듈 딕셔너리 또는 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* 기호를 내보낼 수 있습니다.
이 메커니즘은 현재 추가 모듈을 load하는 데만 사용할 수 있고, 해당 모듈을 find하는 데는 사용할 수 없다는 점에 유의하십시오. (이는 이 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_FromDefAndSpec2PyModule_ExecDefPyModule_SetDocStringPyModule_AddFunctionsPyModuleDef_Init
새 매크로:
Py_mod_createPy_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은 하위 호환 가능한 셈으로 대체될 것입니다.
하위 호환성
기존 모듈은 새로운 버전의 파이썬과 계속 소스 및 바이너리 호환성을 유지할 것입니다. 다단계 초기화를 사용하는 모듈은 이 PEP를 구현하지 않은 파이썬 버전과는 호환되지 않을 것입니다.
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 함수가 필요할 것입니다. 다만 무료 오류 검사는 여전히 유용할 것입니다.)
또 다른 가능성은 모듈이 파이썬의 -m 스위치에 주어졌을 때 실행될 “main” 함수를 제공하는 것입니다. 이것이 동작하려면, runpy 모듈을 수정하여 PEP 451에서 도입된 ModuleSpec 기반 로딩을 활용하도록 해야 할 것입니다. 또한, 모듈이 원래 정의되지 않았던 슬롯에 따라 모듈을 설정하는 메커니즘을 추가할 필요가 있을 것입니다.
구현
이전 접근법
Stefan Behnel의 초기 프로토-PEP [1]는 모듈 클래스를 생성하는 “PyInit_modulename” 훅을 가지고 있었으며, 이 클래스의 __init__이 호출되어 모듈을 생성했습니다. 이 제안은 (당시에는 존재하지 않았던) PEP 451과 일치하지 않았는데, 이 PEP에서는 모듈 생성과 초기화가 별개의 단계로 나뉩니다. 또한 기존에 존재하는 모듈 객체에 확장 모듈을 로드하는 것을 지원하지 않았습니다.
Alyssa (Nick) Coghlan은 “Create”와 “Exec” 훅을 제안하고 프로토타입 구현 [2]을 작성했습니다. 이 시점에는 PEP 451이 아직 구현되지 않았기 때문에, 프로토타입은 ModuleSpec을 사용하지 않습니다.
이 PEP의 원래 버전은 Create와 Exec 훅을 사용했으며, Exec 훅으로 임의의 미리 구성된 객체에 로드하는 것을 허용했습니다. 이 제안은 확장 모듈 초기화를 Python 모듈이 초기화되는 방식에 더 가깝게 만들었지만, 나중에 이것이 중요한 목표가 아니라는 것이 밝혀졌습니다. 현재의 PEP는 더 단순한 해법을 설명합니다.
이후 반복에서는 PyInit의 대안으로 “PyModuleExport” 훅을 사용했는데, PyInit은 기존 방식에, PyModuleExport는 다단계 방식에 사용되었습니다. 그러나 모듈 이름을 기반으로 훅 이름을 결정할 수 없다는 점 때문에 freeze 같은 도구에 의한 PyImport_Inittab의 자동 생성이 복잡해졌습니다. PyInit 훅 이름만 유지하는 것은, 정의를 내보내는 데 완전히 적합하지는 않더라도, 훨씬 단순한 해법을 만들어냈습니다.
참고 문헌
Copyright
This document has been placed in the public domain.