语言基础¶
注意
此页面使用两种不同的语法变体
Cython 特定的
cdef
语法,旨在使类型声明简洁明了,并易于从 C/C++ 的角度阅读。纯 Python 语法,允许在 纯 Python 代码 中使用静态 Cython 类型声明,遵循 PEP-484 类型提示和 PEP 526 变量注释。
要在 Python 语法中使用 C 数据类型,您需要在要编译的 Python 模块中导入特殊的
cython
模块,例如import cython
如果您使用纯 Python 语法,我们强烈建议您使用最新的 Cython 3 版本,因为与 0.29.x 版本相比,这里已经进行了重大改进。
声明数据类型¶
作为一种动态语言,Python 鼓励一种编程风格,即从方法和属性的角度考虑类和对象,而不是它们在类层次结构中的位置。
这使得 Python 成为一种非常轻松舒适的语言,适合快速开发,但代价是——管理数据类型的“繁文缛节”被转嫁给了解释器。在运行时,解释器会做很多工作来搜索命名空间、获取属性以及解析参数和关键字元组。这种运行时的“后期绑定”是 Python 相对于 C++ 等“早期绑定”语言速度较慢的主要原因。
但是,使用 Cython,可以通过使用“早期绑定”编程技术来获得显著的加速。
注意
类型不是必需的
为参数和变量提供静态类型是为了方便加快代码速度,但这不是必需的。在需要的地方进行优化。事实上,在类型不允许优化但 Cython 仍然需要检查某个对象的类型是否与声明的类型匹配的情况下,类型可能会减慢代码速度。
C 变量和类型定义¶
C 变量可以通过以下方式声明
使用 Cython 特定的
cdef
语句,使用带有 C 数据类型的 PEP-484/526 类型注释,或
使用函数
cython.declare()
。
cdef
语句和 declare()
可以定义函数局部变量和模块级变量,以及类中的属性,但类型注释仅影响局部变量和属性,在模块级别被忽略。这是因为类型注释不是 Cython 特定的,所以 Cython 将模块中的变量保留在模块字典中(作为 Python 值),而不是将它们作为模块内部的 C 变量。在 Python 代码中使用 declare()
来显式定义全局 C 变量。
a_global_variable = declare(cython.int)
def func():
i: cython.int
j: cython.int
k: cython.int
f: cython.float
g: cython.float[42]
h: cython.p_float
i = j = 5
cdef int a_global_variable
def func():
cdef int i, j, k
cdef float f
cdef float[42] g
cdef float *h
# cdef float f, g[42], *h # mix of pointers, arrays and values in a single line is deprecated
i = j = 5
如 C 中所知,声明的全局变量会自动初始化为 0
、NULL
或 None
,具体取决于它们的类型。但是,如 Python 和 C 中所知,对于局部变量,仅仅声明它不足以初始化它。如果您使用了一个局部变量但没有赋值,Cython 和 C 编译器都会发出警告“局部变量…在赋值之前被引用”。您需要在第一次使用变量之前为其赋值,但您也可以在大多数情况下在声明时直接赋值
a_global_variable = declare(cython.int, 42)
def func():
i: cython.int = 10
f: cython.float = 2.5
g: cython.int[4] = [1, 2, 3, 4]
h: cython.p_float = cython.address(f)
c: cython.doublecomplex = 2 + 3j
cdef int a_global_variable = 42
def func():
cdef int i = 10, j, k
cdef float f = 2.5
cdef int[4] g = [1, 2, 3, 4]
cdef float *h = &f
cdef double complex c = 2 + 3j
注意
还支持使用 ctypedef
语句或 cython.typedef()
函数为类型命名,例如
ULong = cython.typedef(cython.ulong)
IntPtr = cython.typedef(cython.p_int)
ctypedef unsigned long ULong
ctypedef int* IntPtr
C 数组¶
C 数组可以通过在变量类型中添加 [ARRAY_SIZE]
来声明
def func():
g: cython.float[42]
f: cython.int[5][5][5]
ptr_char_array: cython.pointer(cython.char[4]) # pointer to the array of 4 chars
array_ptr_char: cython.p_char[4] # array of 4 char pointers
def func():
cdef float[42] g
cdef int[5][5][5] f
cdef char[4] *ptr_char_array # pointer to the array of 4 chars
cdef (char *)[4] array_ptr_char # array of 4 char pointers
注意
Cython 语法目前支持两种声明数组的方式
cdef int arr1[4], arr2[4] # C style array declaration
cdef int[4] arr1, arr2 # Java style array declaration
两种声明方式都会生成相同的 C 代码,但 Java 风格的声明与 类型化内存视图 和 融合类型(模板) 更加一致。C 风格的声明已被软弃用,建议使用 Java 风格的声明。
已被软弃用的 C 风格数组声明不支持初始化。
cdef int g[4] = [1, 2, 3, 4] # error
cdef int[4] g = [1, 2, 3, 4] # OK
cdef int g[4] # OK but not recommended
g = [1, 2, 3, 4]
结构体、联合体、枚举¶
除了基本类型之外,C struct
、union
和 enum
也受支持。
Grail = cython.struct(
age=cython.int,
volume=cython.float)
def main():
grail: Grail = Grail(5, 3.0)
print(grail.age, grail.volume)
cdef struct Grail:
int age
float volume
def main():
cdef Grail grail = Grail(5, 3.0)
print(grail.age, grail.volume)
结构体可以声明为 cdef packed struct
,这与 C 指令 #pragma pack(1)
的效果相同。
cdef packed struct StructArray:
int[4] spam
signed char[5] eggs
注意
此声明会移除 C 自动添加的成员之间的空隙,以确保它们在内存中对齐(有关更多详细信息,请参阅 维基百科文章)。主要用途是 numpy 结构化数组以打包形式存储其数据,因此 cdef packed struct
可以 在内存视图中使用 来匹配它。
纯 Python 模式不支持打包结构体。
以下示例展示了联合体的声明。
Food = cython.union(
spam=cython.p_char,
eggs=cython.p_float)
def main():
arr: cython.p_float = [1.0, 2.0]
spam: Food = Food(spam='b')
eggs: Food = Food(eggs=arr)
print(spam.spam, eggs.eggs[0])
cdef union Food:
char *spam
float *eggs
def main():
cdef float *arr = [1.0, 2.0]
cdef Food spam = Food(spam='b')
cdef Food eggs = Food(eggs=arr)
print(spam.spam, eggs.eggs[0])
枚举通过 cdef enum
语句创建。
cdef enum CheeseType:
cheddar, edam,
camembert
cdef enum CheeseState:
hard = 1
soft = 2
runny = 3
print(CheeseType.cheddar)
print(CheeseState.hard)
注意
目前,纯 Python 模式不支持枚举。(GitHub 问题 #4252)
将枚举声明为 cpdef
将创建一个 PEP 435 风格的 Python 包装器。
cpdef enum CheeseState:
hard = 1
soft = 2
runny = 3
目前没有定义常量的特殊语法,但您可以为此目的使用匿名 enum
声明,例如:
cdef enum:
tons_of_spam = 3
注意
在 Cython 语法中,struct
、union
和 enum
这些词仅在定义类型时使用,而不是在引用类型时使用。例如,要声明一个指向 Grail
结构体的变量,您应该编写:
cdef Grail *gp
而不是:
cdef struct Grail *gp # WRONG
类型¶
Cython 语言使用 C 类型的标准 C 语法,包括指针。它提供所有标准 C 类型,即 char
、short
、int
、long
、long long
以及它们的 unsigned
版本,例如 unsigned int
(在 Python 代码中为 cython.uint
)。
Cython 类型 |
纯 Python 类型 |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
注意
其他类型在 stdint pxd 文件 中声明。
特殊类型 bint
用于 C 布尔值(int
,其中 0/非 0 值分别表示 False/True)和 Py_ssize_t
用于 Python 容器的(带符号)大小。
在使用 Cython 语法时,指针类型与 C 中的构造方式相同,在它们指向的基本类型后面附加一个 *
,例如 int**
表示指向指向 C int 的指针的指针。在纯 Python 模式下,简单指针类型使用带有“p”的命名方案,用下划线与类型名称隔开,例如 cython.pp_int
表示指向指向 C int 的指针的指针。可以使用 cython.pointer()
函数构造更深层的指针类型,例如 cython.pointer(cython.int)
。
数组使用正常的 C 数组语法,例如 int[10]
,并且对于栈分配的数组,大小必须在编译时已知。Cython 不支持 C99 中的可变长度数组。请注意,Cython 使用数组访问来进行指针解引用,因为 *x
不是有效的 Python 语法,而 x[0]
是有效的。
此外,Python 类型 list
、dict
、tuple
等可以用于静态类型,以及任何用户定义的 扩展类型。例如
def main():
foo: list = []
cdef list foo = []
这需要类完全匹配,不允许子类。这允许 Cython 通过访问内置类的内部来优化代码,这也是在第一位声明内置类型的主要原因。
对于声明的内置类型,Cython 在内部使用类型为 PyObject* 的 C 变量。
注意
Python 类型 int
、long
和 float
在 .pyx
文件中不可用于静态类型,而是分别解释为 C int
、long
和 float
,因为使用这些 Python 类型静态类型变量没有任何优势。另一方面,在纯 Python 中使用 int
、long
和 float
Python 类型进行注释将被解释为 Python 对象类型。
Cython 提供了 Python 元组的加速和类型化等效项,即 ctuple
。一个 ctuple
由任何有效的 C 类型组成。例如
def main():
bar: tuple[cython.double, cython.int]
cdef (double, int) bar
它们编译为 C 结构,可以用作 Python 元组的有效替代方案。
虽然这些 C 类型可能快得多,但它们具有 C 语义。具体来说,整数类型会溢出,C float
类型只有 32 位精度(与 Python 浮点数包装的 64 位 C double
相比,Python 浮点数包装通常是人们想要的)。如果你想使用这些数值 Python 类型,只需省略类型声明,让它们成为对象。
类型限定符¶
Cython 支持 const
和 volatile
C 类型限定符
cdef volatile int i = 5
cdef const int sum(const int a, const int b):
return a + b
cdef void print_const_pointer(const int *value):
print(value[0])
cdef void print_pointer_to_const_value(int * const value):
print(value[0])
cdef void print_const_pointer_to_const_value(const int * const value):
print(value[0])
注意
纯 Python 模式不支持这两种类型限定符。此外,const
修饰符在许多情况下不可用,因为 Cython 需要分别生成定义及其赋值。因此,我们建议主要在函数参数和指针类型中使用它,在这些类型中,const
是与现有 C/C++ 接口协作所必需的。
扩展类型¶
还可以声明 扩展类型(使用 cdef class
或 @cclass
装饰器声明)。它们的行为非常接近 Python 类(例如创建子类),但从 Cython 代码访问其成员的速度更快。将变量类型化为扩展类型主要用于访问扩展类型的 cdef
/@cfunc
方法和属性。C 代码使用一个指向特定类型结构的指针的变量,类似于 struct MyExtensionTypeObject*
。
以下是一个简单的示例
@cython.cclass
class Shrubbery:
width: cython.int
height: cython.int
def __init__(self, w, h):
self.width = w
self.height = h
def describe(self):
print("This shrubbery is", self.width,
"by", self.height, "cubits.")
cdef class Shrubbery:
cdef int width
cdef int height
def __init__(self, w, h):
self.width = w
self.height = h
def describe(self):
print("This shrubbery is", self.width,
"by", self.height, "cubits.")
您可以在 扩展类型 中了解更多信息。
对多个 C 声明进行分组¶
如果您有一系列声明,它们都以 cdef
开头,您可以将它们分组到一个 cdef
块中,如下所示
注意
这仅在 Cython 的 cdef
语法中受支持。
cdef:
struct Spam:
int tons
int i
float a
Spam *p
void f(Spam *s) except *:
print(s.tons, "Tons of spam")
Python 函数与 C 函数¶
Cython 中有两种函数定义
Python 函数使用 def
语句定义,与 Python 中一样。它们接受 Python 对象 作为参数并返回 Python 对象。
C 函数使用 Cython 语法中的 cdef
语句或 @cfunc
装饰器定义。它们接受 Python 对象或 C 值作为参数,并且可以返回 Python 对象或 C 值。
在 Cython 模块中,Python 函数和 C 函数可以自由地相互调用,但只有 Python 函数可以被解释的 Python 代码从模块外部调用。因此,您要从 Cython 模块“导出”的任何函数都必须使用 def
声明为 Python 函数。还有一种混合函数,在 .pyx
文件中使用 cpdef
或 @ccall
装饰器声明。这些函数可以从任何地方调用,但从其他 Cython 代码调用时使用更快的 C 调用约定。它们也可以被子类上的 Python 方法或实例属性覆盖,即使是从 Cython 调用也是如此。如果发生这种情况,大多数性能提升当然会丢失,即使没有,从 Cython 调用这种方法与调用 C 方法相比也有一点开销。
两种类型的函数的参数都可以声明为具有 C 数据类型,使用正常的 C 声明语法。例如,
def spam(i: cython.int, s: cython.p_char):
...
@cython.cfunc
def eggs(l: cython.ulong, f: cython.float) -> cython.int:
...
def spam(int i, char *s):
...
cdef int eggs(unsigned long l, float f):
...
ctuples
也可以使用
@cython.cfunc
def chips(t: tuple[cython.long, cython.long, cython.double]) -> tuple[cython.int, cython.float]:
...
cdef (int, float) chips((long, long, double) t):
...
当 Python 函数的参数被声明为具有 C 数据类型时,它被作为 Python 对象传入,并在可能的情况下自动转换为 C 值。换句话说,上面 spam
的定义等效于编写
def spam(python_i, python_s):
i: cython.int = python_i
s: cython.p_char = python_s
...
def spam(python_i, python_s):
cdef int i = python_i
cdef char* s = python_s
...
自动转换目前仅对数字类型、字符串类型和结构体(递归地由这些类型中的任何一种组成)有效;尝试对 Python 函数的参数使用任何其他类型会导致编译时错误。在使用字符串时必须注意确保引用,如果指针在调用后要使用。结构体可以从 Python 映射中获取,同样,如果字符串属性在函数返回后要使用,则必须注意。
另一方面,C 函数可以具有任何类型的参数,因为它们是使用正常的 C 函数调用直接传入的。
使用 cdef
或带有 Python 对象返回值的 @cfunc
装饰器声明的 C 函数,与 Python 函数一样,当执行离开函数体而没有显式返回值时,将返回 None
值。这与 C/C++ 不同,C/C++ 会将返回值留作未定义。对于非 Python 对象返回值类型,将返回等效于零的值,例如,int
为 0,bint
为 False
,指针类型为 NULL
。
可以在 早期绑定以提高速度 中找到这些不同方法类型的优缺点的更完整比较。
Python 对象作为参数和返回值¶
如果参数或返回值没有指定类型,则假定它为 Python 对象。(请注意,这与 C 约定不同,C 约定将默认设置为 int
。)例如,以下定义了一个 C 函数,该函数将两个 Python 对象作为参数并返回一个 Python 对象
@cython.cfunc
def spamobjs(x, y):
...
cdef spamobjs(x, y):
...
这些对象的引用计数将根据标准 Python/C API 规则自动执行(即,作为参数获取借用引用,并返回新引用)。
警告
这仅适用于 Cython 代码。其他用 C 实现的 Python 包(如 NumPy)可能不遵循这些约定。
类型名称 object
也可以用来显式地声明某事物为 Python 对象。如果要声明的名称会被用作类型的名称,这将很有用,例如,
@cython.cfunc
def ftang(int: object):
...
cdef ftang(object int):
...
声明一个名为 int
的参数,它是一个 Python 对象。您也可以使用 object
作为函数的显式返回值类型,例如
@cython.cfunc
def ftang(int: object) -> object:
...
cdef object ftang(object int):
...
为了清晰起见,最好始终明确 C 函数中的对象参数。
要创建借用引用,请将参数类型指定为 PyObject*。Cython 不会自动执行 Py_INCREF()
或 Py_DECREF()
,例如
# Py_REFCNT and _Py_REFCNT are the same, except _Py_REFCNT takes
# a raw pointer and Py_REFCNT takes a normal Python object
from cython.cimports.cpython.ref import PyObject, _Py_REFCNT, Py_REFCNT
import sys
python_dict = {"abc": 123}
python_dict_refcount = Py_REFCNT(python_dict)
@cython.cfunc
def owned_reference(obj: object):
refcount1 = Py_REFCNT(obj)
print(f'Inside owned_reference initially: {refcount1}')
another_ref_to_object = obj
refcount2 = Py_REFCNT(obj)
print(f'Inside owned_reference after new ref: {refcount2}')
@cython.cfunc
def borrowed_reference(obj: cython.pointer(PyObject)):
refcount1 = _Py_REFCNT(obj)
print(f'Inside borrowed_reference initially: {refcount1}')
another_ptr_to_object = obj
refcount2 = _Py_REFCNT(obj)
print(f'Inside borrowed_reference after new pointer: {refcount2}')
# Casting to a managed reference to call a cdef function doesn't increase the count
refcount3 = Py_REFCNT(cython.cast(object, obj))
print(f'Inside borrowed_reference with temporary managed reference: {refcount3}')
# However calling a Python function may depending on the Python version and the number
# of arguments.
print(f'Initial refcount: {python_dict_refcount}')
owned_reference(python_dict)
borrowed_reference(cython.cast(cython.pointer(PyObject), python_dict))
# Py_REFCNT and _Py_REFCNT are the same, except _Py_REFCNT takes
# a raw pointer and Py_REFCNT takes a normal Python object
from cpython.ref cimport PyObject, _Py_REFCNT, Py_REFCNT
import sys
python_dict = {"abc": 123}
python_dict_refcount = Py_REFCNT(python_dict)
cdef owned_reference(object obj):
refcount1 = Py_REFCNT(obj)
print(f'Inside owned_reference initially: {refcount1}')
another_ref_to_object = obj
refcount2 = Py_REFCNT(obj)
print(f'Inside owned_reference after new ref: {refcount2}')
cdef borrowed_reference(PyObject * obj):
refcount1 = _Py_REFCNT(obj)
print(f'Inside borrowed_reference initially: {refcount1}')
another_ptr_to_object = obj
refcount2 = _Py_REFCNT(obj)
print(f'Inside borrowed_reference after new pointer: {refcount2}')
# Casting to a managed reference to call a cdef function doesn't increase the count
refcount3 = Py_REFCNT(<object>obj)
print(f'Inside borrowed_reference with temporary managed reference: {refcount3}')
# However calling a Python function may depending on the Python version and the number
# of arguments.
print(f'Initial refcount: {python_dict_refcount}')
owned_reference(python_dict)
borrowed_reference(<PyObject *>python_dict)
将显示
Initial refcount: 2
Inside owned_reference initially: 2
Inside owned_reference after new ref: 3
Inside borrowed_reference initially: 2
Inside borrowed_reference after new pointer: 2
Inside borrowed_reference with temporary managed reference: 2
可选参数¶
与 C 不同,在 C 和 cpdef
/@ccall
函数中可以使用可选参数。但是,您是在 .pyx
/.py
文件中声明它们,还是在相应的 .pxd
文件中声明它们,这会有所不同。
为了避免重复(以及潜在的未来不一致),默认参数值在声明中不可见(在 .pxd
文件中),而只在实现中可见(在 .pyx
文件中)。
当在 .pyx
/.py
文件中时,签名与 Python 本身中的签名相同
@cython.cclass
class A:
@cython.cfunc
def foo(self):
print("A")
@cython.cclass
class B(A):
@cython.cfunc
def foo(self, x=None):
print("B", x)
@cython.cclass
class C(B):
@cython.ccall
def foo(self, x=True, k:cython.int = 3):
print("C", x, k)
cdef class A:
cdef foo(self):
print("A")
cdef class B(A):
cdef foo(self, x=None):
print("B", x)
cdef class C(B):
cpdef foo(self, x=True, int k=3):
print("C", x, k)
当在 .pxd
文件中时,签名会有所不同,例如:cdef foo(x=*)
。这是因为调用函数的程序只需要知道 C 中可能的签名,而不需要知道默认参数的值。
cdef class A:
cdef foo(self)
cdef class B(A):
cdef foo(self, x=*)
cdef class C(B):
cpdef foo(self, x=*, int k=*)
注意
子类化时,参数数量可能会增加,但参数类型和顺序必须保持一致,如上例所示。
当可选参数被没有默认值的参数覆盖时,可能会出现轻微的性能损失。
仅限关键字参数¶
与 Python 3 一样,def
函数可以包含仅限关键字参数,这些参数列在 "*"
参数之后,并在 "**"
参数之前(如果有)。
def f(a, b, *args, c, d = 42, e, **kwds):
...
# We cannot call f with less verbosity than this.
foo = f(4, "bar", c=68, e=1.0)
如上所示,c
、d
和 e
参数不能作为位置参数传递,必须作为关键字参数传递。此外,c
和 e
是**必需的**关键字参数,因为它们没有默认值。
可以使用单个 "*"
(不带参数名称)来终止位置参数列表。
def g(a, b, *, c, d):
...
# We cannot call g with less verbosity than this.
foo = g(4.0, "something", c=68, d="other")
如上所示,签名接受正好两个位置参数,并具有两个必需的关键字参数。
函数指针¶
注意
纯 Python 模式目前不支持指向函数的指针。(GitHub 问题 #4279)
以下示例展示了声明一个 ptr_add
函数指针并将 add
函数分配给它。
cdef int(*ptr_add)(int, int)
cdef int add(int a, int b):
return a + b
ptr_add = add
print(ptr_add(1, 3))
在 struct
中声明的函数会自动转换为函数指针。
cdef struct Bar:
int sum(int a, int b)
cdef int add(int a, int b):
return a + b
cdef Bar bar = Bar(add)
print(bar.sum(1, 2))
有关使用函数指针的错误返回值,请参阅 错误返回值 底部的说明。
错误返回值¶
在 Python(更准确地说,在 CPython 运行时)中,函数内部发生的异常会通过定义的错误返回值向调用者发出信号,并通过调用堆栈向上传播。对于返回 Python 对象(因此,指向此类对象的指针)的函数,错误返回值只是 NULL
指针,因此任何返回 Python 对象的函数都具有明确定义的错误返回值。
虽然这始终适用于 Python 函数,但定义为 C 函数或 cpdef
/@ccall
函数的函数可以返回任意 C 类型,这些类型没有明确定义的错误返回值。默认情况下,Cython 使用专用返回值来指示从非外部 cpdef
/@ccall
函数引发的异常。但是,如果需要,可以更改 Cython 处理这些函数异常的方式。
可以为 cdef
函数声明异常返回值,作为与调用者的契约。以下是一个示例。
@cython.cfunc
@cython.exceptval(-1)
def spam() -> cython.int:
...
cdef int spam() except -1:
...
使用此声明,每当 spam
内部发生异常时,它将立即返回 -1
值。从调用者的角度来看,每当对 spam 的调用返回 -1
时,调用者将假设已发生异常,现在可以处理或传播它。调用 spam()
大致转换为以下 C 代码。
ret_val = spam();
if (ret_val == -1) goto error_handler;
当您为函数声明异常值时,您永远不应该显式或隐式返回该值。这包括没有返回值的空 return
语句,对于这些语句,Cython 会插入默认返回值(例如,对于 C 数字类型,插入 0
)。通常,异常返回值最好从函数的无效或非常不可能的返回值中选择,例如,对于仅返回非负结果的函数,选择负值,或者对于“通常”仅返回较小结果的函数,选择非常大的值,例如 INT_MAX
。
如果所有可能的返回值都是合法的,并且您无法完全保留一个用于信号错误,则可以使用另一种形式的异常值声明。
@cython.cfunc
@cython.exceptval(-1, check=True)
def spam() -> cython.int:
...
关键字参数 check=True
表示值 -1
**可能** 信号错误。
cdef int spam() except? -1:
...
该 ?
表示值 -1
**可能** 信号错误。
在这种情况下,Cython 会生成对 PyErr_Occurred()
如果返回异常值,以确保它确实收到了异常,而不仅仅是正常结果。调用 spam()
大致转换为以下 C 代码
ret_val = spam();
if (ret_val == -1 && PyErr_Occurred()) goto error_handler;
还有第三种形式的异常值声明
@cython.cfunc
@cython.exceptval(check=True)
def spam() -> cython.void:
...
cdef void spam() except *:
...
此形式导致 Cython 在每次调用 spam 后生成对 PyErr_Occurred()
,无论它返回什么值。调用 spam()
大致转换为以下 C 代码
spam()
if (PyErr_Occurred()) goto error_handler;
如果您有一个返回 void
需要传播错误的函数,您将不得不使用此形式,因为没有错误返回值可供测试。否则,显式错误返回值允许 C 编译器生成更有效的代码,因此通常更可取。
可以声明一个可能引发异常的外部 C++ 函数
cdef int spam() except +
注意
这些声明不用于 Python 代码,仅用于 .pxd
和 .pyx
文件。
有关更多详细信息,请参阅 在 Cython 中使用 C++。
最后,如果您确定您的函数不应该引发异常(例如,它根本不使用 Python 对象,或者您计划将其用作 C 代码中的回调,而 C 代码不知道 Python 异常),您可以使用 noexcept
或通过 @cython.exceptval(check=False)
声明它。
@cython.cfunc
@cython.exceptval(check=False)
def spam() -> cython.int:
...
cdef int spam() noexcept:
...
如果一个 noexcept
函数确实以异常结束,它将打印警告消息,但不允许异常进一步传播。另一方面,调用一个 noexcept
函数与管理异常相关的开销为零,这与之前的声明不同。
一些需要注意的事项
cdef
函数也是extern
隐式声明为noexcept
或@cython.exceptval(check=False)
。在外部 C/C++ 函数可以引发 Python 异常的罕见情况下,例如使用 Python C API 的外部函数,您应该使用异常值显式声明它们。cdef
函数不是extern
隐式声明为具有适合返回类型的异常规范(例如,except *
或@cython.exceptval(check=True)
用于void
返回类型,except? -1
或@cython.exceptval(-1, check=True)
用于int
返回类型)。异常值只能为返回 C 整数、枚举、浮点数或指针类型的函数声明,并且该值必须是常量表达式。返回
void
或按值返回结构体/联合体的函数只能使用except *
或exceptval(check=True)
形式。异常值规范是函数签名的一部分。如果您将指向函数的指针作为参数传递或将其分配给变量,则参数或变量的声明类型必须具有相同的异常值规范(或没有)。以下是一个带有异常值的指向函数指针声明的示例
int (*grail)(int, char*) except -1
注意
纯 Python 模式目前不支持指向函数的指针。(GitHub 问题 #4279)
如果带有
except *
或@cython.exceptval(check=True)
的cdef
函数的返回值类型为 C 整数、枚举、浮点数或指针类型,Cython 仅在返回专用值时调用PyErr_Occurred()
,而不是在每次调用函数后进行检查。您不需要(也不应该)为返回 Python 对象的函数声明异常值。请记住,没有声明返回值类型的函数隐式地返回一个 Python 对象。(此类函数上的异常通过返回
NULL
隐式传播。)将
nogil
和except *
@cython.exceptval(check=True)
结合使用时,存在一个已知的性能陷阱。在这种情况下,Cython 必须在函数调用后始终短暂地重新获取 GIL 以检查是否已引发异常。这通常会发生在返回空值(Cvoid
)的函数中。简单的解决方法是,如果您确定不会抛出异常,则将函数标记为noexcept
,或者将返回值类型更改为int
,并让 Cython 使用返回值作为错误标志(默认情况下,-1
会触发异常检查)。
检查非 Cython 函数的返回值¶
重要的是要理解,当返回指定值时,except 子句不会导致引发错误。例如,您不能编写类似
cdef extern FILE *fopen(char *filename, char *mode) except NULL # WRONG!
并期望在调用 fopen()
返回 NULL
时自动引发异常。except 子句的作用并非如此;它的唯一目的是传播已由 Cython 函数或调用 Python/C API 例程的 C 函数引发的 Python 异常。要从非 Python 感知函数(如 fopen()
)获取异常,您必须检查返回值并自行引发异常,例如
from cython.cimports.libc.stdio import FILE, fopen
from cython.cimports.libc.stdlib import malloc, free
from cython.cimports.cpython.exc import PyErr_SetFromErrnoWithFilenameObject
def open_file():
p = fopen("spam.txt", "r") # The type of "p" is "FILE*", as returned by fopen().
if p is cython.NULL:
PyErr_SetFromErrnoWithFilenameObject(OSError, "spam.txt")
...
def allocating_memory(number=10):
# Note that the type of the variable "my_array" is automatically inferred from the assignment.
my_array = cython.cast(p_double, malloc(number * cython.sizeof(double)))
if not my_array: # same as 'is NULL' above
raise MemoryError()
...
free(my_array)
from libc.stdio cimport FILE, fopen
from libc.stdlib cimport malloc, free
from cpython.exc cimport PyErr_SetFromErrnoWithFilenameObject
def open_file():
cdef FILE* p
p = fopen("spam.txt", "r")
if p is NULL:
PyErr_SetFromErrnoWithFilenameObject(OSError, "spam.txt")
...
def allocating_memory(number=10):
cdef double *my_array = <double *> malloc(number * sizeof(double))
if not my_array: # same as 'is NULL' above
raise MemoryError()
...
free(my_array)
在扩展类型中覆盖¶
cpdef
/@ccall
方法可以覆盖 C 方法
@cython.cclass
class A:
@cython.cfunc
def foo(self):
print("A")
@cython.cclass
class B(A):
@cython.cfunc
def foo(self, x=None):
print("B", x)
@cython.cclass
class C(B):
@cython.ccall
def foo(self, x=True, k:cython.int = 3):
print("C", x, k)
cdef class A:
cdef foo(self):
print("A")
cdef class B(A):
cdef foo(self, x=None):
print("B", x)
cdef class C(B):
cpdef foo(self, x=True, int k=3):
print("C", x, k)
当使用 Python 类对扩展类型进行子类化时,Python 方法可以覆盖 cpdef
/@ccall
方法,但不能覆盖普通 C 方法
@cython.cclass
class A:
@cython.cfunc
def foo(self):
print("A")
@cython.cclass
class B(A):
@cython.ccall
def foo(self):
print("B")
class C(B): # NOTE: no cclass decorator
def foo(self):
print("C")
cdef class A:
cdef foo(self):
print("A")
cdef class B(A):
cpdef foo(self):
print("B")
class C(B): # NOTE: not cdef class
def foo(self):
print("C")
如果上面的 C
是扩展类型(cdef class
),则此操作将无法正常工作。在这种情况下,Cython 编译器会发出警告。
自动类型转换¶
在大多数情况下,当 Python 对象用于需要 C 值的上下文中时,或者反之亦然,将对基本数值和字符串类型执行自动转换。下表总结了转换可能性。
C 类型 |
来自 Python 类型 |
到 Python 类型 |
---|---|---|
[unsigned] char, [unsigned] short, int, long |
int, long |
int |
unsigned int, unsigned long, [unsigned] long long |
int, long |
long |
float, double, long double |
int, long, float |
float |
char* |
str/字节 |
str/字节 [3] |
C 数组 |
可迭代对象 |
列表 [6] |
结构体,联合体 |
在 C 上下文中使用 Python 字符串时的注意事项¶
在期望 char*
的上下文中使用 Python 字符串时,您需要小心。在这种情况下,将使用指向 Python 字符串内容的指针,该指针仅在 Python 字符串存在时有效。因此,您需要确保对原始 Python 字符串的引用在需要 C 字符串的整个时间内都保持有效。如果您无法保证 Python 字符串的生存时间足够长,则需要复制 C 字符串。
Cython 会检测并阻止此类错误。例如,如果您尝试以下操作:
def main():
s: cython.p_char
s = pystring1 + pystring2
cdef char *s
s = pystring1 + pystring2
那么 Cython 将产生错误消息 Storing unsafe C derivative of temporary Python reference
。原因是连接两个 Python 字符串会产生一个新的 Python 字符串对象,该对象仅由 Cython 生成的临时内部变量引用。一旦语句完成,临时变量将被递减,Python 字符串将被释放,从而使 s
悬空。由于此代码不可能工作,因此 Cython 拒绝编译它。
解决方案是将连接结果分配给 Python 变量,然后从中获取 char*
,即
def main():
s: cython.p_char
p = pystring1 + pystring2
s = p
cdef char *s
p = pystring1 + pystring2
s = p
然后,您有责任在必要时保持对 p 的引用。
请记住,用于检测此类错误的规则只是启发式方法。有时 Cython 会不必要地抱怨,有时它会无法检测到存在的问题。最终,您需要了解问题并注意您的操作。
类型转换¶
Cython 语言以类似于 C 的方式支持类型转换。C 使用 "("
和 ")"
,而 Cython 使用 "<"
和 ">"
。在纯 Python 模式下,使用 cython.cast()
函数。例如
def main():
p: cython.p_char
q: cython.p_float
p = cython.cast(cython.p_char, q)
当将 C 值转换为 Python 对象类型或反之亦然时,Cython 将尝试进行强制转换。简单的例子是像 cast(int, pyobj_value)
这样的转换,它将 Python 数字转换为普通的 C int
值,或者语句 cast(bytes, charptr_value)
,它将 C char*
字符串复制到新的 Python 字节对象中。
注意
Cython 不会阻止冗余的转换,但会为此发出警告。
要获取某个 Python 对象的地址,请使用转换为指针类型的转换,例如 cast(p_void, ...)
或 cast(pointer(PyObject), ...)
。您也可以将 C 指针转换回 Python 对象引用,使用 cast(object, ...)
,或转换为更具体的内置类型或扩展类型(例如 cast(MyExtType, ptr)
)。这将使对象的引用计数增加一,即转换返回一个拥有引用。以下是一个例子
cdef char *p
cdef float *q
p = <char*>q
当将 C 值转换为 Python 对象类型或反之亦然时,Cython 将尝试进行强制转换。简单的例子是像 <int>pyobj_value
这样的转换,它将 Python 数字转换为普通的 C int
值,或者语句 <bytes>charptr_value
,它将 C char*
字符串复制到新的 Python 字节对象中。
注意
Cython 不会阻止冗余的转换,但会为此发出警告。
要获取某个 Python 对象的地址,请使用转换为指针类型的转换,例如 <void*>
或 <PyObject*>
。您也可以将 C 指针转换回 Python 对象引用,使用 <object>
,或转换为更具体的内置类型或扩展类型(例如 <MyExtType>ptr
)。这将使对象的引用计数增加一,即转换返回一个拥有引用。以下是一个例子
cdef extern from *:
ctypedef Py_ssize_t Py_intptr_t
from cython.cimports.cpython.ref import PyObject
def main():
python_string = "foo"
# Note that the variables below are automatically inferred
# as the correct pointer type that is assigned to them.
# They do not need to be typed explicitly.
ptr = cython.cast(cython.p_void, python_string)
adress_in_c = cython.cast(Py_intptr_t, ptr)
address_from_void = adress_in_c # address_from_void is a python int
ptr2 = cython.cast(cython.pointer(PyObject), python_string)
address_in_c2 = cython.cast(Py_intptr_t, ptr2)
address_from_PyObject = address_in_c2 # address_from_PyObject is a python int
assert address_from_void == address_from_PyObject == id(python_string)
print(cython.cast(object, ptr)) # Prints "foo"
print(cython.cast(object, ptr2)) # prints "foo"
使用 cast(object, ...)
进行转换会创建一个拥有引用。Cython 将自动执行 Py_INCREF()
和 Py_DECREF()
操作。转换为 cast(pointer(PyObject), ...)
会创建一个借用引用,保持引用计数不变。
cdef extern from *:
ctypedef Py_ssize_t Py_intptr_t
from cpython.ref cimport PyObject
python_string = "foo"
cdef void* ptr = <void*>python_string
cdef Py_intptr_t adress_in_c = <Py_intptr_t>ptr
address_from_void = adress_in_c # address_from_void is a python int
cdef PyObject* ptr2 = <PyObject*>python_string
cdef Py_intptr_t address_in_c2 = <Py_intptr_t>ptr2
address_from_PyObject = address_in_c2 # address_from_PyObject is a python int
assert address_from_void == address_from_PyObject == id(python_string)
print(<object>ptr) # Prints "foo"
print(<object>ptr2) # prints "foo"
<...>
的优先级是这样的:<type>a.b.c
被解释为 <type>(a.b.c)
。
转换为 <object>
会创建一个拥有引用。Cython 将自动执行 Py_INCREF()
和 Py_DECREF()
操作。转换为 <PyObject *>
会创建一个借用引用,保持引用计数不变。
检查类型转换¶
像 <MyExtensionType>x
或 cast(MyExtensionType, x)
这样的转换将把 x
转换为类 MyExtensionType
,而没有任何检查。
要检查类型是否匹配,在 Cython 语法中使用 <MyExtensionType?>x
或 cast(MyExtensionType, x, typecheck=True)
。在这种情况下,Cython 会进行运行时检查,如果 x
不是 MyExtensionType
的实例,则会抛出 TypeError
异常。对于内置类型,这会检查确切的类,但允许 扩展类型 的子类。
语句和表达式¶
控制结构和表达式在大多数情况下遵循 Python 语法。当应用于 Python 对象时,它们具有与 Python 中相同的语义(除非另有说明)。大多数 Python 运算符也可以应用于 C 值,具有明显的语义。
如果 Python 对象和 C 值在表达式中混合使用,则会在 Python 对象和 C 数值或字符串类型之间自动进行转换。
所有 Python 对象的引用计数都会自动维护,并且所有 Python 操作都会自动检查错误,并采取相应的措施。
C 和 Cython 表达式之间的差异¶
C 表达式和 Cython 表达式在语法和语义上存在一些差异,特别是在 C 结构体没有 Python 中的直接等效项的情况下。
整数文字被视为 C 常量,并将被截断为 C 编译器认为合适的任何大小。要获取 Python 整数(任意精度),请立即转换为对象(例如
<object>100000000000000000000
或cast(object, 100000000000000000000)
)。L
、LL
和U
后缀在 Cython 语法中的含义与 C 中相同。Cython 中没有
->
运算符。不要使用p->x
,而是使用p.x
Cython 中没有一元
*
运算符。不要使用*p
,而是使用p[0]
Cython 中有一个
&
运算符,语义与 C 中相同。在纯 Python 模式下,请改用cython.address()
函数。空 C 指针称为
NULL
,而不是0
。NULL
是 Cython 中的保留字,cython.NULL
是纯 Python 模式中的特殊对象。类型转换写成
<type>value
或cast(type, value)
,例如,def main(): p: cython.p_char q: cython.p_float p = cython.cast(cython.p_char, q)
cdef char* p cdef float* q p = <char*>q
作用域规则¶
Cython 完全静态地确定变量属于局部作用域、模块作用域还是内置作用域。与 Python 一样,对未声明的变量进行赋值会隐式地将其声明为驻留在赋值作用域中的变量。变量的类型取决于类型推断,除了全局模块作用域,它始终是 Python 对象。
内置函数¶
Cython 将对大多数内置函数的调用编译为对相应 Python/C API 例程的直接调用,使其特别快。
只有使用这些名称的直接函数调用才会被优化。如果您对这些名称中的一个执行其他操作,假设它是一个 Python 对象,例如将其分配给 Python 变量,然后稍后调用它,则该调用将作为 Python 函数调用进行。
函数和参数 |
返回类型 |
Python/C API 等效项 |
---|---|---|
abs(obj) |
对象,双精度浮点数,... |
PyNumber_Absolute, fabs, fabsf, ... |
callable(obj) |
bint |
PyObject_Callable |
delattr(obj, name) |
无 |
PyObject_DelAttr |
exec(code, [glob, [loc]]) |
对象 |
|
dir(obj) |
列表 |
PyObject_Dir |
divmod(a, b) |
元组 |
PyNumber_Divmod |
getattr(obj, name, [default]) (注意 1) |
对象 |
PyObject_GetAttr |
hasattr(obj, name) |
bint |
PyObject_HasAttr |
hash(obj) |
整数 / 长整数 |
PyObject_Hash |
intern(obj) |
对象 |
Py*_InternFromString |
isinstance(obj, type) |
bint |
PyObject_IsInstance |
issubclass(obj, type) |
bint |
PyObject_IsSubclass |
iter(obj, [sentinel]) |
对象 |
PyObject_GetIter |
len(obj) |
Py_ssize_t |
PyObject_Length |
pow(x, y, [z]) |
对象 |
PyNumber_Power |
reload(obj) |
对象 |
PyImport_ReloadModule |
repr(obj) |
对象 |
PyObject_Repr |
setattr(obj, name) |
空 |
PyObject_SetAttr |
注意 1: Pyrex 最初提供了一个函数 getattr3(obj, name, default)()
,对应于 Python 内置函数 getattr()
的三参数形式。Cython 仍然支持此函数,但使用已弃用,建议使用正常的内置函数,Cython 可以对两种形式进行优化。
运算符优先级¶
请记住,Python 和 C 之间在运算符优先级方面存在一些差异,Cython 使用 Python 优先级,而不是 C 优先级。
整数 for 循环¶
注意
此语法仅在 Cython 文件中受支持。请改用正常的 for-in-range() 循环。
Cython 识别常见的 Python for-in-range 整数循环模式
for i in range(n):
...
如果 i
被声明为 cdef
整数类型,它将被优化为纯 C 循环。此限制是必要的,否则由于目标架构上的潜在整数溢出,生成的代码将不正确。如果您担心循环没有被正确转换,请使用 cython 命令行 (-a
) 的注释功能来轻松查看生成的 C 代码。请参阅 自动范围转换
为了向后兼容 Pyrex,Cython 还支持更详细的 for 循环形式,您可能会在遗留代码中找到它
for i from 0 <= i < n:
...
或
for i from 0 <= i < n by s:
...
其中 s
是某个整数步长。
注意
此语法已弃用,不应在新的代码中使用。请改用正常的 Python for 循环。
关于 for-from 循环的一些注意事项
目标表达式必须是简单的变量名。
下限和上限之间的名称必须与目标名称相同。
迭代方向由关系确定。如果它们都来自集合 {
<
,<=
},则为向上;如果它们都来自集合 {>
,>=
},则为向下。(任何其他组合都是不允许的。)
与其他 Python 循环语句一样,break 和 continue 可以用于循环体,循环可以有 else 子句。
Cython 文件类型¶
Cython 中有三种文件类型
实现文件,带有
.py
或.pyx
后缀。定义文件,带有
.pxd
后缀。包含文件,带有
.pxi
后缀。
实现文件¶
实现文件,顾名思义,包含函数、类、扩展类型等的实现。此文件中支持几乎所有 Python 语法。大多数情况下,.py
文件可以重命名为 .pyx
文件,而无需更改任何代码,Cython 将保留 Python 行为。
Cython 可以编译 .py
和 .pyx
文件。如果只想使用 Python 语法,文件名并不重要,Cython 不会根据使用的后缀更改生成的代码。但是,如果想使用 Cython 语法,则必须使用 .pyx
文件。
除了 Python 语法之外,用户还可以利用 Cython 语法(例如 cdef
)来使用 C 变量,可以将函数声明为 cdef
或 cpdef
,并且可以使用 cimport
导入 C 定义。本页面和 Cython 文档的其他部分介绍了可在实现文件中使用的许多其他 Cython 功能。
如果相应的定义文件也定义了该类型,则对某些 扩展类型 的实现部分有一些限制。
注意
当编译 .pyx
文件时,Cython 首先检查是否存在相应的 .pxd
文件,并首先处理它。它就像 Cython .pyx
文件的头文件。您可以在其中放置其他 Cython 模块将使用的函数。这允许不同的 Cython 模块相互使用函数和类,而无需 Python 开销。要详细了解如何执行此操作,您可以查看 pxd 文件。
定义文件¶
定义文件用于声明各种内容。
可以进行任何 C 声明,它也可以是 C 变量或函数的声明,这些变量或函数是在 C/C++ 文件中实现的。这可以使用 cdef extern from
完成。有时,.pxd
文件用作将 C/C++ 头文件转换为 Cython 可以理解的语法的翻译。然后,这允许在实现文件中使用 cimport
直接使用 C/C++ 变量和函数。您可以在 与外部 C 代码交互 和 在 Cython 中使用 C++ 中了解更多信息。
它还可以包含扩展类型的定义部分以及外部库的函数声明。
它不能包含任何 C 或 Python 函数的实现,也不能包含任何 Python 类定义或任何可执行语句。当需要访问 cdef
属性和方法,或继承自本模块中定义的 cdef
类时,需要它。
注意
您不需要(也不应该)在声明文件中声明任何内容 public
以使其可供其他 Cython 模块使用;它在定义文件中的存在就足以做到这一点。只有在您想让某些内容可供外部 C 代码使用时,才需要公开声明。
include 语句和包含文件¶
警告
从历史上看,include
语句用于共享声明。请改用 在 Cython 模块之间共享声明。
Cython 源文件可以使用 include 语句包含来自其他文件的内容,例如,
include "spamstuff.pxi"
命名文件的內容会在该位置文本化地包含。包含的文件可以包含在 include 语句出现的上下文中有效的任何完整语句或声明,包括其他 include 语句。包含文件的内容应从缩进级别为零开始,并将被视为已缩进到包含该文件的 include 语句的级别。但是,include 语句不能在模块范围之外使用,例如在函数或类主体内部。
注意
在许多情况下,还有其他机制可用于将 Cython 代码拆分为单独的部分,这些机制可能更合适。请参阅 在 Cython 模块之间共享声明。
条件编译¶
Cython 源文件内提供了一些语言特性,用于条件编译和编译时常量。
注意
此功能已弃用,不应在新的代码中使用。它与 Python 语言非常不同,并且与 C 预处理器的工作方式也不同。用户经常误解它。有关当前弃用状态,请参阅 https://github.com/cython/cython/issues/4310。有关替代方案,请参阅 DEF/IF 的弃用。
注意
此功能的用例很少。具体来说,它不是调整代码以适应平台和环境的良好方法。为此,请使用运行时条件、条件 Python 导入或 C 编译时自适应。例如,请参阅 包含逐字 C 代码 或 解决命名冲突 - C 命名规范。
注意
Cython 目前不支持纯 Python 模式下的条件编译和编译时定义。就目前而言,这种情况不太可能改变。
编译时定义¶
可以使用 DEF 语句定义编译时常量
DEF FavouriteFood = u"spam"
DEF ArraySize = 42
DEF OtherArraySize = 2 * ArraySize + 17
DEF
的右侧必须是有效的编译时表达式。此类表达式由使用 DEF
语句定义的字面量值和名称组成,并使用任何 Python 表达式语法组合在一起。
注意
Cython 不打算将字面量编译时值 1:1 复制到生成的代码中。相反,这些值在内部表示并计算为普通 Python 值,并在需要序列化时使用 Python 的 repr()
。这意味着使用 DEF
定义的值可能会根据 Cython 解析和转换源代码的 Python 环境的计算规则而丢失精度或更改其类型。具体来说,使用 DEF
定义高精度浮点数常量可能不会得到预期的结果,并且可能在不同的 Python 版本中生成不同的 C 值。
以下编译时名称是预定义的,对应于 os.uname()
返回的值。如上所述,它们不被认为是调整代码以适应不同平台的良好方法,主要出于遗留原因提供。
UNAME_SYSNAME、UNAME_NODENAME、UNAME_RELEASE、UNAME_VERSION、UNAME_MACHINE
以下内置常量和函数选择也可用
None、True、False、abs、all、any、ascii、bin、bool、bytearray、bytes、chr、cmp、complex、dict、divmod、enumerate、filter、float、format、frozenset、hash、hex、int、len、list、long、map、max、min、oct、ord、pow、range、reduce、repr、reversed、round、set、slice、sorted、str、sum、tuple、xrange、zip
请注意,在 Python 2.x 或 3.x 下编译时,其中一些内置函数可能不可用,或者在两者中行为可能不同。
使用 DEF
定义的名称可以在任何标识符可以出现的地方使用,并且它将被替换为其编译时值,就好像它在该点作为字面量写入源代码中一样。为了使这起作用,编译时表达式必须计算为类型为 int
、long
、float
、bytes
或 unicode
(str
在 Py3 中) 的 Python 值。
DEF FavouriteFood = u"spam"
DEF ArraySize = 42
DEF OtherArraySize = 2 * ArraySize + 17
cdef int[ArraySize] a1
cdef int[OtherArraySize] a2
print("I like", FavouriteFood)
条件语句¶
IF
语句可用于在编译时有条件地包含或排除代码部分。它的工作方式类似于 C 中的 #if
预处理器指令。
IF ARRAY_SIZE > 64:
include "large_arrays.pxi"
ELIF ARRAY_SIZE > 16:
include "medium_arrays.pxi"
ELSE:
include "small_arrays.pxi"
ELIF
和 ELSE
子句是可选的。IF
语句可以出现在任何正常语句或声明可以出现的地方,并且它可以包含在该上下文中有效的任何语句或声明,包括 DEF
语句和其他 IF
语句。
IF
和 ELIF
子句中的表达式必须是有效的编译时表达式,如 DEF
语句一样,尽管它们可以计算为任何 Python 值,并且结果的真值将以通常的 Python 方式确定。