C/C++ 拓展 Python 实战 (二) —— 函数
用 C/C++ 实现的 Python 函数统称为 。
上一章 模块中的 函数能接受两个 int 参数并对它们求和。然而靠这样的功能还不能写出真正可用的 Python Module。
在本章我们将实现一个更复杂的以掌握更多必要的细节。
我们将实现一个 Module,其中有一个 函数,这个函数在 Python 中对应的定义如下:
也就是说我们实现的这个 有以下特点:
接受一个 作为第一个positional argument
接受 Python 里面的一个object 为第二个 positional argument,并且会在 里面调用它
接受一个可选的 reporter 作为key-word argument
会检查 argument 的类型,并适时抛出异常
会创建一个新的 object并返回到 Python Interpreter
这个函数恰好涵盖了我们需要特别注意的四个点: 1.处理参数,2.处理逻辑异常,3.调用其它函数,4.返回结果。
函数就是封装起来的可调用的逻辑块。封装、可调用、和逻辑块是函数的三个基本要素。
可调用是本质,函数如果不能调用其它函数或者被其它函数调用,那也就不存在函数了(第3点)。
封装是其表现,把代码块隔离开来,封装和可调用决定函数有其输入和输出(第1和4点)。
函数体的逻辑实现了函数的功能,而处理逻辑的过程中,就可能要处理异常(第2点)。
好了,不多说了,开始写代码吧。
一、 的源码
由于有了前一章的基础,所以这份代码并不难理解,它的整体架构和 类似,只是函数定义更加复杂了。
我们暂时不深入代码细节,而是先通过测试来感觉一下它的威力。
Show Time!
我们看到,第一个参数 被第二个参数 作为参数,其结果 被作为 的结果返回。
而如果传入了第三个参数 ,它将以 的 , 和返回值所构建成的 为参数被调用(此时为被打印出来)。
如上所示,我们也能全部以 kwargs 的形式提供参数,并且此时可以忽略它们在 中声明的顺序。
而如果我们提供的参数不符合规范,函数也能适时抛出异常。
嗯,完美。
下面我们按之前提到的四点深入分析 的细节。
二、处理参数
我们的 myfunc 的参数列表为 。
这已经是最复杂的 的参数列表的形式。 在处理参数时有两个最重要的特点:
不关注参数列表的形式,它们的数量固定(1-3个),类型统一()
需要额外的步骤去解析参数
2.1 不关注参数列表的形式
不关注参数的数量,也不关注参数的数据类型。参数的类型都是 ,数量最多3个:
第一个永远为 , Module 函数的 self 是 Module,Class 函数的 是 object instance;
第二个是可选的,如果被定义了,那就是 元组
第三个也是可选的,如果被定义了,那就是 字典
为什么 不关注参数的数据类型? 即使我们要在 中限定参数类型,那也只能是 或者其子类,而 Python Interpreter 传递的任何对象都是 ,所以想限制也限制不了。
那为什么也统一了参数的数量呢?就不能是 吗?实际上如果这样的话,Python 编译安装好后, 的参数形式就固定了,在 Python Interpreter 里面使用这个函数时,参数顺序只能是 , , 。而 Python 是可以使用 这种打乱了顺序的 的形式提供参数的。
2.2 需要额外的步骤去解析参数
参数都是 ,如果要参与到 C 的逻辑中去进行计算,那它就要被转换成 C 数据类型。 而由于种种限制,这种转换不能在参数列表中自动实现,因此只能我们自己来实现。
Python C API 专门提供了两个常用的解析参数的函数: 和 。前者只用于解析 元组, 后者同时解析 元组 和 字典。它们以类似的解析逻辑参数:
首先传入要解析的 (和 );
再传入一个字符串作为模板,以此来指示 (和 )应该解析出的参数数量以及它们的类型。比如 表示要解析两个 数据类型;
再依次传入参数数量的 C 指针,指针的类型要与参数的类型匹配;
所有参数解析成功,返回 ,参数值被存入对应指针中,否则返回 。
我们还是以代码来说明。
我们不去列举所有的字符和 C 数据类型的对应关系,这些在官方文档里面可以查到。
最后还需要注意一点,如果 要支持按 的形式传入和解析参数,那么在 Function table 里面必须把函数的 flag 加上 ,否则会出现 错误。
三、处理逻辑异常
在 里面我们用诸如 的函数来检查 的类型,当类型不符合要求时,抛出异常:
异常状态由三个全局的静态变量保存。第一个变量用作 flag,它如果不为 ,则编译器判断当前有异常发生;第二个保存了异常的消息,第三个变量保存了异常的 traceback。
我们一般使用 设置异常,第一个参数是异常的类型,第二个参数是异常的消息。 Python C API 以 的形式提供了所有内置的异常类型。我们也能使用 来自动根据操作系统的 来生成和设置异常。
当我们的函数设置异常消息后,需要返回 NULL,以告知调用者有异常发生了。
而当我们调用的 Python C API 提供的函数(除了 )返回了 时,说明调用的函数中发生了异常(也能用 手动检测),我们也需要返回 以传递状态(不需要再次设置异常),或者调用 以清除全局的异常状态并尝试其它的操作。
四、调用 Python 函数
我们接下来看看怎么调用 Python 中传入的函数对象。
我们首先使用 来检查对象是否为 callable 对象。然后再以 来调用对象。第一个参数为 callable,第二个参数为 元组。这个函数调用的效果等同于 Python 里面的 。
如果想以 的形式调用函数,我们可以使用 ,它接受第三个 作为 。不过此时 不能为 了( 可以为 )。
我们还能看到,为了构建一个元组,我们使用了 。这个函数调用的作用是,把 items 这个 C 数据(PyObject *) 转换为一个 Python 对象(字符O),并且包含在一个元组里(字母 O 放在括号里面)。
五、返回结果
我们的函数在重重运算中成功避开了各种异常并拿到了最终的结果。现在我们只剩下最后一件事情了,就是把结果返回给 Python Interpreter。
如果我们的结果是 C 数据,那么我们还需要把它转换成 Python 数据。转换的函数就是我们刚才看见的 。
这个函数怎么用,我们看下面这些例子就明白了:
我们可以参考官方文档以获得更相近的解释,在此就不去详细说明了。
数据转换完成 Python Object 后,我们并不能直接返回。 因为 Python 中所有的对象都存在于 heap 中,而所有的变量其实都是对某一个对象的引用。 Python 根据对象的引用计数来决定是否需要回收它。我们再看我们的代码:
调用成功的话,会返回一个非 的对象。这个新对象目前的引用计数为 0。我们使 指向了这个对象,此时引用计数成为 1。如果这时候我们马上返回结果的话,函数返回后,result 被销毁,对象的引用计数就会从 1 减到 0,从而导致结果被垃圾回收。我们就返回了一个被销毁的对象!
为了避免这种情况,我们通过 把这个对象的引用计数加 1 后再返回。
引用计数涉及到内存管理,是使用 C 拓展 Python 时需要格外小心的地方。我们在下一章展示用 C 实现 Python 的 的时候再详细说明。
好了,我们对 的四个重要特点(参数处理、逻辑异常、函数调用和返回值)有了更深入的了解, 也能写出更强大的函数了。接下来,我们就要挑战更强大的类了。
领取专属 10元无门槛券
私享最新 技术干货