개요
파이썬 프로젝트에서 성능이 중요한 부분을 C++로 구현해야 할 때가 있습니다. 이 가이드에서는 pybind11 라이브러리를 사용해 파이썬에서 사용할 수 있는 C++ 확장 모듈을 개발하는 방법을 설명합니다. 특히 CSV 파일 처리와 같은 특정 기능만 필요한 경우, 전체 pandas 라이브러리 대신 가벼운 C++ 확장 모듈을 구현하는 방법을 다룹니다.
pybind11 설치
pybind11은 C++ 코드를 파이썬 모듈로 변환해주며, 사용이 간편하고 C++ 코드 수정이 최소화됩니다. pybind11은 두 가지 방식으로 설치할 수 있습니다.
전역 설치
- pip을 통한 설치
pip install pybind11 - 수동 설치
git clone https://github.com/pybind/pybind11.git cd pybind11 mkdir build cd build cmake -DPYBIND11_TEST=OFF .. cmake --build . --config Release --target install만약 Visual Studio가 설치되어 있지 않다면, 첫 번째 cmake 명령에
-G "MinGW Makefiles"옵션을 추가하고 mingw32-make를 사용해 빌드할 수 있습니다.
서브모듈로 설치
프로젝트 내에 pybind11을 서브모듈로 추가하려면:
git init
git submodule add https://github.com/pybind/pybind11.git
그리고 프로젝트의 CMakeLists.txt에 다음을 추가합니다:
add_subdirectory(pybind11)
C++ 확장 모듈 코드 작성
다음은 CSV 파일을 읽고 처리하는 간단한 C++ 클래스 예제입니다. pybind11의 "11"은 C++11 표준을 의미하므로, C++11 호환 코드를 작성하는 것이 좋습니다.
#include
#include
#include <iostream>
#include <fstream>
#include <sstream>
#include
#include <vector>
#include <string>
#include <optional>
namespace py = pybind11;
class CSVProcessor {
public:
// 기본 생성자
CSVProcessor() = default;
// 생성자: CSV 파일명을 받아 데이터 로드
explicit CSVProcessor(const std::string& filename) {
loadFile(filename);
}
// CSV 파일 로드
void loadFile(const std::string& filename) {
std::ifstream file(filename);
std::string line;
if (!file.is_open()) {
throw std::runtime_error("파일을 열 수 없습니다: " + filename);
}
columnHeaders.clear();
rowData.clear();
bool isFirstLine = true;
while (std::getline(file, line)) {
std::istringstream ss(line);
std::string token;
std::vector tokens;
while (std::getline(ss, token, ',')) {
tokens.push_back(token);
}
if (isFirstLine) {
columnHeaders = tokens;
isFirstLine = false;
} else {
if (!tokens.empty()) {
std::string key = tokens[0];
std::vector values(tokens.begin() + 1, tokens.end());
rowData[key] = values;
}
}
}
}
// 특정 셀 값 찾기
std::optional findCell(const std::string& rowKey, const std::string& colHeader) {
auto rowIt = rowData.find(rowKey);
if (rowIt != rowData.end()) {
const std::vector& values = rowIt->second;
for (size_t i = 0; i < columnHeaders.size() - 1; ++i) {
if (columnHeaders[i + 1] == colHeader) {
return values[i];
}
}
}
return std::nullopt;
}
// 특정 행 데이터 가져오기 (행 키 제외)
std::vector getRowData(const std::string& rowKey) {
auto it = rowData.find(rowKey);
if (it != rowData.end()) {
return it->second;
}
return {};
}
// 특정 열 데이터 가져오기
std::vector getColumnData(const std::string& colHeader) {
std::vector column;
int colIndex = -1;
for (size_t i = 1; i < columnHeaders.size(); ++i) {
if (columnHeaders[i] == colHeader) {
colIndex = static_cast<int>(i - 1);
break;
}
}
if (colIndex < 0) {
return column;
}
for (const auto& row : rowData) {
const std::vector& values = row.second;
if (static_cast(colIndex) < values.size()) {
column.push_back(values[colIndex]);
} else {
column.push_back("");
}
}
return column;
}
// 열 헤더 가져오기
std::vector getColumnHeaders() const {
return columnHeaders;
}
private:
std::unordered_map> rowData;
std::vector columnHeaders;
};
PYBIND11_MODULE(CSVHandler, m) {
py::class_<CSVProcessor>(m, "CSVProcessor")
.def(py::init<>(), "빈 CSVProcessor 인스턴스를 초기화합니다.")
.def(py::init<const std::string&>(),
py::arg("filename"),
"지정된 CSV 파일을 읽어 CSVProcessor를 초기화합니다.")
.def("loadFile", &CSVProcessor::loadFile,
py::arg("filename"),
"지정된 CSV 파일을 로드합니다.")
.def("findCell", &CSVProcessor::findCell,
py::arg("rowKey"), py::arg("colHeader"),
"행 키와 열 헤더로 셀 값을 찾습니다.")
.def("getRowData", &CSVProcessor::getRowData,
py::arg("rowKey"),
"행 키에 해당하는 데이터를 반환합니다(행 키 제외).")
.def("getColumnData", &CSVProcessor::getColumnData,
py::arg("colHeader"),
"열 헤더에 해당하는 데이터를 반환합니다.")
.def("getColumnHeaders", &CSVProcessor::getColumnHeaders,
"열 헤더 목록을 반환합니다.");
}
CMakeLists.txt 작성
CMakeLists.txt 파일은 빌드 프로세스를 구성하는 데 중요합니다. 다음은 작동하는 예제입니다:
# 최소 CMake 버전 요구사항
cmake_minimum_required(VERSION 3.12...4.0)
# 프로젝트 이름 설정 (PYBIND11_MODULE의 첫 번째 인자와 일치해야 함)
project(CSVHandler)
# C++ 표준 설정
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Python 인터프리터와 라이브러리 찾기
find_package(Python COMPONENTS Interpreter Development REQUIRED)
# pybind11 찾기
find_package(pybind11 REQUIRED)
# 확장 모듈 추가
pybind11_add_module(CSVHandler csv_processor.cpp)
# 대상 속성 설정
set_target_properties(CSVHandler PROPERTIES
CXX_STANDARD 14
CXX_STANDARD_REQUIRED ON
)
# 설치 규칙
install(TARGETS CSVHandler DESTINATION .)
CMakeLists.txt 주요 명령 설명
find_package 명령
사용법: find_package(<PackageName> [version] [REQUIRED] [COMPONENTS ...])
예시: find_package(pybind11 REQUIRED)
이 명령은 pybind11이 올바르게 설치되어 있고 프로젝트에서 감지할 수 있어야 합니다. 실패할 경우 두 번째 CMakeLists.txt 예제를 참고해 경로를 직접 지정할 수 있습니다.
find_path 명령
사용법: find_path(<VAR> NAMES <file> PATHS <paths> [NO_DEFAULT_PATH] [REQUIRED])
예시: find_path(PYBIND11_INCLUDE_DIR pybind11/pybind11.h HINTS "C:/Path/To/pybind11/include" REQUIRED)
이 명령은 지정된 헤더 파일의 경로를 찾아 변수에 저장합니다.
find_library 명령
사용법: find_library(<VAR> NAMES <name> PATHS <paths> [REQUIRED])
예시: find_library(PYTHON_LIBRARY NAMES python312.lib HINTS "C:/Path/To/Python/libs" REQUIRED)
이 명령은 지정된 라이브러리 파일의 경로를 찾아 변수에 저장합니다.
빌드
작업 디렉토리에 CMakeLists.txt를 작성한 후, 빌드 디렉토리를 만들고 다음 명령을 실행합니다:
mkdir build
cd build
cmake ..
cmake --build .
Visual Studio가 설치되어 있지 않다면 첫 번째 명령을 다음과 같이 수정할 수 있습니다:
cmake -G "MinGW Makefiles" ..
mingw32-make
빌드가 성공하면 .pyd 파일이 생성됩니다. 이 파일은 바로 파이썬에서 사용할 수 없으며, 패키징 과정이 필요합니다.
확장 모듈 빌드 및 배포
빌드 환경 설정
빌드 디렉토리에 setup.py 파일을 생성합니다:
from setuptools import setup, Extension
import pybind11
cpp_args = ['-std=c++14', '-stdlib=libc++']
csv_module = Extension(
'CSVHandler',
sources=['csv_processor.cpp'],
include_dirs=[pybind11.get_include()],
language='c++',
extra_compile_args=cpp_args,
)
setup(
name='CSVHandler',
version='1.0',
description='CSV 처리를 위한 C++ 확장 모듈',
ext_modules=[csv_module],
)
동일한 디렉토리에 pyproject.toml 파일을 생성합니다:
[build-system]
requires = ["setuptools", "wheel", "pybind11"]
build-backend = "setuptools.build_meta"
파이썬 프로젝트에서 사용
필요한 패키지를 설치합니다:
pip install setuptools wheel pybind11
그리고 확장 모듈을 설치합니다:
pip install /path/to/your/build/directory
이제 파이썬 코드에서 확장 모듈을 사용할 수 있습니다:
from CSVHandler import CSVProcessor
# CSVProcessor 인스턴스 생성
processor = CSVProcessor()
# CSV 파일 로드
processor.loadFile('data.csv')
# 데이터 출력
print('열 헤더:', processor.getColumnHeaders())
print('첫 번째 행 데이터:', processor.getRowData('row1'))
print('첫 번째 열 데이터:', processor.getColumnData('column1'))
# 특정 셀 값 찾기
cell_value = processor.findCell('row1', 'column1')
if cell_value:
print('찾은 값:', cell_value)
else:
print('값을 찾을 수 없습니다')