Cython 的异常处理

简介: Cython 的异常处理

def 定义的函数在 C 的级别总是会返回一个 PyObject *,这个是恒定的,不会改变,因为 Python 的变量本质上就是一个 PyObject *。因此正常调用一个函数时,它的返回值一定指向了一个合法的 Python 对象。

但如果报错了,在 C 的层面返回的就不再是 PyObject *,而是一个 NULL。解释器在返回 NULL 之前,会将异常信息写入到 stderr(标准错误输出)里面。对于 Python 使用者而言,表现就是:先输出一堆错误信息,然后解释器中止运行。

cpdef test():
    lst = [0]
    # 显然会索引越界
    lst[3]
    return None

文件名仍叫 cython_test.pyx,我们测试一下。

import pyximport
pyximport.install(language_level=3)
import cython_test
print(cython_test.test())
"""
Traceback (most recent call last):
  File "...", line 5, in <module>
    print(cython_test.test())
  File "cython_test.pyx", line 1, in cython_test.test
    cpdef test():
  File "cython_test.pyx", line 4, in cython_test.test
    lst[3]
IndexError: list index out of range
"""

如果程序正常执行完毕,返回的状态码为 0;执行的时候报错了,返回的状态码为 1;如果返回的状态码很古怪,是一个乱七八糟的数字,那么说明解释器内部出现了异常,这种情况基本只有在写 C 扩展的时候才会发生。

总之,解释器抛出异常的本质就是,发现程序出错了,然后将错误信息写入到标准错误输出当中,return NULL 并停止运行。

所以异常输出在 Cython 中的表现也是一样的,它允许 Cython 正确地从函数中抛出异常。但这有一个前提,那就是函数的返回值必须是 Python 类型,显然对于 def 函数是没有问题的。而 cdef 和 cpdef 可能会返回一个非 Python 类型,那么此时则需要一些其它的异常提示机制。

我们举个例子:

cpdef long test():
    lst = [0]
    # 显然会索引越界
    lst[3]
    return 123

此时 test1 的返回值类型是 C 的类型,并且里面的代码会报错,那么 Python 在调用的时候会不会将异常抛出来呢?

import pyximport
pyximport.install(language_level=3)
import cython_test
print(cython_test.test())
"""
IndexError: list index out of range
Exception ignored in: 'cython_test.test'
Traceback (most recent call last):
  File "...", line 5, in <module>
    print(cython_test.test())
IndexError: list index out of range
0
"""
print("我会执行吗?")
"""
我会执行吗?
"""

我们看到了神奇的一幕,程序报错了,但是没有停止运行,而是将异常忽略掉了。并且当函数内部出异常的时候,自动返回零值。

CPython 在判断是否出现异常的时候,首先会根据返回值来判断。如果返回值的类型是 PyObject *,那么正常执行一定会返回一个非 NULL 指针,因为要指向一个合法的 Python 对象;可要是出现异常,那么就会返回一个空指针 NULL,代表函数执行失败,应该将异常抛出来。

因此在调用一个返回值为 Python 类型的函数时,根据返回值是否为 NULL 可以很轻松地判断调用是否出异常。所以当异常发生在 def 函数、或者返回值为 Python 类型的 cdef、cpdef 函数中,那么表现和 Python 代码是一致的。

但如果返回值是一个 C 的类型(针对 cdef 和 cpdef 函数),比如这里返回的是 long 类型,那么执行出错时会设置异常、并返回 0,也就是对应类型的零值。但问题是解释器不知道此时的 0,是因为调用出错返回的 0,还是正常执行完毕后返回的 0,因为返回值也可能是 0。

所以当返回值是 C 的类型时,如果函数调用出错,那么异常没有办法传递给它的调用方。换句话说就是异常没有办法向上抛,最终的结果就是将异常输出到 stderr 当中,但是却无法停止运行。

而为了正确传递异常,Cython 提供了一个 except 字句,允许 cdef、cpdef 函数和调用方通信,如果函数在执行过程中发生了 Python 异常,要将它抛出来。

cpdef long test() except -1:
    lst = [0]
    # 显然会索引越界
    lst[3]
    return 123

此时再调用的话,会有什么结果呢?异常能正常抛出来吗?

