Skip to main content

C++ of the Day #11 - Boost.Python 사용하기 #2

지난번 글 ((http://ideathinking.com/blog/?p=35)) 에 이어 이번에는 C++로 작성한 클래스를 python에서 사용하는 방법에 대해 알아보겠습니다.

예제까지 원문 ((http://www.boost.org/libs/python/doc/tutorial/doc/html/python/exposing.html)) 과 똑같으면 재미가 없으므로 조금 바꾸어서 작성해보았습니다. ;-)

멤버 함수 사용하기

#include <string>
#include <sstream>

struct Point
{
  Point() : x(0), y(0) {
  }
  void set_xy(int x, int y) {
    this->x = x;
    this->y = y;
  }
  std::string to_s() const {
    std::ostringstream ss;
    ss << "(" << std::dec << x << ", " << y << ")";
    return ss.str();
  }

  int x, y;
};

#include <boost python.hpp>
using namespace boost::python;
BOOST_PYTHON_MODULE(point)
{
class_<point>("Point")
  .def("set_xy", &Point::set_xy)
  .def("to_s", &Point::to_s)
;
}
위의 코드에서는 간단히 Point 클래스의 set_xy()와 to_s() 함수를 python에서 사용할 수 있도록 Boost.Python을 사용하였으며 간단히 class_ 의 def() 문법을 사용하여 멤버 함수를 내보낼 수 있음을 알 수 있습니다.

아래는 이 클래스를 python에서 사용한 예제입니다.

>>> import point
>>> x = point.Point()
>>> x.set_xy(1,2)
>>> x.to_s()
'(1, 2)'
>>> x.set_xy(3,4)
>>> x.to_s()
'(3, 4)'

생성자 사용하기

위의 코드에서 Point의 생성자를 다음과 같이 바꾸어 보겠습니다.

struct Point
{
  Point(int x_ = 0, int y_ = 0) : x(x_), y(y_) {
}
...
BOOST_PYTHON_MODULE(point)
{
class_<point>("Point", init<int, int>())
...

보시는 것과 같이 명시적으로 생성자를 지정해주기 위해서는 init<>() 문법을 사용하여 어떤 생성자가 있는지를 Boost.Python에 알려주어야 합니다.

그럼 사용한 예제를 살펴보겠습니다.

>>> import point
>>> x = point.Point(1,2)
>>> x.to_s()
'(1, 2)'
>>> x = point.Point()
Traceback (most recent call last):
File "<stdin>", line 1, in ?
Boost.Python.ArgumentError: Python argument types in
Point.__init__(Point)
did not match C++ signature:
__init__(_object*, int, int)
흠... 인자 두개짜리 생성자외에도 생성자의 default 값을 사용하고도 싶은데 생각처럼 되지를 않았네요. 이 문제를 해결하기 위해 Boost.Python에 생성자의 인자 두개가 모두 optional임을 다음과 같이 알려주어야 합니다.

BOOST_PYTHON_MODULE(point)
{
class_<point>("Point", init<optional<int, int> >())
...
사용 예제는 다음과 같습니다.
>>> import point
>>> x = point.Point()
>>> x.to_s()
'(0, 0)'
>>> x = point.Point(1)
>>> x.to_s()
'(1, 0)'
>>> x = point.Point(2,1)
>>> x.to_s()
'(2, 1)'

멤버 변수 사용하기

현재 Point 클래스에는 x, y를 따로 따로 읽을 수 있는 함수가 존재하지 않습니다. 하지만 Point 클래스의 멤버 변수 x, y가 public이기 때문에 다음과 같이 하여 멤버 변수를 python에서 사용할 수 있습니다.
class_<point>("Point", init<int, int>())
...
.def_readonly("x", &Point::x)
.def_readonly("y", &Point::y)
;
여기서는 def_readonly() 문법을 사용하여 멤버 변수를 읽기 전용으로 만들었습니다. 아래 사용예를 보면 변수에 값을 쓰려고 하면 예외가 발생함을 알 수 있습니다.
>>> import point
>>> x = point.Point(1,2)
>>> x.x
1
>>> x.y
2
>>> x.x = 2
Traceback (most recent call last):
File "<stdin>", line 1, in ?
AttributeError: can't set attribute
쓰기도 가능하게 하려면 def_readwrite() 문법을 사용하면 됩니다.

Property 사용하기

하지만 멤버 변수를 public 으로 선언하는 것은 일반적으로 잘못된 코딩 방법이죠. 따라서 Point 클래스를 아래와 같이 다시 작성하였습니다.
class Point
{
public:
  Point(int x_ = 0, int y_ = 0) : x(x_), y(y_) {
  }
  void set_x(int x) {
    this->x = x;
  }
  void set_y(int y) {
    this->y = y;
  }
  int get_x() const {
    return x;
  }
  int get_y() const {
    return y;
  }
  std::string to_s() const {
    std::ostringstream ss;
    ss << "(" << std::dec << x << ", " << y << ")";
    return ss.str();
  }
private:
  int x, y;
};
이 클래스의 x, y 값을 python에서 property로 사용하고 싶다면 다음과 같이 할 수 있습니다.
BOOST_PYTHON_MODULE(point)
{
class_<point>("Point", init<int, int>())
  .def(init<>())
  .add_property("x", &Point::get_x, &Point::set_x)
  .add_property("y", &Point::get_y, &Point::set_y)
  .def("to_s", &Point::to_s)
;
}
위의 코드는 add_property() 문법을 사용하여 'x' 라는 이름의 변수를 읽을때는 get_x()를 사용하고 쓸때는 set_x()를 사용하라고 알려주는 것입니다. 여기서 add_property()의 세번째 인자인 set_x() 부분을 빼면 'x'는 readonly 변수가 됩니다.
>>> import point
>>> x = point.Point()
>>> x.x = 1
>>> x.y = 2
>>> x.to_s()
'(1, 2)'
>>> x.x
1
>>> x.y
2


상속

상속에 대해 알아보기 위해 다음과 같이 Point에서 상속받는 PrettyPoint라는 클래스를 작성하였습니다. 이 클래스는 to_pretty_s()라는 함수를 추가로 제공합니다.
...
PrettyPoint(int x = 0, int y = 0) : Point(x, y) {
}
std::string to_pretty_s() const {
  std::ostringstream ss;
  ss << "(x=" << std::dec << get_x() << ", y=" << get_y() << ")";
  return ss.str();
}
};

BOOST_PYTHON_MODULE(point)
{
…
class_<prettypoint>("PrettyPoint", init<int, int>())
.def(init<>())
.def("to_pretty_s", &PrettyPoint::to_pretty_s)
;
}
자 그럼 PrettyPoint 클래스를 사용해 볼까요?
>>> import point
>>> x = point.PrettyPoint(1,2)
>>> x.to_pretty_s()
'(x=1, y=2)'
>>> x.to_s()
Traceback (most recent call last):
File "<stdin>", line 1, in ?
AttributeError: 'PrettyPoint' object has no attribute 'to_s'
흠… 예상대로 to_pretty_s() 함수는 정상적으로 동작하는데 부모 클래스에 구현되어 있는 to_s() 함수를 찾지 못하는군요. 상속관계가 정상적으로 동작하기 위해서는 다음과 같이 Boost.Python에게 이를 알려주어야 합니다.
class_<prettypoint, bases<point> >("PrettyPoint", init<int, int>())
예상대로 정상적으로 동작함을 알 수 있습니다.
>>> import point
>>> x = point.PrettyPoint(1,2)
>>> x.to_pretty_s()
'(x=1, y=2)'
>>> x.to_s()
'(1, 2)'

Virtual 함수

사실 위의 to_pretty_s() 함수는 to_s() 함수가 virtual 이었다면 더 깔끔하게 만들어질 수 있었을 것입니다. 굳이 _pretty_ 라는 이름을 짓지 않아도 되고 말이죠. 따라서 Point와 PrettyPoint 클래스를 다음과 같이 다시 작성하였습니다.
class Point
{
…
virtual ~Point() {
}
…
void println() const {
  std::cout << to_s() << std::endl;
}
virtual std::string to_s() const {
  std::ostringstream ss;
  ss << "(" << std::dec << x << ", " << y << ")";
  return ss.str();
}
…
};

class PrettyPoint : public Point
{
…
virtual std::string to_s() const {
  std::ostringstream ss;
  ss << "(x=" << std::dec << get_x() << ", y=" << get_y() << ")";
  return ss.str();
}
};
이를 사용한 예제는 다음과 같습니다.
>>> import point
>>> x = point.Point(1,2)
>>> x.println()
(1, 2)
>>> x = point.PrettyPoint(3,4)
>>> x.println()
(x=3, y=4)
예상대로 잘 동작합니다. 그럼 이번에 이 Point 클래스를 python에서 상속받아 볼까요?
>>> class PyPoint(point.Point):
…     def to_s(self):
…             return 'x: ' + str(self.x) + ', y: ' + str(self.y)
…
>>> x = PyPoint(2,4)
>>> x.println()
(2, 4)
흠 예상과 달리 동작하는군요. Python에서도 to_s() virtual 함수를 재정의하고 싶은데 말이죠. 이를 위해서는 애석하게도 아래와 같은 wrapper 클래스를 작성해야 합니다.
#include <boost python.hpp>

using namespace boost::python;

struct PointWrap : Point, wrapper<point>
{
PointWrap(int x = 0, int y = 0) : Point(x, y) {
}

std::string to_s() const
{
if (override to_s = this->get_override("to_s"))
return to_s();
return Point::to_s();
}

std::string default_to_s() const { return this->Point::to_s(); }
};
그리고 python에서 Point를 사용할때 이 wrapper를 사용하도록 아래와 같이 수정합니다.
class_<pointwrap, boost::noncopyable>("Point", init<int, int>())
…
.def("to_s", &Point::to_s, &PointWrap::default_to_s)
…
;
그럼 다시 한번 시험해볼까요?
>>> import point
>>> class PyPoint(point.Point):
…     def to_s(self):
…             return 'x: ' + str(self.x) + ', y: ' + str(self.y)
…
>>> x = PyPoint(2,4)
>>> x.println()
x: 2, y: 4
와우! C++의 virtual 함수를 python에서 상속받는데 성공했네요. :-) 여기서 한가지 중요한 점은 원래 Point 클래스는 수정할 필요가 전혀 없다는 점입니다. 따라서 기존에 작성된 코드를 그대로 유지하면서 python에서도 사용할 수 있게 할 수 있다는 것이죠. 물론 이러한 wrapper를 만드는 것이 귀찮기도 하고 어렵기 때문에 이를 자동으로 만들어주는 pyste 같은 프로젝트도 존재합니다.

Operator Overloading

그럼 이제 Point 클래스에 다음과 같이 += operator를 정의해볼까요?
void operator += (int val) {
x += val;
y += val;
}
이를 python에서 사용하기 위해서 아래와 같이 def() 문을 추가해줍니다.
class_<pointwrap, boost::noncopyable>("Point", init<int, int>())
…
.def(self += int())
;
너무 간단하고 직관적이죠? 그럼 python에서 사용해볼까요?
>>> import point
>>> x = point.Point(1,1)
>>> x += 2
>>> x.println()
(3, 3)

Conclusion

이번엔 Boost.Python을 사용하여 C++로 구현된 클래스를 python에서 사용하는 방법을 알아보았습니다. C++의 template 사용 방법들이 하나씩 개발되면서 Boost.Python과 같은 일종의 DSL(Domain-Specific Language)들이 늘어나고 있습니다. 다른 예로 Sprit 같은 라이브러리도 일종의 parsing을 위한 DSL이라고 할 수 있겠죠. ((예전에 작성해 두었던 Spirit 강좌가 있습니다. 참고하세요.
http://ideathinking.com/wiki/index.php/Main_Page#Spirit_-_boost_parsing_library)) 앞으로 더욱 다양해질 이러한 기법들이 기다려집니다.

Comments

Popular posts from this blog

1의 개수 세기 - 해답

벌써 어제 말한 내일이 되었는데 답을 주신 분이 아무도 없어서 좀 뻘쭘하네요. :-P 그리고 어제 문제에 O(1)이라고 적었는데 엄밀히 얘기하자면 O(log 10 n)이라고 적었어야 했네요. 죄송합니다. ... 문제를 잠시 생각해보면 1~n까지의 수들 중 1의 개수를 얻기 위해서는 해당 숫자 n의 각 자리의 1의 개수가 모두 몇개나 될지를 구해서 더하면 된다는 사실을 알 수 있습니다. 예를 들어 13이라는 수를 생각해 보면 1~13까지의 수에서 1의 자리에는 1이 모두 몇개나 되는지와 10의 자리에는 모두 몇개나 되는지를 구해 이 값을 더하면 됩니다. 먼저 1의 자리를 생각해 보면 1, 11의 두 개가 있으며 10의 자리의 경우, 10, 11, 12, 13의 네 개가 있습니다. 따라서 2+4=6이라는 값을 구할 수 있습니다. 이번엔 234라는 수에서 10의 자리를 예로 들어 살펴 보겠습니다. 1~234라는 수들 중 10의 자리에 1이 들어가는 수는 10, 11, ..., 19, 110, 111, ... 119, 210, 211, ..., 219들로 모두 30개가 있음을 알 수 있습니다. 이 규칙들을 보면 해당 자리수의 1의 개수를 구하는 공식을 만들 수 있습니다. 234의 10의 자리에 해당하는 1의 개수는 ((234/100)+1)*10이 됩니다. 여기서 +1은 해당 자리수의 수가 0이 아닌 경우에만 더해집니다. 예를 들어 204라면 ((204/100)+0)*10으로 30개가 아닌 20개가 됩니다. 이런 방식으로 234의 각 자리수의 1의 개수를 구하면 1의 자리에 해당하는 1의 개수는 ((234/10)+1)*1=24개가 되고 100의 자리에 해당하는 개수는 ((234/1000)+1)*100=100이 됩니다. 이들 세 수를 모두 합하면 24+30+100=154개가 됩니다. 한가지 추가로 생각해야 할 점은 제일 큰 자리의 수가 1인 경우 위의 공식이 아닌 다른 공식이 필요하다는 점입니다. 예를 들어 123에서 100의 자리에 해당하는 1의 개수는 ((123/1...

CodeHighlighter plugin test page.

This post is for testing CodeHighlighter plugin which uses GeSHi as a fontifier engine. ((Those code blocks are acquired from Google Code Search .)) ((For more supported languages, go CodeHighlighter plugin or GeSHi homepage.)) C++ (<pre lang="cpp" lineno="1">) class nsScannerBufferList { public: /** * Buffer objects are directly followed by a data segment. The start * of the data segment is determined by increment the |this| pointer * by 1 unit. */ class Buffer : public PRCList { public: Buffer() { ++index_; } PHP (<pre lang="php" lineno="4">) for ($i = 0; $i $value = ord( $utf8_string[ $i ] ); if ( $value < 128 ) { // ASCII $unicode .= chr($value); } else { if ( count( $values ) == 0 ) { $num_octets = ( $value } $values[] = $value; Lisp (<pre lang="lisp">) ;;; Assignment (define-caller-pattern setq ((:star var fo...

C++ of the Day #43 - SQLite3 C++ wrapper #1

The Definitive Guide to SQLite 를 읽다가 공부 겸 해서 C++ wrapper를 만들어 보았습니다. 최대한 C++ 냄새(?)가 나도록 만들어 보았습니다. :-) ((SQLite는 복잡한 관리가 필요없이 사용가능한, 파일이나 메모리 기반의, 라이브러리로 제공되는, 약 250kb 용량의, 대부분의 SQL92문을 지원하는, open source RDB입니다.)) 이 wrapper를 사용하기 위해서는 (당연하게도!) sqlite3 와 (당연하게도?) boost 라이브러리가 필요합니다. 사용 예들을 살펴보는 것으로 설명을 대신합니다. 이번 글에서는 다음과 같은 contacts 테이블이 test.db에 존재한다고 가정합니다. CREATE TABLE contacts ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, phone TEXT NOT NULL, UNIQUE(name, phone) ); Command 먼저 test.db 파일을 사용하기 위해 다음과 같이 파일 이름을 주어 connection 객체를 생성합니다. 생성과 동시에 test.db와 연결이 이루어집니다. ((생성자외에 open() 함수를 사용할 수도 있습니다.)) sqlite3pp::connection conn("test.db"); 다음은 contacts 테이블에 정보를 추가하는 가장 간단한 방법입니다. connection 클래스에서 제공하는 execute 함수를 사용합니다. ((executef 함수를 사용하면 printf와 같은 문법을 사용하여 query문을 작성할 수 있습니다.)) conn.execute("INSERT INTO contacts (name, phone) VALUES ('user', '1234')"); 위와 동일한 작업을 parameterized query를 사용하여 할 수도 있습니다. ((step()함수가 실제 query문을 수행하는 함수입니다. ...