Python과 C 성능 연동: 수동 바인딩, ctypes, cffi를 통한 Python 확장
Ethan Miller
Product Engineer · Leapcell

소개
Python의 인기는 가독성, 방대한 라이브러리, 빠른 개발 능력에서 비롯됩니다. 그러나 순수한 연산 능력이나 저수준 시스템 리소스와의 직접적인 상호 작용과 관련해서는 Python이 때때로 성능 한계에 부딪힐 수 있습니다. 바로 여기서 속도와 제어로 유명한 언어인 C와의 공생 관계가 귀중해집니다. Python에 대한 C 확장을 작성하면 개발자가 성능 집약적인 작업을 오프로드하고, 기존 C 라이브러리를 활용하거나, 하드웨어와 직접 상호 작용하여 Python 애플리케이션의 속도를 효과적으로 높일 수 있습니다. 이 글에서는 Python과 C의 격차를 연결하는 다양한 방법, 특히 수동 C 확장, ctypes
, cffi
에 중점을 두고 접근 방식, 장점 및 이상적인 사용 사례를 비교합니다. 이러한 기술을 이해하는 것은 코드를 최적화하거나 외부 C 구성 요소와 인터페이스하려는 모든 Python 개발자에게 중요합니다.
Python의 C 통합 환경 해독
세부 사항을 자세히 살펴보기 전에 논의 전반에 걸쳐 반복될 몇 가지 주요 용어를 정의해 보겠습니다.
- C 확장: Python 내에서 직접 가져와 사용할 수 있는 C(또는 C++)로 작성된 모듈로, 특정 기능의 네이티브 속도 실행을 허용합니다.
- 이종 함수 인터페이스(FFI): 한 언어로 작성된 프로그램이 다른 언어로 작성된 함수를 호출하거나 서비스를 사용할 수 있도록 하는 메커니즘입니다.
ctypes
와cffi
는 Python의 주요 FFI 도구입니다. - Python C API: Python 인터프리터에서 제공하는 C 함수 집합으로, C 코드가 Python 객체와 직접 상호 작용하고, 메모리를 관리하고, 새 유형 또는 모듈을 정의할 수 있도록 합니다. 수동 C 확장은 이 API에 크게 의존합니다.
- 공유 라이브러리 (Windows의 동적 링크 라이브러리 - DLL, Linux/macOS의 .so): 컴파일 시 연결되는 대신 런타임에 프로그램에서 로드할 수 있는 사전 컴파일된 코드 및 데이터가 포함된 파일입니다. 이것은
ctypes
와cffi
가 C 코드와 상호 작용하는 일반적인 방법입니다. - 헤더 파일(.h): 함수, 변수 및 매크로의 선언이 포함된 파일입니다. C 컴파일러는 이를 사용하여 올바른 함수 호출 및 데이터 유형 호환성을 보장합니다.
cffi
는 종종 C 인터페이스를 구문 분석하는 데 이를 사용합니다.
수동 C 확장: 직접 접근 방식
수동 C 확장은 Python C API와 직접 상호 작용하는 C 코드를 작성하는 것을 포함합니다. 이 방법은 Python과 C 간에 오버헤드가 최소화되어 있어 가장 높은 수준의 제어와 성능을 제공합니다.
원칙: Python C API의 호출 규약을 준수하는 C 함수를 작성합니다. 이러한 함수는 Python 객체를 C 유형으로 변환하고, C 논리를 수행한 다음, 결과를 다시 Python 객체로 변환합니다. C 코드는 Python이 가져올 수 있는 공유 라이브러리(예: .so
또는 .pyd
파일)로 컴파일됩니다.
구현 예: 두 숫자를 더하는 간단한 C 확장을 만들어 보겠습니다.
-
adder.c
:#include <Python.h> // 두 숫자를 더하는 C 함수 static PyObject* add_numbers(PyObject* self, PyObject* args) { long a, b; // Python에서 인수 구문 분석 (두 개의 long 정수) if (!PyArg_ParseTuple(args, "ll", &a, &b)) { return NULL; // 오류 시 NULL 반환 } // 덧셈 수행 long result = a + b; // C 결과를 Python 정수 객체로 다시 변환 return PyLong_FromLong(result); } // 메서드 정의 구조체 static PyMethodDef AdderMethods[] = { {"add", add_numbers, METH_VARARGS, "덧셈을 수행합니다."}, {NULL, NULL, 0, NULL} // 종료자 }; // 모듈 정의 구조체 static struct PyModuleDef addermodule = { PyModuleDef_HEAD_INIT, "adder", // 모듈 이름 "numbers 덧셈을 위한 간단한 C 확장 모듈.", // 모듈 설명 -1, // 인터프리터당 모듈 상태 크기, 또는 모듈이 전역 변수에 상태를 유지하는 경우 -1. AdderMethods }; // 모듈 초기화 함수 PyMODINIT_FUNC PyInit_adder(void) { return PyModule_Create(&addermodule); }
-
컴파일 방법 (Linux/macOS):
gcc -shared -Wall -fPIC -I/usr/include/python3.8 -o adder.so adder.c # (필요에 따라 Python 포함 경로 조정)
-
test.py
:import adder print(adder.add(5, 7)) # 출력: 12
장점:
- 최대 성능: 네이티브 C 실행에 가장 가깝고 오버헤드가 최소화됩니다.
- 완전한 제어: Python의 내부 기능 및 C 기능에 대한 완전한 액세스 권한을 제공합니다.
- 복잡한 데이터 구조: 복잡한 C 데이터 유형 및 객체를 Python에 노출하는 데 가장 좋습니다.
단점:
- 가파른 학습 곡선: Python C API, 메모리 관리 및 참조 카운트에 대한 깊은 이해가 필요합니다.
- 오류 발생 가능성: 수동 메모리 관리 및 API 사용은 주의해서 처리하지 않으면 충돌을 일으킬 수 있습니다.
- 상용구 코드: 간단한 함수조차도 상당한 C 상용구 코드가 필요합니다.
- 컴파일: C 컴파일러와 빌드 프로세스 관리가 필요합니다.
응용 시나리오:
- 고성능 수치 컴퓨팅 (예: NumPy, SciPy 내부).
- 시스템 호출 또는 하드웨어 인터페이스와의 직접적인 상호 작용.
- 세밀한 제어가 필요한 크고 기존 C 라이브러리 래핑.
ctypes: Python의 내장 이종 함수 인터페이스
ctypes
는 Python용 이종 함수 라이브러리로, C 호환 데이터 유형을 제공하고 Python 코드에서 공유 라이브러리(DLL/공유 객체)의 함수를 호출할 수 있도록 합니다. Python 표준 라이브러리의 일부입니다.
원칙: ctypes
는 공유 라이브러리를 동적으로 로드하고 Python 코드에서 정의된 C 함수 주위에 Python 래퍼를 제공합니다. Python 유형에서 C 데이터 유형을 추론하거나 명시적 ctypes
유형 선언을 사용하여 Python과 C 간의 올바른 데이터 마샬링(변환)을 보장합니다.
구현 예: C 덧셈 함수를 동일하게 사용하지만, 이번에는 C 코드가 Python을 인식할 필요가 없습니다.
-
c_adder.c
:// 이 C 코드는 Python을 전혀 인식하지 못합니다. long add_two_numbers(long a, long b) { return a + b; }
-
컴파일 방법 (Linux/macOS):
gcc -shared -Wall -fPIC -o c_adder.so c_adder.c
-
test_ctypes.py
:import ctypes import os # 공유 라이브러리 로드 script_dir = os.path.dirname(__file__) lib_path = os.path.join(script_dir, 'c_adder.so') c_lib = ctypes.CDLL(lib_path) # C 함수의 인수 유형 및 반환 유형 정의 c_lib.add_two_numbers.argtypes = [ctypes.c_long, ctypes.c_long] c_lib.add_two_numbers.restype = ctypes.c_long # Python에서 C 함수 호출 result = c_lib.add_two_numbers(5, 7) print(result) # 출력: 12
장점:
- C API 지식 불필요: C 코드 자체에 Python 관련 헤더나 API 호출이 전혀 필요하지 않습니다.
- 포함된 기능: 표준 라이브러리의 일부이므로 외부 종속성이 없습니다.
- 동적 로드: 라이브러리는 런타임에 로드되어 유연성을 제공합니다.
- 단순 기본 유형: 간단한 C 데이터 유형(정수, 부동 소수점, 포인터)을 다루는 함수에 사용하기 비교적 쉽습니다.
단점:
- 수동 유형 매핑: 명시적인
argtypes
및restype
정의가 필요하여 복잡한 API의 경우 번거로울 수 있습니다. - 성능 오버헤드: Python과 C 유형 간의 데이터 마샬링은 특히 복잡한 구조체나 대규모 배열의 경우 오버헤드를 발생시킬 수 있습니다.
- 런타임 오류: 유형 불일치는 컴파일 타임이 아닌 런타임에 감지되므로 디버깅이 더 어렵습니다.
- 제한된 C++ 지원: 주로 C 인터페이스용으로 설계되었으며, C++ 클래스 및 오버로드된 함수에는 덜 직관적입니다.
응용 시나리오:
- Python 바인딩을 위해 재컴파일하거나 수정하는 것이 실현 가능하지 않은 기존 C 라이브러리와의 인터페이스.
- 사소한 성능 향상을 위해 간단한 C 함수 호출.
- OS 수준 C API와 상호 작용해야 하는 시스템 프로그래밍 작업.
cffi: 현대적인 FFI 대안
cffi
(Python용 C 이종 함수 인터페이스)는 Python에서 C 함수를 호출하고 C에서 Python 함수를 호출할 수도 있는 강력한 라이브러리입니다. ctypes
에 비해 C 코드와 상호 작용할 수 있는 보다 Pythonic하고 강력한 방법을 제공하는 것을 목표로 합니다.
원칙: cffi
는 C 헤더 파일 또는 문자열로 작성된 C와 같은 선언을 활용하여 Python 바인딩을 자동으로 생성합니다. "ABI" 모드( ctypes
와 유사, 런타임 로딩)와 "API" 모드(사전 컴파일, C 확장 모듈 생성)의 두 가지 모드에서 작동할 수 있습니다. 후자는 수동 C 확장과 가까운 성능을 제공합니다.
구현 예 (ABI 모드):
-
c_adder.c
:(ctypes
예와 동일)long add_two_numbers(long a, long b) { return a + b; }
-
컴파일 방법 (Linux/macOS):
gcc -shared -Wall -fPIC -o c_adder.so c_adder.c
-
test_cffi_abi.py
:from cffi import FFI import os ffi = FFI() # C 인터페이스 정의 ffi.cdef(""" long add_two_numbers(long a, long b); """) # 공유 라이브러리 로드 script_dir = os.path.dirname(__file__) lib_path = os.path.join(script_dir, 'c_adder.so') C = ffi.dlopen(lib_path) # C 함수 호출 result = C.add_two_numbers(5, 7) print(result) # 출력: 12
구현 예 (API 모드 - 프로덕션에 더 강력함):
-
builder.py
(빌드 스크립트):from cffi import FFI ffibuilder = FFI() ffibuilder.cdef(""" long add_two_numbers(long a, long b); """) # 이는 컴파일될 C 소스 코드를 정의합니다. # 문자열이거나 .c 파일에서 읽을 수 있습니다. ffibuilder.set_source("_adder_cffi", """ long add_two_numbers(long a, long b) { return a + b; } """, # C 코드에 다른 라이브러리가 포함되어 있다면 필요할 수 있습니다. # libraries=['m'] ) if __name__ == "__main__": ffibuilder.compile(verbose=True)
-
빌더 실행:
python builder.py
(이것은_adder_cffi.c
를 생성하고_adder_cffi.cpython-3x.so
와 같은 공유 라이브러리로 컴파일합니다.) -
test_cffi_api.py
:from _adder_cffi import lib # 생성된 모듈 가져오기 print(lib.add_two_numbers(5, 7)) # 출력: 12
장점:
- Pythonic 인터페이스:
ctypes
보다 깔끔하고 종종 더 직관적입니다. - 자동 유형 변환: C 선언에서 유형을 추론하여 상용구 코드 줄 수를 줄입니다.
- 성능: API 모드(
set_source
)는 C 코드를 네이티브 확장으로 컴파일하여 수동 C 확장에 필적하는 성능을 제공할 수 있습니다. - Python 2 & 3 호환: 두 Python 버전을 모두 지원합니다.
- 더 나은 오류 처리: 특히 ABI 모드에서 더 유익한 오류를 제공할 수 있습니다.
- 콜백: C에서 Python 함수를 호출하는 훌륭한 지원.
단점:
- 외부 종속성:
cffi
를 설치해야 합니다. - 빌드 프로세스: API 모드는 빌드 단계를 도입하여 배포를 약간 복잡하게 만들 수 있습니다.
- 학습 곡선: 두 가지 작동 모드로 인해
ctypes
보다 처음에 더 복잡해 보일 수 있습니다.
응용 시나리오:
ctypes
보다 더 쉽게 복잡한 C 라이브러리를 래핑.- 성능, 안전성 및 사용 편의성의 균형이 필요할 때.
- 콜백을 포함하여 Python과 C 코드 간에 상당한 상호 작용이 필요한 프로젝트.
- FFI 기능이 필요한 최신 Python 프로젝트.
결론
수동 C 확장, ctypes
, cffi
중에서 선택하는 것은 프로젝트의 특정 요구 사항, 성능 요구 사항 및 개발 선호도에 크게 좌우됩니다. 수동 C 확장은 비교할 수 없는 강력함과 속도를 제공하지만, 가파른 학습 곡선과 더 높은 복잡성을 수반합니다. ctypes
는 기존 C 라이브러리와의 빠른 상호 작용을 위한 간단하고 내장된 솔루션을 제공하지만, 복잡한 데이터의 경우 런타임 유형 오류 및 성능 오버헤드의 영향을 받을 수 있습니다. cffi
는 특히 API 모드에서 더 Pythonic 인터페이스, 강력한 기능 및 네이티브 확장에 가까운 성능을 제공하는 설득력 있는 균형을 이룹니다. C 통합이 필요한 대부분의 최신 Python 프로젝트의 경우 cffi
가 종종 권장되는 선택으로, Python의 유연성과 C의 원시 성능 간의 간극을 효과적으로 연결하는 효율성, 안전성 및 개발 경험의 훌륭한 조합을 제공합니다.