import pyximport
pyximport.install(language_level=3)
import cython_test
print(cython_test.test())
print("我会执行吗?")
"""
Traceback (most recent call last):
  File "...", line 5, in <module>
    print(cython_test.test())
  File "cython_test.pyx", line 1, in cython_test.test
    cpdef long test() except -1:
  File "cython_test.pyx", line 4, in cython_test.test
    lst[3]
IndexError: list index out of range
"""

我们看到此时异常就正确地传递给调用方了,当出现异常时,将异常信息打印出来,然后中止运行。

所以 except -1 相当于充当异常发生时的哨兵,当然啦不仅是 -1,任何整数都是可以的。但是还有一个问题,如果返回值恰好也是 -1 怎么办?我们举个例子:

cpdef long test(long i, long j) except -1:
    return i // j

这里需要写成 i // j,不能写 i / j,因为返回值是 long 类型,而 i / j 得到的是浮点数。虽然在变量赋值的时候,表达式计算得到的浮点数会自动向下取整,但此处不行,我们必须显式地返回一个整数。

然后测试一下:

import pyximport
pyximport.install(language_level=3)
import cython_test
try:
    cython_test.test(3, 0)
except ZeroDivisionError as e:
    print(e)
"""
integer division or modulo by zero
"""
print(ext.test(3, -3))
"""
Traceback (most recent call last):
  File "...", line 14, in <module>
    print(cython_test.test(3, -3))
SystemError: <built-in function test> returned NULL without setting an error
"""

神奇的地方出现了,虽然异常依旧能够正常抛出来,但是当返回值为 -1 时居然又抛出了一个 SystemError。

如果你使用 C 编写过扩展模块的话,应该会遇见过这个问题。前面说了,Python 的函数一定会返回一个 PyObject *,但如果函数执行出错了,那么在 C 一级就会返回一个 NULL,并且将发生的异常设置进去。

如果返回了 NULL 但是没有设置异常的话,就会抛出上面的那个错误。因为解释器发现函数返回了 NULL,所以知道出现异常了,于是会将回溯栈里设置好的异常信息打印出来,告诉开发者出现异常的具体原因,到底是哪部分代码出错了。但问题是解释器发现异常回溯栈是空的,所以会抛出一个 SystemError,表示函数返回了 NULL,但却没有设置异常。

而出现上述结果的原因就是我们这里的 except -1,它允许 -1 充当异常发生时的哨兵。但如果函数正常执行、只是返回的恰好是 -1,那么也表示发生异常了,于是底层会返回NULL。然而实际上异常并没有发生,所以没有设置异常,而解释器又发现返回值为 NULL,所以提示我们 returned NULL without setting an error。

一个比较笨的解决办法是将 except -1 换成 except -2,显然这是治标不治本,因为当返回值为 -2 的时候还是会出现上面的结果。所以能不能有这样一种机制,就是当返回值恰好和哨兵相等时,让解释器去看一眼异常回溯栈。如果回溯栈为空,证明返回的 NULL 不是因为执行出错返回的 NULL,而是返回值和哨兵恰好相等,那么此时就不要返回 NULL 了,因为没有报错,所以应该将返回值正常返回。

cpdef long test(long i, long j) except ? -1:
    return i // j

我们在 except 后面加上了一个问号,来看看执行结果。

import pyximport
pyximport.install(language_level=3)
import cython_test
print(cython_test.test(3, -3))  # -1

此时就没有问题了。

总结一下,except ? -1 只是单纯为了在发生异常的时候能够往上抛,这里可以是 -1、也可以是其它的什么值。而函数如果也返回了相同的值,那么就会检测异常回溯栈,如果为空(表示没有报错)就会正常返回。而触发检测的条件就是中间的那个 ?,如果不指定 ?,那么当函数返回了和哨兵相同的值,也是会报错的,因此这个时候你应该确保函数不可能返回 except 后面指定的值(哨兵)。

但很明显,这样的逻辑不具备可靠性,还是在 except 后面加上 ? 要更保险一些。

事实上,在 CPython 源码内部也有大量相似的判断逻辑。

调用 new_values 如果成功,那么 values 一定指向一个合法的 Python 对象,但如果调用失败,values 则为 NULL。所以下一步就要判断 values 是不是等于 NULL,如果等于 NULL,证明执行失败了,应该设置异常、然后返回 NULL。而解释器发现为 NULL 了,证明执行出现异常了,于是会将回溯栈里的异常信息打印出来,然后中止运行。

