默认用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里(源文件在这)。主要做了几件事

  1. 拿到当前对象的__class__属性(也就是class object);
  2. 检查对象是否有__safe_for_unpickling__属性。如果没有的话,就是本文最早提到的报错了;
  3. 检查对象是否有__getinitargs__()方法,若有则取值。没有的话,unpickle的时候按照无参数构造来处理;
  4. 检查对象是否有__getstate__()方法,若有则取值;
  5. 检查对象是否有__dict__属性,若有则会检查是否有__getstate_manages_dict__属性并获取这两个属性的值。
  6. 返回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__()

标签: C++, boost python, boost, pickle

添加新评论

所有评论将经过人工审核:)