纯 Python 模式¶
在某些情况下,希望加速 Python 代码,但又不失去使用 Python 解释器运行它的能力。虽然可以使用 Cython 编译纯 Python 脚本,但通常只会带来大约 20%-50% 的速度提升。
为了超越这一点,Cython 提供了语言结构,可以将静态类型和 Cython 功能添加到 Python 模块中,使其在编译后运行得更快,同时仍然允许解释它。这是通过一个增强的 .pxd
文件、通过 Python 类型 PEP-484 类型注释(遵循 PEP 484 和 PEP 526),以及/或者通过导入魔法 cython
模块后可用的特殊函数和装饰器来实现。所有这三种方法都可以根据需要组合使用,尽管项目通常会决定使用一种特定的方法来使静态类型信息易于管理。
虽然通常不建议在 .pyx
文件中直接编写 Cython 代码,但这样做有正当理由 - 更容易测试和调试、与纯 Python 开发人员协作等。在纯模式下,您或多或少地限制在可以使用 Python 表达(或至少模拟)的代码,加上静态类型声明。除此之外的任何内容只能在具有扩展语言语法的 .pyx 文件中完成,因为它依赖于 Cython 编译器的功能。
增强 .pxd¶
使用增强的 .pxd
允许原始的 .py
文件完全保持不变。另一方面,需要维护 .pxd
和 .py
,以保持它们同步。
虽然 .pyx
文件中的声明必须与具有相同名称的 .pxd
文件中的声明完全一致(任何矛盾都会导致编译时错误,请参见 pxd 文件),但 .py
文件中未类型化的定义可以被 .pxd
文件中更具体的静态类型定义覆盖和增强。
如果找到一个与要编译的 .py
文件具有相同名称的 .pxd
文件,则将搜索该文件以查找 cdef
类和 cdef
/cpdef
函数和方法。然后,编译器将转换 .py
文件中相应的类/函数/方法,使其具有声明的类型。因此,如果有一个文件 A.py
def myfunction(x, y=2):
a = x - y
return a + x * y
def _helper(a):
return a + 1
class A:
def __init__(self, b=0):
self.a = 3
self.b = b
def foo(self, x):
print(x + _helper(1.0))
并添加 A.pxd
cpdef int myfunction(int x, int y=*)
cdef double _helper(double a)
cdef class A:
cdef public int a, b
cpdef foo(self, double x)
那么 Cython 将编译 A.py
,就好像它是以下面这样编写的:
cpdef int myfunction(int x, int y=2):
a = x - y
return a + x * y
cdef double _helper(double a):
return a + 1
cdef class A:
cdef public int a, b
def __init__(self, b=0):
self.a = 3
self.b = b
cpdef foo(self, double x):
print(x + _helper(1.0))
请注意,为了向 .pxd
文件中的定义提供 Python 包装器,即,使其可以从 Python 访问,
Python 可见函数签名必须声明为 cpdef(使用默认参数替换为 * 以避免重复)
cpdef int myfunction(int x, int y=*)
内部函数的 C 函数签名可以声明为 cdef
cdef double _helper(double a)
cdef 类(扩展类型)声明为 cdef class;
cdef 类属性必须声明为 cdef public(如果需要读写 Python 访问),cdef readonly(只读 Python 访问),或纯 cdef(内部 C 级属性);
cdef 类方法必须声明为 cpdef(用于 Python 可见方法)或 cdef(用于内部 C 方法)。
在上面的示例中,myfunction() 中局部变量 a 的类型没有固定,因此将是一个 Python 对象。要静态地对其进行类型化,可以使用 Cython 的 @cython.locals
装饰器(参见 魔法属性 和 .pxd 中的魔法属性)。
普通 Python (def
) 函数不能在 .pxd
文件中声明。因此,目前无法在 .pxd
文件中覆盖普通 Python 函数的类型,例如覆盖其局部变量的类型。在大多数情况下,将它们声明为 cpdef 将按预期工作。
魔法属性¶
魔法 cython
模块中提供了特殊的装饰器,可用于在 Python 文件中添加静态类型,同时被解释器忽略。
此选项将 cython
模块依赖项添加到原始代码中,但不需要维护辅助的 .pxd
文件。Cython 提供了此模块的伪版本,即 Cython.Shadow,它在安装 Cython 时可用作 cython.py,但可以在未安装 Cython 时复制以供其他模块使用。
“编译”开关¶
compiled
是一个特殊变量,在编译器运行时设置为True
,在解释器中设置为False
。因此,代码import cython if cython.compiled: print("Yep, I'm compiled.") else: print("Just a lowly interpreted script.")
的行为将根据代码是作为编译扩展 (
.so
/.pyd
) 模块还是普通.py
文件执行而有所不同。
静态类型¶
cython.declare
在当前作用域中声明一个类型化的变量,它可以代替cdef type var [= value]
结构。它有两种形式,第一种是赋值(很有用,因为它在解释模式下也创建声明)import cython x = cython.declare(cython.int) # cdef int x y = cython.declare(cython.double, 0.57721) # cdef double y = 0.57721
第二种形式是简单的函数调用
import cython cython.declare(x=cython.int, y=cython.double) # cdef int x; cdef double y
它还可以用于定义扩展类型的私有、只读和公共属性
import cython @cython.cclass class A: cython.declare(a=cython.int, b=cython.int) c = cython.declare(cython.int, visibility='public') d = cython.declare(cython.int) # private by default. e = cython.declare(cython.int, visibility='readonly') def __init__(self, a, b, c, d=5, e=3): self.a = a self.b = b self.c = c self.d = d self.e = e
@cython.locals
是一个装饰器,用于指定函数体(包括参数)中局部变量的类型import cython @cython.locals(a=cython.long, b=cython.long, n=cython.longlong) def foo(a, b, x, y): n = a * b # ...
@cython.returns(<type>)
指定函数的返回值类型。@cython.exceptval(value=None, *, check=False)
指定函数的异常返回值和异常检查语义,如下所示@exceptval(-1) # cdef int func() except -1: @exceptval(-1, check=False) # cdef int func() except -1: @exceptval(check=True) # cdef int func() except *: @exceptval(-1, check=True) # cdef int func() except? -1: @exceptval(check=False) # no exception checking/propagation
如果禁用异常传播,则在函数内部引发的任何 Python 异常都将被打印并忽略。
C 类型¶
Cython 模块内置了多种类型。它提供了所有标准的 C 类型,包括 char
、short
、int
、long
、longlong
以及它们的无符号版本 uchar
、ushort
、uint
、ulong
、ulonglong
。特殊的 bint
类型用于 C 布尔值,而 Py_ssize_t
用于 Python 容器的(带符号)大小。
对于每种类型,都有指针类型 p_int
、pp_int
等,在解释模式下最多可达三层深度,在编译模式下则无限深度。可以使用 cython.pointer(cython.int)
构造更深层的指针类型,并使用 cython.int[10]
构造数组。对这些更复杂的类型进行了有限的模拟尝试,但从 Python 语言中只能做到这么多。
Python 类型 int、long 和 bool 分别被解释为 C int
、long
和 bint
。此外,还可以使用 Python 内置类型 list
、dict
、tuple
等,以及任何用户定义的类型。
可以将类型化的 C 元组声明为 C 类型的元组。
扩展类型和 cdef 函数¶
类装饰器
@cython.cclass
创建一个cdef class
。函数/方法装饰器
@cython.cfunc
创建一个cdef
函数。@cython.ccall
创建一个cpdef
函数,即 Cython 代码可以在 C 级调用该函数。@cython.locals
声明局部变量(见上文)。它还可以用于声明参数的类型,即签名中使用的局部变量。@cython.inline
等同于 C 的inline
修饰符。@cython.final
通过阻止将类型用作基类或在子类型中覆盖方法来终止继承链。这可以实现某些优化,例如内联方法调用。
以下是一个 cdef
函数的示例
@cython.cfunc
@cython.returns(cython.bint)
@cython.locals(a=cython.int, b=cython.int)
def c_compare(a,b):
return a == b
管理全局解释器锁¶
cython.nogil
可用作上下文管理器或装饰器来替换nogil
关键字with cython.nogil: # code block with the GIL released @cython.nogil @cython.cfunc def func_released_gil() -> cython.int: # function that can be run with the GIL released
请注意,这两种用法有所不同:上下文管理器释放 GIL,而装饰器标记函数可以在没有 GIL 的情况下运行。有关更多详细信息,请参见 <cython_and_gil>。
cython.gil
可用作上下文管理器来替换gil
关键字with cython.gil: # code block with the GIL acquired
注意
Cython 目前不支持
@cython.with_gil
装饰器。
这两个指令都接受一个可选的布尔参数,用于有条件地释放或获取 GIL。条件必须是常量(在编译时)。
with cython.nogil(False):
# code block with the GIL not released
@cython.nogil(True)
@cython.cfunc
def func_released_gil() -> cython.int:
# function with the GIL released
with cython.gil(False):
# code block with the GIL not acquired
with cython.gil(True):
# code block with the GIL acquired
有条件地获取和释放 GIL 的一个常见用例是融合类型,它们允许根据特定类型进行不同的 GIL 处理(参见 有条件地获取/释放 GIL)。
cimports¶
特殊的 cython.cimports
包名允许在使用 Python 语法的代码中访问 cimports。请注意,这并不意味着 C 库对 Python 代码可用。它只意味着你可以告诉 Cython 你想要使用哪些 cimports,而不需要特殊的语法。在纯 Python 中运行此类代码将失败。
from cython.cimports.libc import math
def use_libc_math():
return math.ceil(5.5)
由于此类代码必须引用不存在的 cython.cimports
‘包’,因此无法使用纯 cimport 形式 cimport cython.cimports...
。你必须使用 from cython.cimports...
形式。
更多 Cython 函数和声明¶
address
用于代替&
运算符cython.declare(x=cython.int, x_ptr=cython.p_int) x_ptr = cython.address(x)
sizeof
模拟 sizeof 运算符。它可以接受类型和表达式。cython.declare(n=cython.longlong) print(cython.sizeof(cython.longlong)) print(cython.sizeof(n))
typeof
返回参数类型的字符串表示形式,用于调试目的。它可以接受表达式。cython.declare(n=cython.longlong) print(cython.typeof(n))
struct
可用于创建结构类型。MyStruct = cython.struct(x=cython.int, y=cython.int, data=cython.double) a = cython.declare(MyStruct)
等效于代码
cdef struct MyStruct: int x int y double data cdef MyStruct a
union
使用与struct
完全相同的语法创建联合类型。typedef
在给定名称下定义类型T = cython.typedef(cython.p_int) # ctypedef int* T
cast
将(不安全地)重新解释表达式的类型。cython.cast(T, t)
等效于<T>t
。第一个属性必须是类型,第二个是要转换的表达式。指定可选的关键字参数typecheck=True
具有<T?>t
的语义。t1 = cython.cast(T, t) t2 = cython.cast(T, t, typecheck=True)
fused_type
创建一个新的类型定义,它引用多个类型。以下示例声明了一个名为my_fused_type
的新类型,它可以是int
或double
。my_fused_type = cython.fused_type(cython.int, cython.float)
.pxd 中的魔法属性¶
特殊的 cython 模块也可以在增强 .pxd
文件中导入和使用。例如,以下 Python 文件 dostuff.py
def dostuff(n):
t = 0
for i in range(n):
t += i
return t
可以使用以下 .pxd
文件 dostuff.pxd
增强
import cython
@cython.locals(t=cython.int, i=cython.int)
cpdef int dostuff(int n)
cython.declare()
函数可用于在增强 .pxd
文件中为全局变量指定类型。
PEP-484 类型注解¶
Python 类型提示 可用于声明参数类型,如以下示例所示
import cython def func(foo: dict, bar: cython.int) -> tuple: foo["hello world"] = 3 + bar return foo, 5
请注意使用 cython.int
而不是 int
- Cython 默认情况下不会将 int
注解转换为 C 整数,因为在溢出和除法方面,其行为可能大不相同。
全局变量上的注解目前被忽略。这是因为我们预计注解类型的代码主要用于 Python,而全局类型注解会将 Python 变量转换为内部 C 变量,从而将其从模块字典中删除。要将全局变量声明为类型化的 C 变量,请使用 @cython.declare()
。
注解可以与 @cython.exceptval()
装饰器结合使用,用于非 Python 返回类型
import cython @cython.exceptval(-1) def func(x: cython.int) -> cython.int: if x < 0: raise ValueError("need integer >= 0") return x + 1
请注意,当返回 C 数值类型时,默认的异常处理行为是检查 -1
,如果返回了该值,则检查 Python 的错误指示器以获取异常。这意味着,如果没有提供 @exceptval
装饰器,并且返回类型是数值类型,那么带有类型注释的默认值为 @exceptval(-1, check=True)
,以确保异常能够正确且高效地报告给调用者。可以使用 @exceptval(check=False)
显式禁用异常传播,在这种情况下,函数内部引发的任何 Python 异常都将被打印并忽略。
从 0.27 版本开始,Cython 也支持 PEP 526 中定义的变量注释。这允许以与 Python 3.6 兼容的方式声明变量类型,如下所示
import cython
def func():
# Cython types are evaluated as for cdef declarations
x: cython.int # cdef int x
y: cython.double = 0.57721 # cdef double y = 0.57721
z: cython.float = 0.57721 # cdef float z = 0.57721
# Python types shadow Cython types for compatibility reasons
a: float = 0.54321 # cdef double a = 0.54321
b: int = 5 # cdef object b = 5
c: long = 6 # cdef object c = 6
pass
@cython.cclass
class A:
a: cython.int
b: cython.int
def __init__(self, b=0):
self.a = 3
self.b = b
目前无法表达对象属性的可见性。
禁用注释¶
为了避免与其他类型的注释使用冲突,可以使用 annotation_typing
编译器指令 禁用 Cython 使用注释来指定类型。从 Cython 3 开始,您可以将其用作装饰器或 with 语句,如以下示例所示
import cython
@cython.annotation_typing(False)
def function_without_typing(a: int, b: int) -> int:
"""Cython is ignoring annotations in this function"""
c: int = a + b
return c * a
@cython.annotation_typing(False)
@cython.cclass
class NotAnnotatedClass:
"""Cython is ignoring annotatons in this class except annotated_method"""
d: dict
def __init__(self, dictionary: dict):
self.d = dictionary
@cython.annotation_typing(True)
def annotated_method(self, key: str, a: cython.int, b: cython.int):
prefixed_key: str = 'prefix_' + key
self.d[prefixed_key] = a + b
def annotated_function(a: cython.int, b: cython.int):
s: cython.int = a + b
with cython.annotation_typing(False):
# Cython is ignoring annotations within this code block
c: list = []
c.append(a)
c.append(b)
c.append(s)
return c
typing
模块¶
对 PEP-484 描述的完整范围的注释的支持尚不完整。Cython 3 目前理解 typing
模块中的以下功能
Optional[tp]
,它被解释为tp or None
;类型化容器,例如
List[str]
,它被解释为list
。元素类型为str
的提示目前被忽略;Tuple[...]
,它被转换为 Cython C 元组(如果可能)或常规 Pythontuple
(否则)。ClassVar[...]
,它在cdef class
或@cython.cclass
的上下文中被理解。
一些不受支持的功能可能会继续不受支持,因为这些类型提示与编译为高效 C 代码无关。但是,在其他情况下,如果生成的 C 代码可以从这些类型提示中受益,但目前还没有,欢迎您帮助改进 Cython 中的类型分析。
参考表¶
以下参考表记录了类型注释的当前解释方式。仅在 Cython 0.29 的行为与 Cython 3.0 的行为不同时才显示 Cython 0.29 的行为。当前的限制可能会在某个时候被解除。
功能 |
Cython 0.29 |
Cython 3.0 |
---|---|---|
|
任何 Python 对象 |
精确的 Python |
|
C |
|
内置类型,例如 |
精确类型(没有子类),不是 |
|
在 Cython 中定义的扩展类型 |
指定的类型或子类,不是 |
|
|
等效的 C 数值类型 |
|
|
不支持 |
指定类型(必须是 Python 对象)允许 |
|
不支持 |
精确的 |
|
不支持 |
Python 对象类变量(在类定义中使用时) |
技巧和窍门¶
调用 C 函数¶
通常,在纯 Python 模式下无法调用 C 函数,因为在普通(未编译)Python 中没有通用的支持方式。但是,在存在等效 Python 函数的情况下,可以通过将 C 函数强制转换与条件导入相结合来实现,如下所示
# mymodule.pxd
# declare a C function as "cpdef" to export it to the module
cdef extern from "math.h":
cpdef double sin(double x)
# mymodule.py
import cython
# override with Python import if not in compiled code
if not cython.compiled:
from math import sin
# calls sin() from math.h when compiled with Cython and math.sin() in Python
print(sin(0))
请注意,“sin” 函数将显示在“mymodule” 的模块命名空间中(即,将存在一个 mymodule.sin()
函数)。您可以按照 Python 约定将其标记为内部名称,方法是在 .pxd
文件中将其重命名为“_sin”,如下所示
cdef extern from "math.h":
cpdef double _sin "sin" (double x)
然后,您还需要将 Python 导入更改为 from math import sin as _sin
,以使名称再次匹配。
使用 C 数组表示固定大小的列表¶
C 数组可以自动强制转换为 Python 列表或元组。这可以用来在编译时用 C 数组替换 Python 代码中的固定大小的 Python 列表。一个例子
import cython
@cython.locals(counts=cython.int[10], digit=cython.int)
def count_digits(digits):
"""
>>> digits = '01112222333334445667788899'
>>> count_digits(map(int, digits))
[1, 3, 4, 5, 3, 1, 2, 2, 3, 2]
"""
counts = [0] * 10
for digit in digits:
assert 0 <= digit <= 9
counts[digit] += 1
return counts
在普通 Python 中,这将使用 Python 列表来收集计数,而 Cython 将生成使用 C int 数组的 C 代码。