所以当返回值是 PyObject * 时,根据返回值是否为 NULL 即可判断执行是否出现了异常。但如果返回值不是 PyObject *、或者说不是 Python 类型呢?比如是 C 的整型。

我们看到 CPython 内部也是使用 -1  充当的哨兵,如果返回值不是 -1,证明正常执行。如果返回值是 -1,则说明有可能出异常了,此时需要调用 PyErr_Occurred 检测回溯栈是否为空,如果不为空,证明确实出现异常了;如果为空则证明没有出现异常,只是返回值恰好是 -1。

另外哨兵的值要和返回值类型相匹配,返回值类型为整型,那么哨兵可以是任意的整数;返回值类型是浮点型,那么哨兵可以是任意的浮点数。如果我们将 -1 改成 -1.0 的话:

编译的时候报错了,因为哨兵的值的类型和返回值类型不兼容

所以工作中建议加上 except ? val 作为异常传递的哨兵,val 的值任意,只要和返回值类型匹配即可。但只有返回值是 C 的类型时,才需要这么做。如果返回值是 Python 类型,那么使用 except 子句会报错,比如下面的代码就是不合法的。

cpdef tuple test(long i, long j) except ? ():
    pass

我们的本意是使用空元组作为异常传递的哨兵,但当返回值为 Python 类型时,异常是可以正常抛的,它的表现和 Python 是一致的。只有当返回值是 C 的类型,才需要哨兵,所以上面的代码属于画蛇添足,反而会编译错误。

报错信息很明显,当返回值是 Python 类型时,不允许使用 except 子句。

以上就是 Cython 中的异常处理,准确来说是异常在 Cython 中的一个坑,因为当返回值是 C 的类型时,异常无法正常抛给调用方,需要使用哨兵。至于异常处理本身(try),在 Cython 和 Python 中的表现都是一致的。

相关文章
|
5月前
|
安全 程序员 Python
Python中的异常处理与错误调试
【4月更文挑战第8天】本文探讨Python中的异常处理和错误调试,将其比喻为驾驶过程中的意外情况。异常是程序执行时的非正常事件,如文件缺失或网络故障,而错误是代码本身的逻辑或语法问题。Python通过try-except语句处理异常,确保程序在遇到问题时不会立即崩溃。错误调试则需定位问题根源,利用pdb等工具逐行检查代码。这两个技能对保持代码稳定性和可靠性至关重要,能增强程序应对意外的能力并帮助修复潜在问题。
29 1
|
4月前
|
运维 监控 API
Python进行异常处理
【6月更文挑战第14天】 ```markdown # Python异常处理与日志记录最佳实践概览 - 异常处理:确保程序稳定,改善用户体验,简化调试。 - `try-except`用于捕获异常,`except`针对具体异常类型,`else`处理无异常情况,`finally`确保资源释放。 - 日志记录:追踪执行,辅助诊断,监控分析。 - `logging`模块用于记录不同级别的日志,如`info`、`warning`、`error`。
34 6
Python进行异常处理
|
4月前
|
Java 索引 Python
Python 异常处理
Python 异常处理
|
4月前
|
Java 索引 Python
python异常处理
python异常处理
|
5月前
|
人工智能 数据挖掘 索引
Python中的异常处理
异常是程序执行过程中不可避免的出错情况,而在Python中,通过使用异常处理机制可以有效地应对这些异常情况。本文将介绍Python中的异常处理机制,并讲解如何使用try-except代码块来捕获和处理异常。
|
5月前
|
Python
Python 中的异常处理机制是一种强大的错误处理工具
【5月更文挑战第8天】Python的异常处理机制借助try/except结构管理错误,提高程序健壮性。异常是中断正常流程的问题,可由多种原因引发。基本结构包括try块(执行可能出错的代码)和except块(处理异常)。通过多个except块可捕获不同类型的异常,finally块确保无论是否异常都执行的代码。此外,raise语句用于主动抛出异常,自定义异常通过继承Exception类实现。with语句配合上下文管理器简化资源管理并确保异常情况下资源正确释放。
42 2
|
5月前
|
Java 编译器 数据库
Python的异常处理
Python的异常处理
|
5月前
|
Python
Python异常处理(七)
Python异常处理(七)
35 0
Python异常处理(七)
|
5月前
|
架构师 Java 程序员
Python 的异常处理
Python 的异常处理
18 0
|
12月前
|
Python
Python异常处理之分享
Python异常处理之分享