Skip to content

纯 Python 模式

原文: http://docs.cython.org/en/latest/src/tutorial/pure.html

在某些情况下,我们需要在不失去使用 Python 解释器运行能力的情况下,加速 Python 代码。虽然可以使用 Cython 编译纯 Python 脚本,但通常只能获得大约 20%-50%的速度增益。

为了超越这一点,Cython 提供了为 Python 模块添加静态类型和 cythonic 功能的语言结构,使其在编译时运行得更快,同时仍允许对其进行解释。这是通过增加.pxd文件,通过 Python 类型注释(在 PEP 484PEP 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
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; (默认参数替换为 * 以避免重复)

    ```py cpdef int myfunction(int x, int y=*)

    ```

  • 内部函数的 C 函数签名可以声明为cdef

    ```py cdef double _helper(double a)

    ```

  • cdef类(扩展类型)声明为 cdef 类

  • 如果需要提供读写的Python访问,cdef类属性必须声明为cdef public; 用于只读 Python 访问, 则cdef readonly;或用于内部 C 级属性, 直接写cdef

  • 对于Python可见的方法的cdef 类方法必须声明为 cpdef, 如果是用于内部 C 方法, cdef即可

在上面的例子中, myfunction() 中局部变量的类型不固定,因此是一个 Python 对象。如果要给它添加静态类型,可以使用 Cython 的@cython.locals装饰器(参见 魔法属性魔法属性.pxd) 。

普通 Python( def )函数不能在.pxd文件中声明。因此,目前不可能在.pxd文件中覆盖普通 Python 函数的类型,例如覆盖其局部变量的类型。在大多数情况下,将它们声明为 cpdef 将按预期工作。

魔法属性

cython模块提供了特殊装饰器,可用于在 Python 文件中添加静态类型,同时被解释器忽略。

此选项将cython模块依赖项添加到原始代码,但不需要维护补充.pxd文件。 Cython 提供了这个模块的伪造版本 Cython.Shadow,当安装 Cython 时可以以 cython.py 的形式提供,但是当 Cython 没有安装时,也可以直接复制过来供其他模块使用。

“compiled” 编译开关

  • compiled是一个特殊变量,在编译器运行时为True,在解释器中为False。因此,代码

    ```py 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]构造。这有两种形式,第一种作为赋值(在解释模式中创建声明时很有用):

    ```py import cython

    x = cython.declare(cython.int) # cdef int x y = cython.declare(cython.double, 0.57721) # cdef double y = 0.57721

    ```

    和第二种模式作为一个简单的函数调用:

    ```py import cython

    cython.declare(x=cython.int, y=cython.double) # cdef int x; cdef double y

    ```

    它还可以用于定义扩展类型的 private,readonly 和 public 属性:

    ```py 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是一个装饰器,用于指定函数体中局部变量的类型(包括参数):

    ```py 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)指定函数的异常返回值和异常检查语义,如下所示:

    ```py @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:

    ``` 当异常传播机制被禁止时, 函数内部引发的任何Python异常都会被打印出来并忽略。

C 类型

Cython 模块内置了许多类型。它提供所有标准 C 类型,即charshortintlonglonglong以及它们的无符号版本ucharushortuintulongulonglong。特殊的bint类型用于 C 布尔值,Py_ssize_t用于表示 Python 容器的 带符号大小。

对于每种类型,都有指针类型p_intpp_int等,在解释模式下最多三级,在编译模式下无限深。可以使用cython.pointer(cython.int)构建更多指针类型,将数组构造为cython.int[10]。针对这些复杂类型的模拟我们进行了有限的尝试,在Python语言下只能做到这么多。

Python 类型 int,long 和 bool 分别被解释为 C intlongbint。此外,可以使用 Python 内置类型listdicttuple等,以及任何用户定义的类型。

键入的 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

管理全局解释器锁(GIL)

  • cython.nogil 可以被用做一个上下文管理器或者是一个装饰器用来取代 nogil 这个关键字:
with cython.nogil:
    # code block with the GIL released
@cython.nogil
@cython.cfunc
def func_released_gil() -> cython.list:
    # function that can be run with the GIL released

请注意,这两种用法是不同的:上下文管理释放GIL,装饰器只是标记这个函数可以在没有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语法的情况下提供引入Cython代码的入口。注意,这并不代表C语言库因此对Python代码变得可用了。这只代表你可以在不使用特殊语法的情况下告诉 Cython 你需要使用哪些 cimports 库。在普通的Python(没安装Cython)下运行这些代码会失败。

from cython.cimports.libc import math

def use_libc_math():
    return math.ceil(5.5)

进一步的 Cython 函数和声明

  • address用于代替&运算符:

    ```py cython.declare(x=cython.int, x_ptr=cython.p_int) x_ptr = cython.address(x)

    ```

  • sizeof 模拟 sizeof 运算符。它可以接受类型或者是表达式:

    ```py cython.declare(n=cython.longlong) print(cython.sizeof(cython.longlong)) print(cython.sizeof(n))

    ```

  • typeof 出于Debug的目的,可以返回一个参数类型的字符串表达式。它可以接受表达式:

    py cython.declare(n=cython.longlong) print(cython.typeof(n))

  • struct可用于创建结构类型:

    ```py MyStruct = cython.struct(x=cython.int, y=cython.int, data=cython.double) a = cython.declare(MyStruct)

    ```

    相当于代码:

    ```py cdef struct MyStruct: int x int y double data

    cdef MyStruct a

    ```

  • union使用与struct完全相同的语法创建联合类型。

  • typedef可以给类型设置一个指定的别名:

    ```py T = cython.typedef(cython.p_int) # ctypedef int* T

    ```

  • cast将(不安全地)重新解释表达式类型。 cython.cast(T, t)相当于<T>t。第一个属性必须是类型,第二个属性是要转换的表达式。指定可选关键字参数typecheck=True具有<T?>t的语义。

    ```py t1 = cython.cast(T, t) t2 = cython.cast(T, t, typecheck=True)

    ```

  • fused_type 创建指向多种类型的类型定义。下面这个例子声明一个新的类型my_fused_type,可以代表int或者double:

    py 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类型的注解翻译为cython.int, 因为涉及到溢出和除法的情况下两者表现相当不同。

对于 非Python 的返回类型,类型注解可以和 @cython.exceptval() 装饰器结合使用。

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 ,如果收到了 -1 , 会调用Python错误指示器检查是否出现异常。这也就是说,如果没有提供@exceptval装饰器,且返回值是数字类型时,类型注解会隐式的带一个 @exceptval(-1,check=True), 以确保异常被正确、有效的报告给调用者。使用 @exceptval(check=False) 异常传播机制可以被显式得禁用,也就是说任何函数内部引发的Python异常都只会被打印出来并忽略。

自从0.27版本起,Cython增加了对定义在PEP-526内的变量注解的支持。这允许我们用下面这样方式声明变量类型(兼容Python3.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

目前,还没有办法表达对象属性的可见性(即private, local, global)。

禁用注解

为了避免和其他类型的注解使用方式发生冲突,可以使用一个编译器指令 annotation_typing 来禁止Cython使用注解声明类型。从Cython3版本起,你可以把这个当成上下文管理或者是装饰器使用,像下面的例子展示的:

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到目前为止,支持了typing模块的下面特性:

  • Optional[tp], 被解释为tp or None;
  • 带类型的容器,比如 List[str], 会被解释为List,关于容器内容的类型提示目前被忽略;
  • Tuple[...],如果可能,将会被转换为一个Cython的 C-tuple,否则就是一个普通的Python元组。
  • ClassVar[...], 被解释为cdef class@cython.cclass

一些没有被支持的特性很可能维持不被支持的现状,因为这些类型提示对于编译高效的C语言代码没有帮助。如果不是这样的话,如果生成的C语言代码可以从这些类型注解中获益、且目前没有被支持,我们欢迎任何能够提升Cython类型分析的帮助。

引用表格

下面这个引用表格记录了类型注解是如何被翻译的。Cython 0.29的行为只在被明确标注的情况下和Cython3.0不同。下面这些限制在未来可能会被解除。

特性 Cython 0.29 Cython 3.0
int Any Python Object Python int (不含子类,且仅当language_level=3)
float C double
内置类型,比如dict,list 一样的类型(不含子类), 不是None
Cython定义的扩展类 指定类型或者子类,不是None
cython.int,cython等等 对应的C语言数字类型
typing.Optional[任何类型] 不支持 指定的类型,必须是Python对象,允许是None
typing.List[任何类型] 不支持 list类型(不含子类),内容物类型忽视
typiing.ClassVar[...] 不支持 代表类变量的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 数组的 C 代码。



回到顶部