腌制一个包含 __cinit__ 的 cython class :__setstate__ vs __reduce__?

pickle a cython class containing __cinit__ : __setstate__ vs __reduce__?

我正在努力使一些 cython 对象可拾取,并且有一个关于使用 __setstate___reduce__ 的问题。似乎当您 pickle.loads() 一个具有 __setstate__ 方法和 __cinit__ 方法的对象时,__cinit__ 确实会被调用(不同于 __init__ ).有没有办法防止这种情况或传递默认参数,或者我应该只使用 __reduce__?

这里有一个玩具问题来说明(代码修改自 blog)。

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 构建后,pickling 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__ 被调用....

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

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

那么有没有办法用__setstate__腌制Person2

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

The purpose of __cinit__ is that it always gets run.

Your __cinit__() method is guaranteed to be called exactly once.

相比之下,__init__ 可以不调用(例如在您的情况下,或者在继承的 classes 中)或多次调用。

总是被调用的 __cinit__ 的价值在于,许多 classC 类型的 es 必须以某种方式设置,否则它们将自动无效 - 例如,它们可能期望指向被初始化为 class 持有的一些内存。 (这是导致桌面自动崩溃的那种无效,而不是应该只导致 Python 异常的“Python”无效)。

由于您的玩具示例仅包含 Python 个对象,您最好只使用 __init__ - 没有理由 必须 进行初始化。如果需要,您可以同时使用 __init____cinit__,以将正常初始化中必须发生的部分与刚发生的部分分开。

如果您确实选择了使用 __cinit__,但想从具有不同数量参数的各种上下文中调用它,文档建议

you may find it useful to give the __cinit__() method * and ** arguments so that it can accept and ignore extra arguments.


总而言之,使用 __cinit__ 进行初始化,以免程序崩溃。接受它将从 __setstate__ 调用(因为您不希望程序在使用 __setstate__ 后崩溃,对吧?)。将它与 __init__ 结合起来进行通常应该发生但不是必需的初始化,有时可能会被覆盖。使用默认参数或 *** 参数使 __cinit__ 足够灵活以满足您的需求。

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

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

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

pickle 使用稍微不同的算法:

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

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

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

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__-方法所需的信息到 __new__-方法:__getnewargs_ex__ and __getnewargs__.

您的 Person2 class 可能如下所示:

%%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-classes。然而,总的来说,它不是 100% 正确的,当涉及到酸洗时,重要的是要确定 __cinit__ 中发生的事情以及 __init__ 中发生的事情。

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

还必须注意,虽然 __cinit__ 只执行一次(当执行 __new__ 时),但 __init__ 可以执行多次(例如 __new__ 可以覆盖一个子 class ,这样它总是 returns 相同的单例),这意味着:

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)

是一个可能的 memory-leak,因为 a 可能是一个初始化的指针而不是 NULL,这仅对 __cinit__.

有保证