使用 cython 从 C++ 构造函数传播异常
Propagate exception from C++ constructor with cython
如果某些参数无效,我想在 C++ class 构造函数中引发异常,例如 PyErr_SetString(PyExc_ValueError, "Error occurred")
。不幸的是,它没有正确传播,我得到的是 SystemError: <class 'testlib.foo.Foo'> returned a result with an error set
。甚至可以让它在构造函数中工作吗?
几个问题:
- 所有逻辑都应该在 C++ class 中,而不会分散到
.pyx
个文件中。
- 验证应该在构造函数内部进行,这样就不需要在创建对象后调用单独的
init()
方法。
- 不仅可以提出一些标准异常,还可以提出任何自定义。
setup.py
from Cython.Build import cythonize
from Cython.Distutils import Extension
from setuptools import find_packages, setup
setup(
name='testlib',
python_requires='~=3.7.0',
packages=find_packages('src'),
package_dir={'': 'src'},
ext_modules=cythonize(
[
Extension(
'testlib.foo',
['src/testlib/foo.pyx', 'src/testlib/c_foo.cpp'],
extra_compile_args=['-std=c++11'], language='c++',
)
], language_level='3'
)
)
src/testlib/c_foo.h
#ifndef FOO_H
#define FOO_H
#define PY_SSIZE_T_CLEAN
#include <Python.h>
namespace foo {
class Foo {
public:
Foo();
~Foo();
};
}
#endif
src/testlib/c_foo.cpp
#include "c_foo.h"
namespace foo {
Foo::Foo() {
PyErr_SetString(PyExc_ValueError, "Error occurred");
}
Foo::~Foo() {}
}
src/testlib/c_foo.pxd
cdef extern from "c_foo.h" namespace "foo":
cdef cppclass Foo:
Foo()
src/testlib/foo.pyx
from .c_foo cimport Foo as CFoo
cdef class Foo:
cdef CFoo *c_foo
def __cinit__(self):
self.c_foo = new CFoo()
def __dealloc__(self):
del self.c_foo
tests/test_foo.py
import pytest
from testlib.foo import Foo
def test_foo():
with pytest.raises(ValueError):
Foo() # SystemError: <class 'testlib.foo.Foo'> returned a result with an error set
前两个问题可以通过 throw std::invalid_argument("Some error")
和 c_foo.pxd
中的 Foo() except +
一起解决,但它不适用于任意 python 异常。
首先,请注意这可能会出错。 Python 范围规则不同于 C++ 范围规则,Cython 主要遵循 Python 范围规则。考虑
# distutils: language=c++
cdef extern from "something.hpp":
cdef cppclass C:
C()
def f(x):
cdef C c
if x>0:
c = C()
c
必须 在你选择的任何分支都可用,因此在函数开始时默认初始化:
CYTHON_UNUSED C __pyx_v_c;
这个默认初始化不会有围绕它的标准异常处理(原则上它可能像在这种情况下那样处理 Python 异常,但在构造函数并生成用 C++ 编译的代码)。
因此,如果您的 class 有任何堆栈分配的变量,您就有可能将 Cython 置于设置异常的状态,但它认为不应该设置异常。
话虽如此,我认为这样做的方法可能是使用静态工厂函数。 Cython 确实理解这些异常规范(它不理解 c++ 构造函数上的 except *
可能是一个小错误...)
# distutils: language=c++
cdef extern from *:
"""
class Foo {
public:
Foo() {
PyErr_SetString(PyExc_ValueError, "Error occurred");
}
~Foo() {}
static Foo getFoo() {
return Foo();
}
};
"""
cdef cppclass Foo:
Foo()
@staticmethod
Foo getFoo() except *
def f():
Foo.getFoo()
此代码从 Cython 生成以下 C++ 代码:
Foo::getFoo(); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 23, __pyx_L1_error)
从而实现您所期望的 - 它检查 C++ 异常并按预期处理它。
我仍然认为这是个坏主意。
如果某些参数无效,我想在 C++ class 构造函数中引发异常,例如 PyErr_SetString(PyExc_ValueError, "Error occurred")
。不幸的是,它没有正确传播,我得到的是 SystemError: <class 'testlib.foo.Foo'> returned a result with an error set
。甚至可以让它在构造函数中工作吗?
几个问题:
- 所有逻辑都应该在 C++ class 中,而不会分散到
.pyx
个文件中。 - 验证应该在构造函数内部进行,这样就不需要在创建对象后调用单独的
init()
方法。 - 不仅可以提出一些标准异常,还可以提出任何自定义。
setup.py
from Cython.Build import cythonize
from Cython.Distutils import Extension
from setuptools import find_packages, setup
setup(
name='testlib',
python_requires='~=3.7.0',
packages=find_packages('src'),
package_dir={'': 'src'},
ext_modules=cythonize(
[
Extension(
'testlib.foo',
['src/testlib/foo.pyx', 'src/testlib/c_foo.cpp'],
extra_compile_args=['-std=c++11'], language='c++',
)
], language_level='3'
)
)
src/testlib/c_foo.h
#ifndef FOO_H
#define FOO_H
#define PY_SSIZE_T_CLEAN
#include <Python.h>
namespace foo {
class Foo {
public:
Foo();
~Foo();
};
}
#endif
src/testlib/c_foo.cpp
#include "c_foo.h"
namespace foo {
Foo::Foo() {
PyErr_SetString(PyExc_ValueError, "Error occurred");
}
Foo::~Foo() {}
}
src/testlib/c_foo.pxd
cdef extern from "c_foo.h" namespace "foo":
cdef cppclass Foo:
Foo()
src/testlib/foo.pyx
from .c_foo cimport Foo as CFoo
cdef class Foo:
cdef CFoo *c_foo
def __cinit__(self):
self.c_foo = new CFoo()
def __dealloc__(self):
del self.c_foo
tests/test_foo.py
import pytest
from testlib.foo import Foo
def test_foo():
with pytest.raises(ValueError):
Foo() # SystemError: <class 'testlib.foo.Foo'> returned a result with an error set
前两个问题可以通过 throw std::invalid_argument("Some error")
和 c_foo.pxd
中的 Foo() except +
一起解决,但它不适用于任意 python 异常。
首先,请注意这可能会出错。 Python 范围规则不同于 C++ 范围规则,Cython 主要遵循 Python 范围规则。考虑
# distutils: language=c++
cdef extern from "something.hpp":
cdef cppclass C:
C()
def f(x):
cdef C c
if x>0:
c = C()
c
必须 在你选择的任何分支都可用,因此在函数开始时默认初始化:
CYTHON_UNUSED C __pyx_v_c;
这个默认初始化不会有围绕它的标准异常处理(原则上它可能像在这种情况下那样处理 Python 异常,但在构造函数并生成用 C++ 编译的代码)。
因此,如果您的 class 有任何堆栈分配的变量,您就有可能将 Cython 置于设置异常的状态,但它认为不应该设置异常。
话虽如此,我认为这样做的方法可能是使用静态工厂函数。 Cython 确实理解这些异常规范(它不理解 c++ 构造函数上的 except *
可能是一个小错误...)
# distutils: language=c++
cdef extern from *:
"""
class Foo {
public:
Foo() {
PyErr_SetString(PyExc_ValueError, "Error occurred");
}
~Foo() {}
static Foo getFoo() {
return Foo();
}
};
"""
cdef cppclass Foo:
Foo()
@staticmethod
Foo getFoo() except *
def f():
Foo.getFoo()
此代码从 Cython 生成以下 C++ 代码:
Foo::getFoo(); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 23, __pyx_L1_error)
从而实现您所期望的 - 它检查 C++ 异常并按预期处理它。
我仍然认为这是个坏主意。