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