【问题标题】:Propagate exception from C++ constructor with cython使用 cython 从 C++ 构造函数传播异常
【发布时间】:2021-02-22 15:53:47
【问题描述】:

如果某些参数无效,我想在 C++ 类构造函数中引发异常,例如,执行PyErr_SetString(PyExc_ValueError, "Error occurred")。不幸的是,它没有正确传播,我得到了SystemError: <class 'testlib.foo.Foo'> returned a result with an error set。甚至有可能让它在构造函数中工作吗?

几个问题:

  • 所有逻辑都应该在 C++ 类中,而不是分散到 .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

测试/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

前两个问题可以通过在c_foo.pxd 中执行throw std::invalid_argument("Some error")Foo() except + 来解决,但它不适用于任意python 异常。

【问题讨论】:

  • 你试过except *吗(这意味着可以设置任何Python异常,无法从返回类型中判断)?不过,空的 c++ 构造函数可能存在特定问题。
  • docs.cython.org/en/latest/src/userguide/… 中描述的可能方法是否不够(通过选择适当的 C++ 异常,或尝试分配“异常转换”函数或将所有异常转换为 Py 端的某个特定异常) .
  • @DavidW 是的,我试过了,它给了Exception clause not allowed for function returning Python object
  • 这听起来像是一个错误。复杂之处在于,任何堆栈分配的 C++ 对象都在 Cython 函数的开头进行初始化,在该函数中实际上不可能添加异常检查,因此可能很难用它做很多有用的事情。
  • 你可以用except *而不是构造函数来查看静态工厂函数吗?

标签: python c++ exception cython


【解决方案1】:

首先,请注意,这可能会出错。 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++ 异常并生成可编译的代码是非常困难的在 C++ 中)。

因此,如果您的类有任何堆栈分配的变量,您可能会将 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++ 异常并按预期进行处理。


我仍然认为这是一个坏主意。

【讨论】:

    猜你喜欢
    • 2012-10-22
    • 2019-03-11
    • 2023-04-07
    • 1970-01-01
    • 1970-01-01
    • 2023-03-11
    • 1970-01-01
    • 1970-01-01
    • 2011-11-04
    相关资源
    最近更新 更多