默认用boost python包裹的C++对象是不支持pickle的,如果要用pickle.dumps(obj)的话那会提示错误
Pickling of "xxx" instances is not enabled.
这边吐槽一下在最新的代码里,给的reference链接其实还是不可用的。真正正确的是https://www.boost.org/doc/libs/1_74_0/libs/python/doc/html/reference/topics/pickle_support.html。
让你的class支持Pickle协议
若要让你的C++ Class支持Pickle协议,比较“正统”的方法是利用boost提供的boost::python::pickle_suite
. 拿代码说话:
struct world_t {
world_t(const string& country) { ... }
};
struct world_pickle_suite : boost::python::pickle_suite
{
static
boost::python::tuple
getinitargs(const world_t& w)
{
// [可选实现] 返回一个boost::python::tuple元组,其值被用来构造
// 如果一个类的构造函数**不需要参数**的话,可以不用重载这个方法。
return boost::python::make_tuple(w.country());
}
static
boost::python::tuple
getstate(const world_t& w)
{
// [可选实现] 如果对象的构造函数并不能完全恢复对象的状态,
// 那么要用此函数返回其状态值
}
static
void
setstate(world_t& w, boost::python::tuple state)
{
// [可选实现] 如果对象的构造函数并不能完全恢复对象的状态,
// 那么要用此函数把state里的状态恢复到w
}
};
boost::python::class_<world_t>("world", args<const std::string&>())
// 定义world_t的boost python包装类
.def_pickle(world_pickle_suite());
// ...
需要注意的是,如果在suite里定义了getstate/setstate并且这个类的__dict__
属性非空,boost是会报错的。一种可能的情况是你包装的类是一个子类,这时候还需要自己实现__getstate_manages_dict__
属性。这边不赘述,可参考这里。
原理简释
总所周知,Python3里若要在自定义的类里实现可Pickle,至少需要:
- 它的
__dict__
属性是可pickle的,或; - 调用
__getstate__()
拿到的返回值是可pickle的,随后会调用__setstate__()
方法在unpickle的时候恢复状态.
但在这些函数的背后,再稍微底层一点其实是通过__reduce__()
方法实现的(更严格一点,它会先去寻找__reduce_ex__()
是否可用)。这个方法简单来说就是返回一个tuple, 这个tuple有从构建对象到恢复状态所需的各种元素.
In fact, these methods are part of the copy protocol which implements the __reduce__() special method.
— https://docs.python.org/3/library/pickle.html
所以boost::python其实自己实现了一个对象的__reduce__()
方法,在src/object/pickle_support.cpp
里(源文件在这)。主要做了几件事
- 拿到当前对象的
__class__
属性(也就是class object); - 检查对象是否有
__safe_for_unpickling__
属性。如果没有的话,就是本文最早提到的报错了; - 检查对象是否有
__getinitargs__()
方法,若有则取值。没有的话,unpickle的时候按照无参数构造来处理; - 检查对象是否有
__getstate__()
方法,若有则取值; - 检查对象是否有
__dict__
属性,若有则会检查是否有__getstate_manages_dict__
属性并获取这两个属性的值。 - 返回tuple(),内容依次为1-5里拿到的值。
可以看到,这个__reduce__()
方法是遵循了Python里的object.__reduce__()
协定的。当然了,如果某些使用者觉得继承一个suite类都觉得麻烦或者达不到自己的需求,也可以通过其他手段完成,只要记得自己整一个__reduce__()
方法,只需满足Python的协定即可.
class_obj.attr("__reduce__") = boost::python::object(/*YOUR REDUCE FUNCTION*/);
再深一点~
如果你跟我一样无聊,那就来看看cpython自己想要__reduce__()
的时候是怎么用到__getstate__()
的吧.
cpython/Objects/typeobject.c这边有个函数叫
static PyObject* _common_reduce(PyObject *self, int proto)
,在pickle protocol>2时会调用`static PyObject *
reduce_newobj(PyObject *obj)`方法,这个方法是Python里大多数默认对象的默认__reduce__逻辑。其中有一句
state = _PyObject_GetState(obj,
!hasargs && !PyList_Check(obj) && !PyDict_Check(obj));
_PyObject_GetState里大致是从__dict__
或者__slots__
属性去获取对象的状态。
这边再往上一些还能看到有调用_PyObject_GetNewArguments()
的,里面的逻辑就会去拿__getnewargs_ex__
或是__getnewargs__
属性的值作为__new__
一个新对象时候的传入参数。这两个函数是Pickle协议文档里介绍的四个推荐覆盖的“高阶”函数之一。与之对应,Python并不希望一般人去自己写一个__reduce__()
出来.
看到这里,再回想一下boost::python,pickle_suite里面的getinitargs()
和getstate()
就分别对应到__getnewargs__()
与__getstate__()
。
发表回复