暴露非const但在加速Python的不可复制成员(Expose a non-const but n

2019-07-20 14:03发布

这里是我的问题:

我有两个类这样的:

class Signal {
public:
    void connect(...) { sig.connect(...); }
private:
    boost::signal2::signal sig;
};

class MyClass {
public:
    Signal on_event;
};

我想揭露MyClass::on_event ,这样我可以调用my_class_instance.on_event.connect(...)从Python的。

这就是我裹这些类:

class_<Signal, boost::noncopyable> ("Signal", noinit)
    .def("connect", &some_helper_function);

class_<MyClass> ("MyClass")
    .def_readonly("on_event", &MyClass::on_event);

这将编译,但是当我尝试调用connect在Python,我得到: AttributeError: can't set attribute 。 这是在这里解释: http://www.boost.org/doc/libs/1_53_0/libs/python/doc/tutorial/doc/html/python/exposing.html ,所以我改成.def_readwriteon_event

但现在我得到一个编译时错误信息。 它几乎是不可能的阅读C ++模板的错误消息,但据我了解,因为它boost::signals2::signal是不可复制。 由于.def_readwrite使得成员分配它不能是不可复制。 但我使用我不想分配成员,我只是wan't调用一个方法。

我想过使得connect的方法Signal常量,即使它改变了对象,但我不能叫sig.connect()从该方法,所以这是一个没有去以及..

有任何想法吗?

Answer 1:

我有问题重现你的结果,但这里是一些信息可能会解决此问题的帮助。

通过简单的类:

class Signal
{
public:
  void connect() { std::cout << "connect called" << std::endl; }
private:
  boost::signals2::signal<void()> signal_;
};

class MyClass
{
public:
  Signal on_event;
};

而基本绑定:

namespace python = boost::python;
python::class_<Signal, boost::noncopyable>("Signal", python::no_init)
  .def("connect", &Signal::connect)
  ;

python::class_<MyClass>("MyClass")
  .def_readonly("on_event", &MyClass::on_event)
  ;

该代码编译失败。 当暴露一类,Boost.Python的默认行为的注册转换器。 这些转换器需要复制构造,以C ++类对象复制到存储可由Python对象进行管理的装置。 这种行为可以为一类通过提供禁用boost::noncopyable作为参数传递给class_类型。

在这种情况下, MyClass结合不抑制拷贝构造函数。 Boost.Python的将尝试绑定中使用拷贝构造函数和失败,编译器错误,因为成员变量on_event是不可拷贝。 Signal是不可拷贝,因为它包含与一种类型的一个成员变量boost::signal2::signal ,从继承boost::noncopyable

添加boost:::noncopyable的参数类型MyClass的绑定允许代码进行编译。

namespace python = boost::python;
python::class_<Signal, boost::noncopyable>("Signal", python::no_init)
  .def("connect", &Signal::connect)
  ;

python::class_<MyClass, boost::noncopyable>("MyClass")
  .def_readonly("on_event", &MyClass::on_event)
  ;

用法:

>>> import example
>>> m = example.MyClass()
>>> m.on_event.connect()
connect called
>>> m.on_event = None
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute
>>> 

虽然这种设置允许所需的绑定和调用语法,它看起来好像是在最终目标的第一步。


我的道歉,如果这是太放肆。 然而,根据最近的其他问题,我想借此在最初的例子扩大到覆盖似乎是最终的目标时间:能够使用Python回调连接到signal2::signal 。 我将介绍两种不同的方法,如力学和复杂程度不同,但他们可能会提供洞察应考虑最终解决方案的详细信息。

只有Python的线程。

对于这第一个场景中,让我们假设只有Python的线程与库交互。

,保持它相对简单的一种方法是使用继承。 通过定义一个辅助启动Slot类,它可以连接到Signal

class Slot
  : public boost::python::wrapper<Slot>
{
public:
  void operator()()
  {
    this->get_override("__call__")();
  }
};

Slot类所继承boost::python::wrapper ,即unintrusively提供了钩子的一类,以允许Python类覆盖在基类的功能。

当调用类型连接boost::signals2::signal ,该信号可以争论复制到其内部列表。 因此,对于仿函数能够延长使用寿命是很重要的Slot实例,只要其保持连接到signal 。 做到这一点最简单的方法是通过管理Slot通过boost::shared_ptr

产生的Signal类的样子:

class Signal
{
public:
  template <typename Callback>
  void connect(const Callback& callback)
  {
    signal_.connect(callback);
  }

  void operator()() { signal_(); }
private:
  boost::signals2::signal<void()> signal_;
};

和一个辅助功能,有助于保持Signal::connect一般,在其他情况下,C ++类型需要连接到它。

void connect_slot(Signal& self, 
                  const boost::shared_ptr<Slot>& slot)
{
  self.connect(boost::bind(&Slot::operator(), slot));
}

这将导致以下绑定:

BOOST_PYTHON_MODULE(example) {
  namespace python = boost::python;
  python::class_<Signal, boost::noncopyable>("Signal", python::no_init)
    .def("connect",  &connect_slot)
    .def("__call__", &Signal::operator())
    ;

  python::class_<MyClass, boost::noncopyable>("MyClass")
    .def_readonly("on_event", &MyClass::on_event)
    ;

  python::class_<Slot, boost::shared_ptr<Slot>, 
                 boost::noncopyable>("Slot")
    .def("__call__", python::pure_virtual(&Slot::operator()))
    ;
}

而它的用法如下:

>>> from example import *
>>> class Foo(Slot):
...     def __call__(self):
...          print "Foo::__call__"
... 
>>> m = MyClass()
>>> foo = Foo()
>>> m.on_event.connect(foo)
>>> m.on_event()
Foo::__call__
>>> foo = None
>>> m.on_event()
Foo::__call__

虽然成功的,它具有不Python的一个不幸的特点。 例如:

>>> def spam():
...     print "spam"
... 
>>> m = MyClass()
>>> m.on_event.connect(spam)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
Boost.Python.ArgumentError: Python argument types in
    Signal.connect(Signal, function)
did not match C++ signature:
    connect(Signal {lvalue}, boost::shared_ptr<Slot>)

这将是理想的,如果任何可调用对象可以连接到信号。 一个简单的方法这一点,猴修补绑定在Python。 是透明的最终用户:

  • 改变C ++从结合模块名字example_example 。 请务必同时更改库名。
  • 创建example.py将补丁Signal.connect()的论点包装成从继承的类型Slot

example.py可能是这个样子:

from _example import *

class _SlotWrap(Slot):

    def __init__(self, fn):
        self.fn = fn
        Slot.__init__(self)

    def __call__(self):
        self.fn()

def _signal_connect(fn):
    def decorator(self, slot):
        # If the slot is not an instance of Slot, then aggregate it
        # in SlotWrap.
        if not isinstance(slot, Slot):
            slot = _SlotWrap(slot)
        # Invoke the decorated function with the slot.
        return fn(self, slot)
    return decorator

# Patch Signal.connect.
Signal.connect = _signal_connect(Signal.connect)

修补是无缝的最终用户。

>>> from example import *
>>> def spam():
...     print "spam"
... 
>>> m = MyClass()
>>> m.on_event.connect(spam)
>>> m.on_event()
spam

有了这个补丁,任何调用类型可以连接到Signal ,而不必从明确继承Slot 。 因此,它变得比初始溶液更Python化。 永远不要低估了利益在保持简单的绑定和非Python的,但修补他们在蟒蛇Python的。


Python和C ++线程。

在接下来的情景,让我们考虑其中C ++线程与Python交互的情况下。 例如,一个C ++线程可以被设定的一段时间之后调用信号。

这个例子可以成为相当棘手,所以让我们从基础开始:Python的全局解释器锁 (GIL)。 总之,GIL是围绕着解释互斥。 如果一个线程在做任何影响蟒蛇管理对象的引用计数,那么它需要已经获得了GIL。 在前面的例子中,由于没有C ++线程,而GIL已经收购了发生的一切行动。 尽管这相当简单,它可以相当迅速地变得复杂。

首先,该模块需要有Python的初始化GIL的线程。

BOOST_PYTHON_MODULE(example) {
  PyEval_InitThreads(); // Initialize GIL to support non-python threads.
  ...
}

为方便起见,让我们创建一个简单的类来帮助管理GIL:

/// @brief RAII class used to lock and unlock the GIL.
class gil_lock
{
public:
  gil_lock()  { state_ = PyGILState_Ensure(); }
  ~gil_lock() { PyGILState_Release(state_);   }
private:
  PyGILState_STATE state_;
};

该线程将被调用MyClass的信号。 因此,它需要延长的寿命MyClass ,而线程是活动的。 一个很好的人选来完成,这是通过管理MyClass一个shared_ptr

让我们找出当C ++线程将需要GIL:

  • MyClass被删除shared_ptr
  • boost::signals2::signal可以使连接的对象的额外副本,当信号完成并发调用。
  • 调用一个Python反对通过连接boost::signals2::signal 。 回调势必会影响Python对象。 例如, self设置于参数__call__方法将增加和减少一个对象的引用计数。

支撑MyClass正在从C ++线程删除。

为了保证当GIL举行MyClass被删除shared_ptr从C ++线程内,一个定制删除是必需的。 这也需要绑定,以抑制默认的构造函数,并使用自定义构造函数来代替。

/// @brief Custom deleter.
template <typename T>
struct py_deleter
{
  void operator()(T* t)
  {
    gil_lock lock;    
    delete t;
  }
};

/// @brief Create Signal with a custom deleter.
boost::shared_ptr<MyClass> create_signal()
{
  return boost::shared_ptr<MyClass>(
    new MyClass(),
    py_deleter<MyClass>());
}

...

BOOST_PYTHON_MODULE(example) {

  ...

  python::class_<MyClass, boost::shared_ptr<MyClass>,
                 boost::noncopyable>("MyClass", python::no_init)
    .def("__init__", python::make_constructor(&create_signal))
    .def_readonly("on_event", &MyClass::on_event)
    ;
}

线程本身。

该线程的功能是相当简单:然后它睡觉调用信号。 然而,了解GIL的情况下是很重要的。

/// @brief Wait for a period of time, then invoke the
///        signal on MyClass.
void call_signal(boost::shared_ptr<MyClass>& shared_class,
                 unsigned int seconds)
{
  // The shared_ptr was created by the caller when the GIL was
  // locked, and is accepted as a reference to avoid modifying
  // it while the GIL is not locked.

  // Sleep without the GIL so that other python threads are able
  // to run.
  boost::this_thread::sleep_for(boost::chrono::seconds(seconds));

  // We do not want to hold the GIL while invoking C++-specific
  // slots connected to the signal.  Thus, it is the responsibility of
  // python slots to lock the GIL.  Additionally, the potential
  // copying of slots internally by the signal will be handled through
  // another mechanism.
  shared_class->on_event();

  // The shared_class has a custom deleter that will lock the GIL
  // when deletion needs to occur.
}

/// @brief Function that will be exposed to python that will create
///        a thread to call the signal.
void spawn_signal_thread(boost::shared_ptr<MyClass> self,
                       unsigned int seconds)
{
  // The caller owns the GIL, so it is safe to make copies.  Thus,
  // spawn off the thread, binding the arguments via copies.  As
  // the thread will not be joined, detach from the thread.
  boost::thread(boost::bind(&call_signal, self, seconds)).detach();
}

MyClass绑定更新。

python::class_<MyClass, boost::shared_ptr<MyClass>,
               boost::noncopyable>("MyClass", python::no_init)
  .def("__init__", python::make_constructor(&create_signal))
  .def("signal_in", &spawn_signal_thread)
  .def_readonly("on_event", &MyClass::on_event)
  ;

boost::signals2::signal和Python对象进行交互。

boost::signals2::signal可进行复制被调用时。 此外,可能存在连接到信号C ++时隙,所以这将是理想的,以不具有GIL锁定而信号被调用。 然而, signal不提供钩子允许我们创建时隙的副本或调用时隙之前获取GIL。

为了增加复杂性,当绑定暴露接受一个C ++类与C ++函数HeldType这不是一个智能指针,然后Boost.Python的将提取非参考从基准计数蟒对象计数C ++的对象。 它可以安全地做到这一点,因为调用线程,在Python中,有GIL。 为了保持一个引用计数插槽试图从Python的连接,以及允许任何调用类型的连接,我们可以使用不透明类型boost::python::object

为了避免signal创建提供的复印件boost::python::object ,可以创建副本boost::python::object ,这样的引用计数保持准确,并通过管理复制shared_ptr 。 这使得signal可以自由创建副本shared_ptr而不是创建boost::python::object没有GIL。

这GIL安全插槽可在一个辅助类封装。

/// @brief Helper type that will manage the GIL for a python slot.
class py_slot
{
public:

  /// @brief Constructor that assumes the caller has the GIL locked.
  py_slot(const boost::python::object& object)
    : object_(new boost::python::object(object),   // GIL locked, so copy.
              py_deleter<boost::python::object>()) // Delete needs GIL.
  {}

  void operator()()
  {
    // Lock the gil as the python object is going to be invoked.
    gil_lock lock;
    (*object_)(); 
  }

private:
  boost::shared_ptr<boost::python::object> object_;
};

一个辅助函数将被暴露在Python的帮助适应的类型。

/// @brief Signal connect helper.
void signal_connect(Signal& self,
                    boost::python::object object)
{
  self.connect(boost::bind(&py_slot::operator(), py_slot(object)));
}

而更新的绑定暴露辅助函数:

python::class_<Signal, boost::noncopyable>("Signal", python::no_init)
  .def("connect",  &signal_connect)
  .def("__call__", &Signal::operator())
  ;

最终的解决方案是这样的:

#include <boost/bind.hpp>
#include <boost/python.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/signals2/signal.hpp>
#include <boost/thread.hpp>

class Signal
{
public:
  template <typename Callback>
  void connect(const Callback& callback)
  {
    signal_.connect(callback);
  }

  void operator()() { signal_(); }
private:
  boost::signals2::signal<void()> signal_;
};

class MyClass
{
public:
  Signal on_event;
};

/// @brief RAII class used to lock and unlock the GIL.
class gil_lock
{
public:
  gil_lock()  { state_ = PyGILState_Ensure(); }
  ~gil_lock() { PyGILState_Release(state_);   }
private:
  PyGILState_STATE state_;
};    

/// @brief Custom deleter.
template <typename T>
struct py_deleter
{
  void operator()(T* t)
  {
    gil_lock lock;    
    delete t;
  }
};

/// @brief Create Signal with a custom deleter.
boost::shared_ptr<MyClass> create_signal()
{
  return boost::shared_ptr<MyClass>(
    new MyClass(),
    py_deleter<MyClass>());
}

/// @brief Wait for a period of time, then invoke the
///        signal on MyClass.
void call_signal(boost::shared_ptr<MyClass>& shared_class,
                 unsigned int seconds)
{
  // The shared_ptr was created by the caller when the GIL was
  // locked, and is accepted as a reference to avoid modifying
  // it while the GIL is not locked.

  // Sleep without the GIL so that other python threads are able
  // to run.
  boost::this_thread::sleep_for(boost::chrono::seconds(seconds));

  // We do not want to hold the GIL while invoking C++-specific
  // slots connected to the signal.  Thus, it is the responsibility of
  // python slots to lock the GIL.  Additionally, the potential
  // copying of slots internally by the signal will be handled through
  // another mechanism.
  shared_class->on_event();

  // The shared_class has a custom deleter that will lock the GIL
  // when deletion needs to occur.
}

/// @brief Function that will be exposed to python that will create
///        a thread to call the signal.
void spawn_signal_thread(boost::shared_ptr<MyClass> self,
                       unsigned int seconds)
{
  // The caller owns the GIL, so it is safe to make copies.  Thus,
  // spawn off the thread, binding the arguments via copies.  As
  // the thread will not be joined, detach from the thread.
  boost::thread(boost::bind(&call_signal, self, seconds)).detach();
}

/// @brief Helepr type that will manage the GIL for a python slot.
struct py_slot
{
public:

  /// @brief Constructor that assumes the caller has the GIL locked.
  py_slot(const boost::python::object& object)
    : object_(new boost::python::object(object),   // GIL locked, so copy.
              py_deleter<boost::python::object>()) // Delete needs GIL.
  {}

  void operator()()
  {
    // Lock the gil as the python object is going to be invoked.
    gil_lock lock;
    (*object_)(); 
  }

private:
  boost::shared_ptr<boost::python::object> object_;
};

/// @brief Signal connect helper.
void signal_connect(Signal& self,
                    boost::python::object object)
{
  self.connect(boost::bind(&py_slot::operator(), py_slot(object)));
}

BOOST_PYTHON_MODULE(example) {
  PyEval_InitThreads(); // Initialize GIL to support non-python threads.

  namespace python = boost::python;
  python::class_<Signal, boost::noncopyable>("Signal", python::no_init)
    .def("connect",  &signal_connect)
    .def("__call__", &Signal::operator())
    ;

  python::class_<MyClass, boost::shared_ptr<MyClass>,
                 boost::noncopyable>("MyClass", python::no_init)
    .def("__init__", python::make_constructor(&create_signal))
    .def("signal_in", &spawn_signal_thread)
    .def_readonly("on_event", &MyClass::on_event)
    ;
}

和测试脚本( test.py ):

from time import sleep
from example import *

def spam():
    print "spam"

m = MyClass()
m.on_event.connect(spam)
m.on_event()

m.signal_in(2)
m = None
print "Sleeping"
sleep(5)
print "Done sleeping"

结果如下:

spam
Sleeping
spam
Done sleeping

总之,当一个对象通过Boost.Python的层通过,需要时间来考虑如何管理其寿命和它要使用的上下文。 这通常需要了解如何使用其他库将处理对象。 这不是一个简单的问题,并提供解决方案Python的可能是一个挑战。



Answer 2:

写这个问题后,我添加了一个公共的拷贝构造函数,以信号与现在的作品。



文章来源: Expose a non-const but noncopyable member in Boost Python