파이썬용 C++ 확장 모듈 개발하기: pybind11 활용 가이드

개요

파이썬 프로젝트에서 성능이 중요한 부분을 C++로 구현해야 할 때가 있습니다. 이 가이드에서는 pybind11 라이브러리를 사용해 파이썬에서 사용할 수 있는 C++ 확장 모듈을 개발하는 방법을 설명합니다. 특히 CSV 파일 처리와 같은 특정 기능만 필요한 경우, 전체 pandas 라이브러리 대신 가벼운 C++ 확장 모듈을 구현하는 방법을 다룹니다.

pybind11 설치

pybind11은 C++ 코드를 파이썬 모듈로 변환해주며, 사용이 간편하고 C++ 코드 수정이 최소화됩니다. pybind11은 두 가지 방식으로 설치할 수 있습니다.

전역 설치

  1. pip을 통한 설치
    pip install pybind11
  2. 수동 설치
    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('값을 찾을 수 없습니다')

태그: pybind11 cmake python C++ 확장 모듈

5월 25일 09:01에 게시됨