파이썬 C 확장 프로그래밍 마스터: 빠르고 네이티브한 모듈 만들기
C, C++, Java와 같은 컴파일 언어를 사용하여 작성한 코드는 다른 파이썬 스크립트에 통합하거나 임포트할 수 있습니다. 이 코드를 "확장(extension)"이라고 합니다.
파이썬 확장 모듈은 일반 C 라이브러리일 뿐입니다. 유닉스 머신에서는 이러한 라이브러리가 보통 .so (공유 객체)로 끝납니다. 윈도우 머신에서는 일반적으로 .dll (동적 연결 라이브러리)로 보입니다.
확장 작성 전제 조건
확장 작성을 시작하려면 파이썬 헤더 파일이 필요합니다.
또한 C 또는 C++에 대한 좋은 지식이 있어 C 프로그래밍을 사용하여 파이썬 확장을 작성할 수 있다고 가정합니다.
파이썬 확장의 첫 살펴보기
파이썬 확장 모듈을 처음 살펴보려면 코드를 네 부분으로 그룹화해야 합니다.
헤더 파일 Python.h
C 소스 파일에 Python.h 헤더 파일을 포함해야 하며, 이는 모듈을 인터프리터에 연결하는 데 사용되는 내부 파이썬 API에 접근할 수 있게 해줍니다.
다른 필요한 헤더 전에 Python.h를 포함해야 합니다. 포함 뒤에 파이썬에서 호출하려는 함수들을 따라야 합니다.
C 함수들
함수의 C 구현 시그니처는 항상 다음 세 가지 형태 중 하나를 취합니다.
static PyObject *MyFunction(PyObject *self, PyObject *args);
static PyObject *MyFunctionWithKeywords(PyObject *self, PyObject *args, PyObject *kw);
static PyObject *MyFunctionWithNoArgs(PyObject *self);
앞의 선언들은 각각 파이썬 객체를 반환합니다. C에서처럼 파이썬에는 void 함수가 없습니다. 함수가 값을 반환하지 않기를 원하면 파이썬의 None 값에 해당하는 C 값을 반환하세요. 파이썬 헤더는 이를 위한 매크로 Py_RETURN_NONE을 정의합니다.
C 함수의 이름은 확장 모듈 외부에서 결코 보이지 않으므로 원하는 대로 지을 수 있습니다. 이들은 static 함수로 정의됩니다.
보통 파이썬 모듈과 함수 이름을 결합하여 C 함수 이름을 짓습니다.
static PyObject *module_func(PyObject *self, PyObject *args) {
/* 여기에 작업 수행 */
Py_RETURN_NONE;
}
이는 모듈 module 내부의 func라는 파이썬 함수입니다. 포인터를 모듈의 메서드 테이블에 넣을 것이며, 이는 보통 소스 코드에서 다음에 옵니다.
메서드 매핑 테이블
이 메서드 테이블은 PyMethodDef 구조체의 간단한 배열입니다. 이 구조체는 다음과 같습니다.
struct PyMethodDef {
char *ml_name;
PyCFunction ml_meth;
int ml_flags;
char *ml_doc;
};
이 구조체 멤버의 설명은 다음과 같습니다.
ml_name - 파이썬 인터프리터가 파이썬 프로그램에서 사용될 때 표시하는 함수 이름.
ml_meth - 이전 섹션에서 설명한 세 가지 시그니처 중 하나를 가진 함수의 주소여야 합니다.
ml_flags - 인터프리터에게 ml_meth가 세 가지 시그니처 중 어느 것을 사용하는지 알립니다.
이 플래그는 보통 METH_VARARGS 값을 가집니다.
키워드 인수를 함수에 허용하려면 METH_KEYWORDS와 비트별 OR로 결합할 수 있습니다.
인수를 전혀 받지 않으려면 METH_NOARGS 값이 될 수 있습니다.
ml_doc - 함수의 독스트링으로, 작성하고 싶지 않으면 NULL일 수 있습니다.
이 테이블은 적절한 멤버에 대한 NULL과 0 값으로 구성된 센티널로 종료되어야 합니다.
예제
위에서 정의한 함수에 대한 메서드 매핑 테이블은 다음과 같습니다.
static PyMethodDef module_methods[] = {
{ "func", (PyCFunction)module_func, METH_NOARGS, NULL },
{ NULL, NULL, 0, NULL }
};
초기화 함수
확장 모듈의 마지막 부분은 초기화 함수입니다. 이 함수는 모듈이 로드될 때 파이썬 인터프리터에 의해 호출됩니다. 함수는 initModule이라는 이름이어야 하며, 여기서 Module은 모듈 이름입니다.
초기화 함수는 빌드할 라이브러리에서 내보내져야 합니다. 파이썬 헤더는 특정 컴파일 환경에서 이를 가능하게 하는 적절한 주문을 포함하기 위해 PyMODINIT_FUNC를 정의합니다. 함수를 정의할 때 이를 사용하기만 하면 됩니다.
C 초기화 함수는 일반적으로 다음과 같은 전체 구조를 가집니다.
PyMODINIT_FUNC initModule() {
Py_InitModule3(func, module_methods, "docstring...");
}
Py_InitModule3 함수의 설명은 다음과 같습니다.
이 모든 것을 합치면 다음과 같습니다.
#include
static PyObject *module_func(PyObject *self, PyObject *args) {
/* Do your stuff here. */
Py_RETURN_NONE;
}
static PyMethodDef module_methods[] = {
{ "func", (PyCFunction)module_func, METH_NOARGS, NULL },
{ NULL, NULL, 0, NULL }
};
PyMODINIT_FUNC initModule() {
Py_InitModule3(func, module_methods, "docstring...");
}
예제
위 모든 개념을 사용하는 간단한 예제입니다.
#include
static PyObject* helloworld(PyObject* self) {
return Py_BuildValue("s", "Hello, Python extensions!!");
}
static char helloworld_docs[] = "helloworld(): Any message you want to put here!!\n";
static PyMethodDef helloworld_funcs[] = {
{"helloworld", (PyCFunction)helloworld, METH_NOARGS, helloworld_docs},
{NULL}
};
void inithelloworld(void) {
Py_InitModule3("helloworld", helloworld_funcs, "Extension module example!");
}
여기서 Py_BuildValue 함수는 파이썬 값을 만드는 데 사용됩니다. 위 코드를 hello.c 파일에 저장하세요. 이 모듈을 컴파일하고 설치하여 파이썬 스크립트에서 호출할 수 있게 하는 방법을 보겠습니다.
확장 빌드 및 설치
distutils 패키지는 순수 파이썬과 확장 모듈 모두를 표준 방식으로 배포하는 것을 매우 쉽게 만듭니다. 모듈은 소스 형태로 배포되며, 보통 setup.py라고 불리는 설정 스크립트를 통해 빌드되고 설치됩니다.
위 모듈에 대해 다음 setup.py 스크립트를 준비해야 합니다.
from distutils.core import setup, Extension
setup(name='helloworld', version='1.0',
ext_modules=[Extension('helloworld', ['hello.c'])])
이제 다음 명령을 사용하면 올바른 컴파일러와 링커 명령 및 플래그로 필요한 모든 컴파일 및 링크 단계를 수행하고, 결과 동적 라이브러리를 적절한 디렉터리에 복사합니다.
$ python setup.py install
유닉스 기반 시스템에서는 site-packages 디렉터리에 쓰기 권한을 갖기 위해 이 명령을 root로 실행해야 할 것입니다. 이는 보통 윈도우에서 문제가 되지 않습니다.
확장 임포트
확장을 설치하면 파이썬 스크립트에서 다음과 같이 임포트하고 호출할 수 있습니다.
#!/usr/bin/python
import helloworld
print(helloworld.helloworld())
이 코드는 다음과 같은 결과를 출력합니다.
Hello, Python extensions!!
함수 매개변수 전달
인수를 받는 함수를 정의하려 할 가능성이 높으므로, C 함수의 다른 시그니처 중 하나를 사용할 수 있습니다. 예를 들어, 몇 개의 매개변수를 받는 다음 함수는 다음과 같이 정의됩니다.
static PyObject *module_func(PyObject *self, PyObject *args) {
/* 인수 파싱하고 흥미로운 작업 수행 */
Py_RETURN_NONE;
}
새 함수에 대한 항목을 포함하는 메서드 테이블은 다음과 같습니다.
static PyMethodDef module_methods[] = {
{ "func", (PyCFunction)module_func, METH_NOARGS, NULL },
{ "func", module_func, METH_VARARGS, NULL },
{ NULL, NULL, 0, NULL }
};
C 함수에 전달된 하나의 PyObject 포인터에서 인수를 추출하기 위해 API PyArg_ParseTuple 함수를 사용할 수 있습니다.
PyArg_ParseTuple의 첫 번째 인수는 args 인수입니다. 이는 파싱할 객체입니다. 두 번째 인수는 인수가 나타날 것으로 예상되는 방식으로 인수를 설명하는 형식 문자열입니다. 각 인수는 형식 문자열에서 하나 이상의 문자로 표현됩니다.
static PyObject *module_func(PyObject *self, PyObject *args) {
int i;
double d;
char *s;
if (!PyArg_ParseTuple(args, "ids", &i, &d, &s)) {
return NULL;
}
/* 흥미로운 작업 수행 */
Py_RETURN_NONE;
}
모듈의 새 버전을 컴파일하고 임포트하면 임의의 타입과 개수의 인수로 새 함수를 호출할 수 있습니다.
module.func(1, s="three", d=2.0)
module.func(i=1, d=2.0, s="three")
module.func(s="three", d=2.0, i=1)
더 많은 변형을 생각해낼 수 있을 것입니다.
PyArg_ParseTuple 함수
PyArg_ParseTuple 함수의 표준 시그니처는 다음과 같습니다.
int PyArg_ParseTuple(PyObject* tuple, char* format, ...)
이 함수는 오류 시 0을, 성공 시 0이 아닌 값을 반환합니다. tuple은 C 함수의 두 번째 인수였던 PyObject*입니다. 여기서 format은 필수 및 선택적 인수를 설명하는 C 문자열입니다.
PyArg_ParseTuple 함수에 대한 형식 코드 목록은 다음과 같습니다.
| 코드 |
C 타입 |
의미 |
| c |
char |
길이 1의 파이썬 문자열이 C char가 됩니다. |
| d |
double |
파이썬 float가 C double이 됩니다. |
| f |
float |
파이썬 float가 C float이 됩니다. |
| i |
int |
파이썬 int가 C int가 됩니다. |
| l |
long |
파이썬 int가 C long이 됩니다. |
| L |
long long |
파이썬 int가 C long long이 됩니다. |
| O |
PyObject* |
파이썬 인수에 대한 NULL이 아닌 빌려온 참조를 얻습니다. |
| s |
char* |
내장 null이 없는 파이썬 문자열이 C char*가 됩니다. |
| s# |
char*+int |
임의의 파이썬 문자열이 C 주소와 길이가 됩니다. |
| t# |
char*+int |
읽기 전용 단일 세그먼트 버퍼가 C 주소와 길이가 됩니다. |
| u |
Py_UNICODE* |
내장 null이 없는 파이썬 유니코드가 C가 됩니다. |
| u# |
Py_UNICODE*+int |
임의의 파이썬 유니코드가 C 주소와 길이가 됩니다. |
| w# |
char*+int |
읽기/쓰기 단일 세그먼트 버퍼가 C 주소와 길이가 됩니다. |
| z |
char* |
s와 같지만 None도 허용(C char*를 NULL로 설정). |
| z# |
char*+int |
s#과 같지만 None도 허용(C char*를 NULL로 설정). |
| (...) |
as per ... |
파이썬 시퀀스는 항목당 하나의 인수로 처리됩니다. |
| | |
|
다음 인수들은 선택적입니다. |
| : |
|
형식 끝, 오류 메시지용 함수 이름. |
| ; |
|
형식 끝, 전체 오류 메시지 텍스트. |
값 반환하기
Py_BuildValue는 PyArg_ParseTuple과 매우 유사한 형식 문자열을 받습니다. 만들 값의 주소를 전달하는 대신 실제 값을 전달합니다. add 함수를 구현하는 방법을 보여주는 예제입니다.
static PyObject *foo_add(PyObject *self, PyObject *args) {
int a;
int b;
if (!PyArg_ParseTuple(args, "ii", &a, &b)) {
return NULL;
}
return Py_BuildValue("i", a + b);
}
파이썬으로 구현하면 다음과 같습니다.
def add(a, b):
return (a + b)
함수에서 두 값을 반환할 수 있으며, 이는 파이썬에서 리스트로 캡처됩니다.
static PyObject *foo_add_subtract(PyObject *self, PyObject *args) {
int a;
int b;
if (!PyArg_ParseTuple(args, "ii", &a, &b)) {
return NULL;
}
return Py_BuildValue("ii", a + b, a - b);
}
파이썬으로 구현하면 다음과 같습니다.
def add_subtract(a, b):
return (a + b, a - b)
Py_BuildValue 함수
Py_BuildValue 함수의 표준 시그니처는 다음과 같습니다.
PyObject* Py_BuildValue(char* format, ...)
여기서 format은 만들 파이썬 객체를 설명하는 C 문자열입니다. Py_BuildValue의 다음 인수들은 결과가 빌드되는 C 값들입니다. PyObject* 결과는 새로운 참조입니다.
다음 표는 0개 이상이 결합되어 문자열 형식이 되는 일반적으로 사용되는 코드 문자열을 나열합니다.
| 코드 |
C 타입 |
의미 |
| c |
char |
C char가 길이 1의 파이썬 문자열이 됩니다. |
| d |
double |
C double이 파이썬 float가 됩니다. |
| f |
float |
C float이 파이썬 float가 됩니다. |
| i |
int |
C int가 파이썬 int가 됩니다. |
| l |
long |
C long이 파이썬 int가 됩니다. |
| N |
PyObject* |
파이썬 객체를 전달하고 참조를 훔칩니다. |
| O |
PyObject* |
파이썬 객체를 전달하고 정상적으로 INCREF합니다. |
| O& |
convert+void* |
임의의 변환 |
| s |
char* |
C 0-종료 char*가 파이썬 문자열이 되거나 NULL이면 None. |
| s# |
char*+int |
C char*와 길이가 파이썬 문자열이 되거나 NULL이면 None. |
| u |
Py_UNICODE* |
C 와이드, null-종료 문자열이 파이썬 유니코드가 되거나 NULL이면 None. |
| u# |
Py_UNICODE*+int |
C 와이드 문자열과 길이가 파이썬 유니코드가 되거나 NULL이면 None. |
| w# |
char*+int |
읽기/쓰기 단일 세그먼트 버퍼가 C 주소와 길이가 됩니다. |
| z |
char* |
s와 같지만 None도 허용(C char*를 NULL로 설정). |
| z# |
char*+int |
s#과 같지만 None도 허용(C char*를 NULL로 설정). |
| (...) |
as per ... |
C 값들로부터 파이썬 튜플을 만듭니다. |
| [...] |
as per ... |
C 값들로부터 파이썬 리스트를 만듭니다. |
| {...} |
as per ... |
C 값들로부터 파이썬 딕셔너리를 만듭니다. 키와 값을 번갈아 가며. |
코드 {...}는 짝수 개의 C 값으로부터 딕셔너리를 만듭니다. 번갈아 키와 값으로. 예를 들어, Py_BuildValue("{issi}", 23, "zig", "zag", 42)는 파이썬의 {23:'zig', 'zag':42} 같은 딕셔너리를 반환합니다.