首页 > 解决方案 > 腌制一个包含 __cinit__ 的 cython 类:__setstate__ vs __reduce__?

问题描述

我正在努力制作一些 cython 对象,并且对使用__setstate_vs有疑问__reduce__。似乎当您pickle.loads()使用一个__setstate__方法和一个__cinit__方法的对象时,__cinit__DOES 会被调用(不像它是一个__init__)。有没有办法防止这种情况或传递默认参数,或者我应该只使用__reduce__

这是一个玩具问题来说明(代码从这个博客修改)。

test.pyx我有三个类:

cdef class Person:
    cdef public str name
    cdef public int age

    def __init__(self,name,age):
        print('in Person.__init__')
        self.name = name 
        self.age = age 

    def __getstate__(self):
        return (self.name, self.age,)

    def __setstate__(self, state):
        name, age = state
        self.name = name
        self.age = age

cdef class Person2:
    cdef public str name
    cdef public int age

    def __cinit__(self,name,age):
        print('in Person2.__cinit__')
        self.name = name 
        self.age = age 

    def __getstate__(self):
        return (self.name, self.age,)

    def __setstate__(self, state):
        name, age = state
        self.name = name
        self.age = age

cdef class Person3:
    cdef public str name
    cdef public int age

    def __cinit__(self,name,age):
        print('in Person3.__cinit__')
        self.name = name 
        self.age = age 

    def __reduce__(self):
        return (newPerson3,(self.name, self.age))

def newPerson3(name,age):
    return Person3(name,age)

使用 构建后python setup.py build_ext --inplace,酸洗Person按预期工作(因为__init__没有被调用):

import test 
import pickle 

p = test.Person('timmy',12)
p_l = pickle.loads(pickle.dumps(p))

酸洗Person2失败:

p2 = test.Person2('timmy',12)
p_l = pickle.loads(pickle.dumps(p2))

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "test.pyx", line 25, in test.Person2.__cinit__
    print('in Person2.__cinit__')
TypeError: __cinit__() takes exactly 2 positional arguments (0 given)

所以__cinit__被称为....

中的__reduce__方法Person3按预期工作:

p3 = test.Person3('timmy',12)
p_l = pickle.loads(pickle.dumps(p3))

那么有没有办法用来__setstate__腌制Person2呢?

在我的实际问题中,类更复杂,使用__setstate__更简单,但也许我必须在__reduce__这里使用?我是 cython 和自定义酸洗的新手(也不太了解 C ......),所以可能会遗漏一些明显的东西......

标签: pythoncythonpickle

解决方案


简而言之:使用__getnewargs_ex__or__getnewargs____cinit__-method 提供所需的参数。

它是如何工作的?创建 Python 对象时,这是一个两步过程:

  • 一、__new__用于创建未初始化的对象
  • 第二步,__init__用于初始化第一步创建的对象

pickle使用稍微不同的算法:

  • __new__用于创建未初始化的对象
  • __setstate__(不再__init__是)用于初始化在第一步中创建的对象。

这是有道理的:__init__与对象的“当前”状态无关。我们不知道参数__init__,即使__init__没有参数,也可能做不必要的工作。

哪来__cinit__的戏?__cinit__定义时,Cython 会自动定义一个__new__-method(这就是不可能在 -calls 中手动定义 -method 的原因__new__) ,它会在返回之前cdef调用提供的-method。__cinit__Person2-example 中,此函数如下所示:

static PyObject *__pyx_tp_new_4test_Person2(PyTypeObject *t, PyObject *a, PyObject *k) {
  struct __pyx_obj_4test_Person2 *p;
  PyObject *o;
  if (likely((t->tp_flags & Py_TPFLAGS_IS_ABSTRACT) == 0)) {
    o = (*t->tp_alloc)(t, 0);
  } else {
    o = (PyObject *) PyBaseObject_Type.tp_new(t, __pyx_empty_tuple, 0);
  }
  if (unlikely(!o)) return 0;
  p = ((struct __pyx_obj_4test_Person2 *)o);
  p->name = ((PyObject*)Py_None); Py_INCREF(Py_None);
  if (unlikely(__pyx_pw_4test_7Person2_1__cinit__(o, a, k) < 0)) goto bad;
  return o;
  bad:
  Py_DECREF(o); o = 0;
  return NULL;
}

if (unlikely(__pyx_pw_4test_7Person2_1__cinit__(o, a, k) < 0)) goto bad;__cinit__被调用的行。

有了上面的内容就很清楚了,为什么__cinit__会被pickle调用,我们无法阻止它,因为__new__无论如何都必须调用它。

pickle然而,提供了更多的钩子来获取__cinit__-method 所需的信息到__new__-method:__getnewargs_ex____getnewargs__.

您的Person2课程可能如下所示:

%%cython
cdef class Person2:
    cdef public str name
    cdef public int age
    
    def __cinit__(self, name, age):
        self.name=name
        self.age=age

    def __getnewargs_ex__(self):
        return (self.name, self.age),{}

    def __getstate__(self):
        return ()
    
    def __setstate__(self, state):
        pass

现在

p2 = test.Person2('timmy',12)
p_l = pickle.loads(pickle.dumps(p2))

确实成功了!

这是一个玩具示例,没有多大意义,因此:

  • __getstate__并且__setstate__这里只是假人,因为所有需要的信息都由 提供__cinit__,通常情况并非如此。
  • 在这个例子__cinit__中没有多大意义,而是更有意义__init__

通常使用__cinit__而不是__init__用于 cdef 类。然而,一般来说,这不是 100% 正确的,当涉及到酸洗时,重要的是要确定发生了__cinit__什么以及发生了什么__init__

另一个极端,即将整个初始化代码放入__init__- 方法中,很容易解决酸洗问题。但是,组合__new__+__init__不是原子的,它可能会__new__被调用,然后在调用之前(或者像pickling那样)使用对象__init__,这可能会导致 NULL 指针取消引用和其他崩溃。

还必须注意,while__cinit__仅执行一次(何时__new__执行),__init__可以执行多次(例如__new__,可以覆盖子类,使其始终返回相同的单例),这意味着:

cdef class A:
    cdef char *a
    def __cinit__(self):
       a=<char*> malloc(1)

没关系,而相同的代码__init__

cdef class A:
    cdef char *a
    def __init__(self):
       a=<char*> malloc(1)

可能是内存泄漏,因为a可能是初始化指针而不是NULL,这仅保证__cinit__.


推荐阅读