파이썬이 다른 언어와 어떻게 잘 어울릴 수 있는지 증명하는 과정? 파이썬으로도 좋은 GUI를 만들 수 있겠지만, 시스템쪽으로 Qt library를 사용하기로 결정하였고, 이것으로 사용하면 더욱 좋은 그래픽 인터페이스를 만들 수 있겠다는 생각을 하였다. 그러나 네트워크 및 인증 모듈 등을 도입하는 과정에서 기존의 C/C++도 나쁘지는 않지만, 파이썬의 기능이 워낙 막강하고 편리하기 때문에 일단 파이썬을 핵심 모듈로 사용하고, 그 이외의 모듈은 Qt를 사용하는 방법을 채택하였다.
이 방법이 아름다운 이유는, 핵심 라이브러리는 여러 시스템, MacOS, Windows, Linux 등에 포팅된다. C/C++로 GUI를 일단 생성하기로 마음먹었지만, 차후 C# 내지는 다른 언어로도 작성될 수 있다고 생각한다. 그런데 이 핵심 모듈을 여러 언어로 작성하기보다, 파이썬으로만 고수한다면 어떨까? 핵심 모듈과 다른 모듈과의 인터페이스만 잘 맞추어주면 되는 것 아닐까.
파이썬 자체의 생산성이 워낙 높기 때문에 핵심 모듈이 점차 다른 언어로 뒷받침된다 하더라도 일단은 파이썬으로도 충분히 커버 가능할 것이다. 이런 이유로 embedded python을 계속 생각하게 되었다.
파이썬을 타 언어와 접목하는 방법에는 두 가지 요령이 있다. 다른 언어가 파이썬의 모듈을 실행하는 것, 반대로 파이썬에서 타 언어의 모듈을 실행하는 방법이다. 전자가 현재 문서에서 말하는 '내장된 파이썬(embedded python)'이고, 후자는 파이썬이 다른 언어로 '확장'되므로 '확장된 파이썬(extended python)'이라 칭한다.
이 문서에서는 내장된 파이썬에 주목하고 이것을 사용하기 위해 어떻게 해야 하는지에 대해 실질적으로 접근해 보도록 한다. 전반적인 사항에 대해서는 생략하고, 주로 파이썬에서 제공하는 C 라이브러리를 직접 사용하는 법과, boost library를 이용하여 보다 편리하게 사용하는 법에 대해 논하도록 하자.
물론 파이썬은 C/C++ 뿐 아니라 C#, Java 등과도 어울릴 수 있으나 문서를 참고하면 일단 C API에 대해서만 서술하고 있다. 일단 C API를 직접 활용하는 방법과, 그리고 boost::python 사용하여 보다 쉽게 코드를 작성하는 방법에 대해 기록하고자 한다.
설명하려는 사람이 가진 마법의 단어가 있다. “매뉴얼을 보세요.” 물론 나도 모든 것을 설명할 수 없기에 결국에는 이 마법의 단어를 사용할 수 밖에는 없겠지만, 최소한 매뉴얼을 보고 언제 어디서든 방법을 따라할 만큼의 디테일한 설명, 다시 말해 매뉴얼이 다 하지 못한 이야기 정도는 할 것이다.
이 문서가 설명하고자 하는 embedded python의 목표는 다음과 같다.
다음과 같은 코드를 작성합니다. 무지하게 간단하지만, 천천히 시작하도록 하지요.
#include <Python.h> int main(int argc, char** argv) { Py_SetProgramName(argv[0]); Py_Initialize(); PyRun_SimpleString("print \'Hello, World!\'\n"); Py_Finalize(); return 0; }
문서를 참고하면 이렇게 작성한 코드는 컴파일을 해야 합니다. 하지만 라이브러리 링크 정보와 헤더 정보를 주지 않으면 에러가 나게 되겠죠. 문서는 이 라이브러리 링크 정보와 헤더 정보를 쉽게 알아내는 법에 대해 설명합니다.
파이썬 버전이 2.7인 경우,
python2.7-config –cflags
: 헤더 정보 및 컴파일 정보를 출력합니다.python2.7-config –ldflags
: 링크 정보를 출력합니다.그러므로 이 정보를 이용하면 다음과 같이 위 코드를 컴파일 할 수 있습니다.
gcc `python2.7-config --cflags` `python2.7-config --ldflags` -o hello_py hello_py.c
그런데 이렇게 매번 컴파일하기 힘드니, Makefile을 따로 만들어 두겠습니다.
CC = /usr/bin/gcc CONFIG = /opt/local/bin/python2.7-config CFLAGS = `$(CONFIG) --cflags` LDFLAGS = `$(CONFIG) --ldflags` TAR = hello_py.out # 여기에 .out 파일을 추가하세요 OBJ = $(TAR:.out=.o) SRC = $(TAR:.out=.c) all: $(TAR) .o.out: $(OBJ) $(CC) $(LDFLAGS) -o $@ $< .c.o: %.c $(CC) $(CFLAGS) -c $< clean: rm -fr $(TAR) $(OBJ)
TAR = hello_py.out
이 적힌 줄에 공백으로 계속 실행 파일의 이름을 입력하면 그에 맞게 소스가 컴파일됩니다.
이렇게 만들어진 바이너리 파일을 실행하면 다음과 같은 결과가 나올 것입니다.
Hello, World!
결과는 성공적이지만, 왠지 석연치 않습니다. PyRun_SimpleString()
함수는 이름 그대로 너무 간단합니다. 우리의 목표에는 미치지 못하는 결과입니다.
두번째는 파이썬의 함수를 호출해 볼 것입니다. 치사하게 인자가 없는 함수를 부르진 않을게요. 이것도 문서에 있는 예제로, 다음과 같은 역할을 하도록 프로그램되어 있습니다.
def multiply(a,b): print "Will compute", a, "times", b c = 0 for i in range(0, a): c = c + b return c
우리는 이 파이썬 함수를 C 코드에서 부를 것입니다. 그러나 이전처럼 뭔가 소스 코드가 하드코딩 되어 있다든지 하는 법 없이 동작하게 할 것입니다. 문서에서는 긴 소스를 죽 나열해두고 나중에 설명을 한 반면, 여기서는 어떻게 프로그램이 흘러가는지에 대해 중점을 맞추고 서술하겟습니다. 그 편이 더 이해하기 좋습니다.
Py_Initialize() 함수가 호출되기 전에 호출해야 하는 함수입니다. 파이썬이 실행 중 가지게 되는 자신의 이름입니다. 그러니까 이 함수로 전달된 값은 파이썬의 sys.argv[0]
와 동일한 것입니다. 이 함수는 선택적이어서 굳이 호출하지 않아도 됩니다. 이 경우 이름은 'python'이 된다고 합니다.
파이썬 API 사용 전후에 반드시 불려야 하는 함수입니다.
우선 multiply.py를 부르는 파이썬 스크립트와, 그것을 실행했을 때 일어나는 과정에 대해 간단히 생각해보죠. 이걸 우선 유념해두고 코드를 읽는 편이 낫다고 생각합니다. 그럼 간단하게 multiply.py를 사용하는 파이썬 스크립트를 생각해 보죠.
import multiply if __name__ == '__main__': print multiply.multiply(4, 5)
이 코드는 다음과 같이 동작합니다.
상당히 간단한 함수 호출이지만, 상당히 번거로운 작업들이 많습니다. 이 번거로운 작업들의 패턴은 크게 이렇게 생각할 수 있습니다.
우선 C의 데이터를 파이썬의 객체형으로 표현하는 법에 대해 설명합니다. C에서 파이썬의 문자열을 생성하는 함수는 PyString_FromString()입니다. 리턴 형은 PyObject*
인데 모든 파이썬의 데이터는 이 PyObject
입니다. 파이썬의 어떤 자료형이라도 PyObject
로 표현되므로, 항상 주의하여야 합니다.
char *str = NULL; PyObject* pString; str = ...; pString = PyString_FromString(str);
위 코드에서 PyString_FromString()
의 반환을 참조하는 pString
과 다른 API를 활용하여 마치 파이썬에서 문자열 함수를 사용하는 것과 유사하게 사용할 수 있습니다. 그러나 모든 기능이 제공되는 것은 아닙니다. 문서의 Concrete Objects Layer를 참고하면 더 많은 자료형에 대해 객체 생성을 할 수 있습니다.
Importing Modules의 문서를 참고합니다.
한 예는 PyImport_Import()입니다. 모듈 이름을 넘기면 모튤을 로딩합니다. 반환값은 그 모듈을 참조합니다. 이 때 모듈 이름은 C의 문자열이 아니라, 파이썬의 문자열입니다.
PyObject *pModuleName; PyObject *pSysModule; pModuleName = PyString_FromString("sys"); pSysModule = PyImport_Import(pModuleName);
파이썬의 변수, 함수, 클래스는 모두 모듈의 '속성(attribute)'입니다. 그러므로 해당 모듈에서 변수, 함수, 클래스 등을 참조하려면, 'Object Protocol' 섹션의 PyObject_GetAttrString()함수를 이용합니다. 이 외에도 여러 함수가 있으므로 문서를 한 번 꼼꼼하게 읽어 둘 필요가 있습니다.
PyObject *pVersion; PyObject *pFunction; PyObject *pReturn; pVersion = PyObject_GetAttrString(pSysModule, "version"); pFunction = PyObject_GetAttrString(pSysModule, "getfilesystemencoding"); pReturn = PyObject_CallObject(pFunction, NULL);
class sampleClass: def __init__(self, arg1, arg2, arg3): self.arg1 = arg1 self.arg2 = arg2 self.arg3 = arg3 ...
위와 같은 파이썬 클래스가 정의되어 있습니다. 이것을 인스턴스화 하는 법은 아래와 같습니다.
PyObject *pSampleClass; PyObject *pInstance; PyObject *pArgList; pSampleClass = PyObject_GetAttrString(pSampleClass, "SampleClass"); pArgList = Py_BuildValue("sid", arg1, arg2, arg3); /* string, integer, double */ pInstance = PyObject_CallObject(pSampleClass, pArgList);
PyObject_CallObject()는 호출할 수 있는 (callable) 오브젝트를 호출하고, 값을 반환받습니다. getfilesystemencoding
는 호출 가능하므로 pReturn에 값이 반환될 것입니다. 반환된 값을 C에서 어떻게 끄집어 내야 할지는 좀 더 후에 서술하겠습니다.
PyObject
의 데이터를 C로 가져오는 함수는 Py[XXX]_As[YYY] 스타일로 정의되어 있습니다. 그래서 오브젝트가 실수형인 경우 실수에서 double 타입으로 가져올 때는 PyFloat_AsDouble()을 사용하고, 정수형인 경우 PyInt_AsLong()을 사용하는 형태를 취하고 있습니다.
int intReturn; float floatReturn; Py_complex complexReturn; pIntReturn = PyInt_AsLong(pyIntObject); pFloatReturn = (float)PyFloat_AsDouble(pyFloatObject); pComplexReturn = PyComplex_AsCComplex(pyComplexObject);
리스트나 딕셔너리에서 값을 가져올 때는 PyList_Size(), PyList_GetItem(), PyList_SetItem(), PyDict_Size(), PyDict_GetItem(), PyDict_SetItem()
을 사용합니다. 사용은 매우 직관적입니다.
파이썬에서는 메모리 관리를 할 필요가 없습니다. 내부적으로 메모리 관리를 해 주지요. 그러나 이 편리한 기능은 C API에서까지 사용할 수는 없습니다. 그러므로 직접 우리가 생성된 PyObject 마다 레퍼런스 카운팅을 해 주어야 합니다. Py_INCREF()
, Py_DECREF()
, 함수들은 다 그런 레퍼런트 카운팅을 위해 존재합니다!
한편 PyList_SetItem(), PyTuple_SetItem()이 두 함수는 레퍼런스 카운팅을 자기가 차지합니다.
이제 소스 코드를 볼께요. 간단한 함수 호출 하나를 하는데 좀 많이 난리를 쳐야 합니다.
원래의 소스 코드와는 달리 메모리 NULL 체크들은 생략했습니다. 물론 실전에서는 해서는 안 될 방법이지만, 핵심적은 흐름을 보다 명확히 보기 위해서입니다.
위 Makefile에 TAR
변수에 call.out
을 추가해주세요.
#include <Python.h> int main(int argc, char** argv) { int i; PyObject *pModule, *pFunc; PyObject *pArgs, *pValue; if (argc < 3) { fprintf(stderr, "Usage: call.out pythonfile function [args]\n"); return 1; } Py_SetProgramName(argv[0]); Py_Initialize(); /* modified part */ PyRun_SimpleString( "import sys\n" "sys.path.append('.')\n"); pModule = PyImport_ImportModule(argv[1]); pFunc = PyObject_GetAttrString(pModule, argv[2]); pArgs = PyTuple_New(argc - 3); for(i = 0; i < argc - 3; ++i) { pValue = PyInt_FromLong(atoi(argv[i+3])); PyTuple_SetItem(pArgs, i, pValue); } pValue = PyObject_CallObject(pFunc, pArgs); printf("Result of call: %ld\n", PyInt_AsLong(pValue)); Py_DECREF(pArgs); Py_DECREF(pValue); Py_DECREF(pFunc); Py_DECREF(pModule); Py_Finalize(); return 0; }
if 블록을 다 제거하여 훨씬 보기는 편합니다. 원래 예제 코드와 다른 점은 바로 이 부분입니다.
PyRun_SimpleString("import sys\n" "sys.path.append('.')\n"); pModule = PyImport_ImportModule(argv[1]);
처음 PyRun_SimpleString()
은 현재 디렉토리도 모듈 검색 범위에 넣도록 하기 위해 추가된 것입니다. 아마 multiply.py
파일은 대개 이 소스 코드와 같은 경로에 두고 있을 것입니다. 그런데 이 코드를 넣지 않으면 아마 에러가 날 수도 있습니다. 그리고 원래 소스는 PyObject *pName
을 선언하여 argv[1]을 파이썬의 문자열 형태로 만듭니다. 그리고 Py_DECREF()
를 호출하지요. 그렇지만 char* 형태를 입력으로 받는 PyImport_ImportModule()
이 있으므로 이것으로 갈음할 수 있습니다.
make ./call.out multiply multiply 3 5 Will compute 3 times 5 Result of call: 15
세번째 예제는 조금 더 다양하게 가 보도록 하죠. urllib을 이용해 웹페이지의 소스 코드를 가져와, 외부 링크 즉, http…
로 시작되는 URL을 가져오도록 합니다.
#!/usr/bin/python import urllib import re href_expr = re.compile(r'<a href=\"(http://.+?)\"', re.DOTALL|re.MULTILINE) class count_href(object): def __init__(self): self.results = [] def count(self, url): html = self.get_html(url) found = href_expr.findall(html) d = {"url": url, "links": []} for item in found: d["links"].append(item) self.results.append(d) def get_html(self, url): opened = urllib.urlopen(url) html = opened.read() opened.close() return html def get_result(self): return self.results
이 모듈은 이렇게 사용하겠죠.
import count_href if __name__ == '__main__': counter = count_href.count_href() counter.count('http://www.google.com/en') counter.count('http://www.daum.net/') counter.count('http://www.naver.com/') result = counter.get_result() for item in result: print "URL: " + item["url"] print len(item["links"]) #for link in item["links"]: # print link
이 모듈의 클래스를 C 코드에서 가져오도록 합니다.
#include <assert.h> #include <Python.h> void extract(PyObject* instance) { PyObject *returned_object; Py_ssize_t i, list_size; returned_object = PyObject_CallMethod(instance, "get_result", NULL); /* returned_object: list of dicts. every dict has keys: url, links links is also list */ list_size = PyList_Size(returned_object); for(i = 0; i < list_size; ++i) { PyObject *dict_item, *url, *links; Py_ssize_t links_size, j; dict_item = PyList_GetItem(returned_object, i); assert(dict_item != NULL); url = PyDict_GetItemString(dict_item, "url"); assert(url != NULL); links = PyDict_GetItemString(dict_item, "links"); assert(links != NULL); links_size = PyList_Size(links); printf("url: %s\t", PyString_AsString(url)); printf("%d entries\n", (int)links_size); for(j = 0; j < links_size; ++j) { PyObject *entry_item = PyList_GetItem(links, j); assert(entry_item != NULL); printf("%s\n", PyString_AsString(entry_item)); } Py_DECREF(returned_object); } } int main(int argc, char** argv) { int i; PyObject *module, *class, *instance; int size; if(argc < 2) { fprintf(stderr, "Usage: count_href.out [URLS...]\n"); return 1; } Py_Initialize(); PyRun_SimpleString( "import sys\n" "sys.path.append('.')\n"); module = PyImport_ImportModule("count_href"); assert(module != NULL); class = PyObject_GetAttrString(module, "count_href"); assert(class != NULL); instance = PyObject_CallObject(class, NULL); assert(instance != NULL); for(i = 1; i < argc; ++i) { printf("count_href: %s\n", argv[i]); PyObject_CallMethod(instance, "count", "s", argv[i]); } extract(instance); Py_Finalize(); return 0; }
C API를 활용하여 원하는 기능을 구현할 수도 있지만, 간단한 데이터를 가져오는데도 상당히 코드가 많이 필요합니다. API를 간단히 파악하는 정도로 두고, 보다 간결하고 편리하게 쓸 수 있는 라이브러리를 찾아야 할 것 같습니다.
파이썬 위키에 다른 언어와 파이썬을 연동하는 방법이 잘 나와 있습니다. 여기서는 C++의 대표적인 라이브러리인 Boost C++ Libraries를 사용하도록 하겠습니다.
CC = /usr/bin/g++ PYTHON_CONFIG = /opt/local/bin/python2.7-config BOOST_INC_PATH = /opt/local/include BOOST_LIB_PATH = /opt/local/lib CFLAGS = `$(PYTHON_CONFIG) --cflags` -I $(BOOST_INC_PATH) LDFLAGS = `$(PYTHON_CONFIG) --ldflags` -L $(BOOST_LIB_PATH) -lboost_python-mt TAR = test.out hello_py.out # 여기에 .out 파일을 추가하세요 OBJ = $(TAR:.out=.o) SRC = $(TAR:.out=.cpp) all: $(TAR) .o.out: $(OBJ) $(CC) $(LDFLAGS) -o $@ $< .cpp.o: %.cpp $(CC) $(CFLAGS) -c $< clean: rm -rf $(TAR) $(OBJ)
#include <boost/python/exec.hpp> int main(int argc, char** argv) { Py_Initialize(); boost::python::exec("print \'Hello, World!\'\n"); Py_Finalize(); return 0; }
현재는 별다른 것이 없어 보이네요
#include <iostream> #include <boost/python.hpp> namespace bp = boost::python; void workaround_for_local_modules() { PyRun_SimpleString( "import sys\n" "sys.path.append('.')\n"); } int main(int argc, char** argv) { if (argc < 3) { fprintf(stderr,"Usage: call pythonfile funcname [args]\n"); return 1; } Py_Initialize(); workaround_for_local_modules(); try { bp::object module_name; bp::object module; bp::object func; module_name = bp::object(bp::handle<>(PyString_FromString(argv[1]))); module = bp::object(bp::handle<>(PyImport_Import(module_name.ptr()))); func = module.attr(argv[2]); if(func && PyCallable_Check(func.ptr())) { bp::object tuple; tuple = bp::object(bp::handle<>(PyTuple_New(argc - 3))); for(int i = 0; i < argc - 3; ++i) { PyObject* arg; arg = PyInt_FromLong(atoi(argv[i+3])); PyTuple_SetItem(tuple.ptr(), i, arg); } bp::object valueObj; int value; valueObj = bp::object(bp::handle<>(PyObject_CallObject(func.ptr(), tuple.ptr()))); value = bp::extract<int>(valueObj); std::cout << value << std::endl; } } catch(bp::error_already_set const &) { PyErr_Print(); } Py_Finalize(); return EXIT_SUCCESS; }
PyObject
가 boost::python::object
로 래핑되어 있습니다. 또한 boost::python::handle<>
을 이용하면 PyObject
의 레퍼런스를 관리해 줍니다. boost::python
의 대부분의 기능은 extending에 초점이 맞춰져 있고 embedding에는 그다지 많은 기능을 제공하지 않는 편이므로 상당히 많은 부분에서 C API를 그대로 가져와 사용해야 합니다.