Skip to content

语言基础

原文: http://docs.cython.org/en/latest/src/userguide/language_basics.html

声明数据类型

作为一种动态语言,Python 鼓励一种编程风格,即在方法和属性方面考虑类和对象,而不是考虑它们适合的类层次结构。

这可以使 Python 成为一种非常轻松和舒适的快速开发语言,但需要付出代价 - 管理数据类型的“繁文缛节”会被转储到解释器上。在运行时,解释器在搜索命名空间,获取属性以及解析参数和关键字元组方面做了大量工作。与“早期绑定”语言(如 C ++)相比,这种运行时“后期绑定”是 Python 相对缓慢的主要原因。

然而,使用 Cython,通过使用“早期绑定”编程技术可以获得显着的加速。

注意:限定类型不是必需的

为参数和变量提供静态类型可以方便地加速代码,但这不是必需的。选择恰当的位置和时机去优化。实际上,在注明类型无法提供优化的地方注明类型反而会拖慢你代码的速度,仅在Cython需要检查对象是否满足对应的类型地方注明即可。

C 变量和类型定义

C 变量可以通过以下方式定义: - 使用Cython特有的 cdef 表述 - 使用 PEP-484/526 类型注释来标明C数据类型 或者 - 使用 cython.declare() 函数

cdef 语句和 declare() 都可以用于声明局部变量、模块级变量以及类属性。但是类型注释之影响局部变量和属性而忽略模块级。这主要是因为类型注释并不是Cython特有的,所以 Cython 把这些变量保存在一个 模块字典里(本身是Python对象)而不是把它们作为模块的内部C变量。在Python代码中使用 declare() 可以显式的声明全局C变量。

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的编译器都会发布一个警告 ”本地变量 ... 引用了未赋值的变量“ -- ”local variable … referenced before assignment“。 你需要在第一次使用变量前的某个地方赋值,但是在大部分场景下,你都可以在声明的时候就直接赋值。

cdef int a_global_variable

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

注意: 除此之外,还支持用 ctypedef 表达式或者是 cython.typedef() 函数来给类型设置别名。 py ctypedef unsigned long ULong ctypedef int* IntPtr

C数组

通过给变量增加 [数组大小] 可以声明C数组。

def func():
    cdef float[42] g
    cdef int[5][5][5] f

注意:Cython语法目前支持两种方法声明一个数组

py cdef int arr1[4], arr2[4] # C语言风格的数组声明 cdef int[4] arr1, arr2 # Java风格的数组声明

两者生成同样的C语言代码,但是Java语言的风格要与 类型化的内存视图融合类型 更加贴合。C语言风格的声明有点过时了,因此作为替代,更建议使用Java风格的声明语句。 过期的C语言风格声明语句不支持初始化时赋值

py cdef int g[4] = [1, 2, 3, 4] # 错误 cdef int[4] g = [1, 2, 3, 4] # 允许 cdef int g[4] # 可以,但是不推荐 g = [1, 2, 3, 4]

结构(Struct)、共用体(Union)、枚举类(enum)

除了基本类型,C语言的 struct , unionenum 也都支持。

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语言给成员之间自动设置的空格来保证它们在内存中是有序排列的(更多细节可以查看wiki 文档). 主要用途在于numpy 结构化的数组将数据以紧凑对其(packed)的形式存储,所以一个 cdef packed struct 可以在内存视图中和它匹配。 纯Python风格的写法不支持紧凑对其的结构。

下面案例演示了共用体的声明。

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 Issue #4252

将枚举声明为cpdef将创建 PEP 435 风格的 Python 包装器:

cpdef enum CheeseState:
    hard = 1
    soft = 2
    runny = 3

目前没有用于定义常量的特殊语法,但您可以使用匿名 enum 声明来实现此目的,例如:

cdef enum:
    tons_of_spam = 3

Note: 在Cython中,structunionenum 关键字仅在定义类型时使用,而不是在引用时使用。例如,要声明一个指向Grail的变量,您将编写:

py cdef Grail *gp

并不是:

py cdef struct Grail *gp # WRONG

类型

Cython 对于C语言类型使用普通的C语言语法,包括指针。它提供所有标准C类型,即charshortintlonglong long以及它们的unsigned版本,例如unsigned int。特殊bint类型用于 C 布尔值(int,其中 0 /非 0 值为 False / True)和Py_ssize_t用于 Python容器的 带符号大小。

在Cython语法中,可以通过将*附加到它们指向的基本类型来在 C 中构造指针类型。比如 int**指向指向 C int 的指针的指针。纯Python风格下,普通的指针类型使用带多个字符"p"并与类型中间用下划线分割的命名风格,比如 cython.pp_int 代表C整形指针的指针。更多的指针类型可以通过 cython.pointer() 函数来构造,比如 cython.pointer(cython.int).

数组使用普通的 C 数组语法,例如, int[10],并且在编译时必须知道堆栈分配的数组的大小。 Cython 不支持 C99 的可变长度数组。请注意,Cython 使用数组访问进行指针解除引用,因为*x不是有效的 Python 语法,而x[0]是。

此外,Python 类型listdicttuple等可用于静态类型,以及任何用户定义的 扩展类型 。例如:

cdef list foo = []

这里需要精确匹配类型(list类型),不允许使用子类。这允许 Cython 通过访问内置类的内部来优化代码,这是首先声明内置类型的主要原因。

对于声明的内置类型,Cython使用带C内部变量的 PyObject* 类型。

注意:Python 类型 int,long 和 float 不能用于静态类型,而是分别解释为 C intlongfloat,因为使用这些 Python 类型的静态类型变量没有任何优势。另一方面,在纯Python风格下,类型标注为 int , long , 和 float Python类型会被解释为对应的Python对象。

Cython 提供了一个加速和类型化的 Python 元组的等效版本,即ctuplectuple由任何有效的 C 类型组装而成。例如:

cdef (double, int) bar

它们编译成 C 结构,可以用作 Python 元组的有效替代品。

虽然这些 C 类型可以非常快,但它们具有 C 语义。具体来说,整数类型溢出,C float类型只有 32 位精度(而不是 Python 浮动包装的 64 位 C double,而且通常是你想要的)。如果要使用这些数字 Python 类型,只需省略类型声明并将它们作为对象。

类型修饰符

Cython 支持 constvolatileC语言类型修饰符.

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风格下还不支持。不仅如此,由于Cython需要为定义和赋值分别生成定义,const 修饰符在许多场景下都不可用。因此,我们建议把它用必须在函数参数和指针类型上增加 const 的场景(为了使用某些现有的C/C ++接口)。

扩展类型

也可以声明 扩展类型 (用cdef class 或者 @cclass装饰器声明)。它们的行为和Python类非常相似(比如创建子类),但是在Cython代码中访问成员的速度要快得多。把变量类型设置为扩展类型主要是用来访问 这些扩展类型的 cdef/@cfunc 方法和属性。C 代码使用一个变量,它是指向特定类型结构的指针,类似于struct MyExtensionTypeObject*

这里是一个简单的例子。

from __future__ import print_function
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 的语法下支持。

from __future__ import print_function

cdef:
    struct Spam:
        int tons

    int i
    float a
    Spam *p

    void f(Spam *s):
        print(s.tons, "Tons of spam")

Python 函数与 C 函数

Cython 中有两种函数定义:

Python 函数是使用 def 语句定义的,就像在 Python 中一样。它们将 Python 对象作为参数并返回 Python 对象。

C 函数使用新的 cdef 语句定义。它们将 Python 对象或 C 值作为参数,并且可以返回 Python 对象或 C 值。

在 Cython 模块中,Python 函数和 C 函数可以自由地相互调用,但只有Python函数能被外部Python解释器解释的代码调用。因此,您希望从 Cython 模块“导出”的任何函数都必须使用 def 声明为 Python 函数。还有一种混合函数,可以通过在.pyx文件用cpdef声明 或者是使用@ccall装饰器来获得。这些函数在可以从任何地方被调用,但在从其他 Cython 代码调用时会使用更快的 C 调用约定。 这些混合函数也可以在子类或实例属性上被 Python 方法覆盖,即使从 Cython 调用也是如此。如果发生这种情况,大多数性能提升当然都会丢失,即使这些性能提升没有丢失,与调用C语言方法相比,从 Cython 调用被覆盖的方法也会只会有一个微小的额外开销。

无论哪种类型的函数都可以用C语言语法来声明使用C数据类型。例如,:

def spam(int i, char *s):
    ...

cdef int eggs(unsigned long l, float f):
    ...

也可以使用ctuples

cdef (int, float) chips((long, long, double) t):
    ...

当 Python 函数的参数声明为具有 C 数据类型时,它将作为 Python 对象传入,并在可能的情况下自动转换为 C 值。换句话说,上面spam的定义等同于写作:

def spam(python_i, python_s):
    cdef int i = python_i
    cdef char* s = python_s
    ...

目前,只能对数字类型,字符串类型和结构(以递归方式组合任何这些类型)进行自动转换; 尝试将任何其他类型用于 Python 函数的参数将导致编译时错误。如果要在调用后使用指针,必须小心使用字符串以确保引用。可以从 Python 映射中获取结构,如果要在函数返回后使用它们,则必须再次使用字符串属性。

另一方面,C 函数可以具有任何类型的参数,因为它们是使用普通的 C 函数调用直接传递的。

使用 cdef 或者 @cfunc 装饰器、且声明了返回一个Python对象的函数,像Python 函数一样,将在执行离开函数体时如果没有显式返回值就返回None值。这与 C / C ++不同,后者保留返回值处于未定义的状态。在返回类型为非Python对象的情况下,返回等效于零的值,例如,对于int为 0,对于bintFalse,对于指针类型为NULL

可以在 早期结合速度 中找到这些不同方法类型的优缺点的更完整比较。

Python 对象作为参数和返回值

如果没有为参数或返回值指定类型,则假定它是 Python 对象。 (请注意,这与 C 约定不同,它默认为 int。)例如,下面定义了一个 C 函数,它将两个 Python 对象作为参数并返回一个 Python 对象:

cdef spamobjs(x, y):
    ...

根据标准 Python / C API 规则自动执行这些对象的引用计数(即,借用的引用被视为参数并返回新引用)。

警告

这仅适用于 Cython 代码。在 C 中实现的其他 Python 包(如 NumPy)可能不遵循这些约定。

类型名称对象 object 也可以用来显式声明某些内容为 Python 对象。如果声明的名称将被视为类型的名称时,这可能很有用,例如:

cdef ftang(object int):
    ...

声明一个名为 int 的参数,它是一个 Python 对象。您还可以使用 object 作为函数的显式返回类型,例如:

cdef object ftang(object int):
    ...

为了清楚起见,始终明确 C 函数中的对象参数可能是个好主意。

要创建借用引用,请将参数类型指定为PyObject*。 Cython 不会执行自动Py_INCREFPy_DECREF,例如:

from __future__ import print_function

from cpython.ref cimport PyObject

import sys

python_dict = {"abc": 123}
python_dict_refcount = sys.getrefcount(python_dict)


cdef owned_reference(object obj):
    refcount = sys.getrefcount(python_dict)
    print('Inside owned_reference: {refcount}'.format(refcount=refcount))


cdef borrowed_reference(PyObject * obj):
    refcount = obj.ob_refcnt
    print('Inside borrowed_reference: {refcount}'.format(refcount=refcount))


print('Initial refcount: {refcount}'.format(refcount=python_dict_refcount))
owned_reference(python_dict)
borrowed_reference(<PyObject *>python_dict)

将显示:

Initial refcount: 2
Inside owned_reference: 3
Inside borrowed_reference: 2

可选参数

与 C 不同,可以在@ccallcpdef函数中使用可选参数。但是,在.pyx / .py文件还是相应的.pxd文件中声明它们存在差异。

为避免重复(以及潜在的未来不一致),默认参数值在声明中(在.pxd文件中)不可见,但仅在实现中(在.pyx文件中)。

.pyx / .py 文件中,签名与 Python 本身的签名相同:

from __future__ import print_function

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)

如上所示,cde参数不能作为位置参数传递,必须作为关键字参数传递。此外,ce必需关键字参数,因为它们没有默认值。

没有参数名称的单个"*"可用于终止位置参数列表:

def g(a, b, *, c, d):
    ...

# 我们至少要写的这么啰嗦才能允许调用
foo = g(4.0, "something", c=68, d="other")

如上所示,签名只需要两个位置参数,并且有两个必需的关键字参数。

函数指针

注意:纯Python风格下目前还不支持函数指针。Github Issue #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中声明的函数会自动转换为函数指针。

有关使用带有函数指针的错误返回值,请参见下面 错误返回值 这一节内容底部的注释。

错误返回值

在Python中(更确切的是CPython运行期间),发生在函数内部的异常会被提示给调用方并且通过定义的错误返回值沿着调用栈向上传播。对于返回Python对象的函数(也就是一个对象指针)错误返回值 就是简单的一个空(NULL)指针,所以任何定义了返回Python对象的函数都有明确定义的错误返回值。

尽管对应Python函数总是如此,定义为C函数或者 cpdef / @ccall的函数能返回任意的C类型,因此没有明确定义的错误返回值。默认情况下,Cython使用一个专属的返回值来表示一个非外部的 cpdef / @call函数中发生了异常。但是,Cython如何处理这些异常可以根据需要进行改变。

你可以用 cdef 声明一个带有异常返回值的函数。比如:

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

如果所有可能的返回值都是合法的,并且您不能完全为信号错误保留一个,则可以使用另一种形式的异常值声明:

cdef int spam() except? -1:
    ...

“?”意味着-1返回值可能代表一个错误。

在这种情况下,如果异常返回值被返回了,Cython会调用 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代码的中的回调函数,对Python异常完全无感知),你可以用 noexcept 或者 @cython.exceptval(check=False) 来声明。

cdef int spam() noexcept:
    ...

如果一个 noexcept 函数确实以异常状态结束,那么它会打印一段警告信息,但是不会允许异常进一步传播。从另一个角度来说,调用 noexcept 的函数在管理异常方面也没有任何额外开销,这点和之前的声明不同。

有些事情需要注意: * externcdef 函数隐式的被声明为 noexcept@cython.exceptval(check=False)。 如果在极端情况下,这些外部的C/C ++ 函数确实应该引发Python异常的话(比如使用Python C API的外部函数),你可以显式给它声明一个异常返回值。

  • 不是externcdef 函数隐式的被声明为带一个合适的、针对返回值类型的异常返回值(比如:except * 或者 @cython.exceptval(check=True))。对于 void 返回类型,则作为int类型返回 -- except? -1 或者 `@cython.exceptval(-1,check=True)

  • 异常值只能为返回整数,枚举,浮点或指针类型的函数声明,并且值必须是常量表达式。 返回 void, 结构/联合体值的函数,只能使用except * 或者 @exceptval(check=True) 的形式。

  • 异常值规范是函数签名的一部分。如果您将指向函数的指针作为参数传递或将其指定给变量,则声明的参数或变量类型必须具有相同的异常值规范(或缺少该规范)。以下是带有异常值的指针到函数声明的示例:

    py int (*grail)(int, char*) except -1

    注意:纯Python风格目前不支持函数指针。 Github Issue #4279

  • 如果一个带 except * 或者 @cython.exceptval(checkTrue)的用cdef` 声明的函数返回值类型为C整型、枚举、浮点值或者指针类型,Cython只会在返回了专属值(-1)的时候调用PyErr_Occurred,而不是每次函数调用结束都吊用。

  • 您不需要(也不应该)为返回 Python 对象的函数声明异常值。请记住,没有声明返回类型的函数会隐式返回 Python 对象。 (通过返回 NULL 隐式传播此类函数的异常。)

  • 当把 nogilexcept * @cython.exceptval(check=True) 结合使用时,这里有一个已知的性能陷阱。在这种情况下,Cython必须在函数调用后重新获得GIL,来检查是否引发了异常。这通常会在函数没有返回任何值(比如C void)时发生。一个简单的解决办法是当你确定函数不应该抛出异常时把函数标记为 noexcept,或者是把返回值设为 int 然后让Cython把返回值当作错误标识使用(比如默认情况下,-1 会触发异常检查)

检查非 Cython 函数的返回值

重要的是要理解,当返回指定的值时,except 子句不会引发错误。例如,你不能写像:

cdef extern FILE *fopen(char *filename, char *mode) except NULL # WRONG!

并且期望如果对fopen()的调用返回 NULL 自动引发异常。这里 except 子句不起作用; 它的唯一目的是传播已经引发的 Python 异常,无论是通过调用 Cython 函数或调用 Python / C API 例程的 C 函数的形式来传播。要从诸如fopen()的非 Python 感知函数中获取异常,您必须检查返回值并自行引发它,例如:

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方法可以覆盖cdef方法:

from __future__ import print_function

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方法(def)可以覆盖cpdef / @ccall方法但不能覆盖cdef方法:

from __future__ import print_function

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 编译器将发出警告。

自动类型转换

在大多数情况下,当在需要 C 值的上下文中使用 Python 对象时,将对基本数字和字符串类型执行自动转换,反之亦然。下表总结了转换的可能性。

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 flot
char* str / bytes str / bytes
C array iterable list
struct, Union dict
  • 对于 Python 2.x来说,约定是转换为/来自 str,对于 Python 3.x,则转换为字节。
  • 从 C union 类型到 Python dict 的转换将为每个 union 字段添加一个值。但是,Cython 0.23 及更高版本将拒绝自动转换具有不安全类型组合的联合。一个例子是intchar*的并集,在这种情况下,指针值可能是也可能不是有效指针。
  • 除了 signed / unsigned char []。如果在编译时未知 C 数组的长度,并且使用 C 数组的切片,则转换将失败。
  • 结构(struct)向 dict 的自动转换有一些潜在的陷阱(在这里详细描述了)

在 C 语境中使用 Python 字符串时的注意事项

在期望char*的上下文中使用 Python 字符串时需要小心。在这种情况下,使用指向 Python 字符串内容的指针,只有 Python 字符串存在时才有效。因此,只要需要 C 字符串,就需要确保保留对原始 Python 字符串的引用。如果您不能保证 Python 字符串的存活时间足够长,则需要复制 C 字符串。

Cython 检测并防止这种错误。例如,如果您尝试以下内容:

cdef char *s
s = pystring1 + pystring2

然后 Cython 将产生错误消息Storing unsafe C derivative of temporary Python reference。原因是连接两个 Python 字符串会产生一个新的 Python 字符串对象,该对象仅由 Cython 生成的临时内部变量引用。语句完成后,临时变量将被删除,Python 字符串被释放,s悬空。由于此代码无法工作,因此 Cython 拒绝编译它。

解决方案是将串联的结果赋给 Python 变量,然后从中获取char*,即:

cdef char *s
p = pystring1 + pystring2
s = p

然后,您有必要根据您的需要持有变量p的引用。

请记住,用于检测此类错误的规则仅是启发式。有时 Cython 会不必要地抱怨,有时它会无法检测到存在的问题。最终,您需要理解这里的问题并保持谨慎。

类型转换

C 使用"("")",Cython 使用"<"">"。在纯Python风格下,可以使用 cython.cast() 函数。例如:

cdef char *p
cdef float *q
p = <char*>q

将 C 值转换为 Python 对象类型或反之时,Cython 将尝试强制。简单示例是类似<int>pyobj的转换,它将 Python 数字转换为普通的 C int值,或者 <bytes>charptr,它将 C char*字符串复制到新的 Python 字节对象中。

注意: Cython 不会阻止冗余的转换操作,只会发出警告。

要获取某些 Python 对象的地址,请使用强制转换为<void*><PyObject*>等指针类型。您还可以使用<object>或更具体的内置或扩展类型(例如<MyExtType>ptr)将 C 指针强制转换回 Python 对象引用。这将使对象的引用计数增加 1,比如转换返回拥有的引用。这是一个例子:

from cpython.ref cimport PyObject

cdef extern from *:
    ctypedef Py_ssize_t Py_intptr_t

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_INCREFPy_DECREF操作。转换为<PyObject *>会创建一个借用的引用,引用计数不变。

检查类型转换

<MyExtensionType>x这样的强制转换会将 x 转换为类MyExtensionType而不进行任何检查。

要检查强制转换,请使用如下语法:<MyExtensionType?>x。在这种情况下,如果x不是MyExtensionType的实例,Cython 将应用运行时检查,该检查会引发TypeError。对于内置类型这会测试是否为确切类(不允许子类),对于扩展类型 则允许子类。

语句和表达式

控件结构和表达式大部分都遵循 Python 语法。当应用于 Python 对象时,它们具有与 Python 相同的语义(除非另有说明)。大多数 Python 运算符也可以应用于 C 值,具有明显的语义。

如果在表达式中混合使用 Python 对象和 C 值,则会在 Python 对象和 C 数字或字符串类型之间自动执行转换。

为所有 Python 对象自动维护引用计数,并自动检查所有 Python 操作是否有错误,并采取适当的操作。

C 和 Cython 表达式之间的差异

C 表达式和 Cython 表达式之间在语法和语义上存在一些差异,特别是在 Python 中没有直接的等价物 的C 语言结构领域。

  • 整数字面量被视为 C 常量,并将被截断为 C 编译器认为合适的任何大小。获取 Python 整数(任意精度)立即转换为对象(例如<object>100000000000000000000)。 LLLU后缀与 C 中的含义相同。

  • Cython 中没有->运算符。而不是p->x,使用p.x

  • 在 Cython 中没有一元*运算符。不使用*p,而是使用p[0]

  • 有一个&运算符,其语义与 C 中相同。

  • 空 C 指针称为NULL,而不是0NULL是保留字)。

  • 类型转换被写为<type>value,例如:

    ```py cdef char p, float q p = q

    ```

范围规则

Cython 完全静态地确定变量是属于局部范围,模块范围还是内置范围。与 Python 一样,给一个没有明确声明的变量赋值将会隐式地把该变量声明为在该赋值区块的局部变量。变量的类型取决于类型推断,但全局模块范围除外,它始终是 Python 对象。

内置函数

Cython 将对大多数内置函数的调用编译为对相应 Python / C API 例程的直接调用,使得它们特别快。

仅优化使用这些名称的直接函数调用。如果你使用其中一个名称假设它是 Python 对象,比如将其分配给 Python 变量,然后调用它,那么调用将作为 Python 函数调用。

功能和参数 返回类型 Python / C API 等效
abs(obj) object,double,...... PyNumber_Absolute,fabs,fabsf,......
callable(obj) bint PyObject_Callable
delattr(obj,name) None PyObject_DelAttr
exec(code,[glob,[loc]]) object *
dir(obj) list PyObject_Dir
divmod(a,b) tuple PyNumber_Divmod
getattr(obj,name,[default]) object PyObject_GetAttr
hasattr(obj,name) bint PyObject_HasAttr
hash(obj) int / long PyObject_Hash
intern(obj) object Py*_InternFromString
isinstance(obj,type) bint PyObject_IsInstance
issubclass(obj,type) bint PyObject_IsSubclass
iter(obj,[sentinel]) object PyObject_GetIter
len(obj) Py_ssize_t PyObject_Length
pow(x,y,[z]) object PyNumber_Power
reload(obj) object PyImport_ReloadModule
repr(obj) object PyObject_Repr
setattr(obj,name) vpid PyObject_SetAttr

注 1:Pyrex 最初提供了一个函数getattr3(obj, name, default)(),对应 Python 内置getattr()的三参数形式。 Cython 仍然支持这个功能,但不赞成使用普通的内置,因为Cython 可以在两种形式中进行优化。

运算符优先级

请记住,Python 和 C 之间的运算符优先级存在一些差异,并且 Cython 使用 Python 优先级,而不是 C 优先级。

整数 for 循环

注意:这个语法仅在Cython文件中支持。如果不是Cython文件,可以使用普通的 for-in-range() 来代替

Cython 识别普通的 Python for-in-range 整数循环模式:

for i in range(n):
    ...

如果i被声明为cdef整数类型,它会将其优化为纯 C 循环。需要此限制,否则由于目标体系结构上的潜在整数溢出,生成的代码将不正确。如果您担心循环未正确转换,请使用 cython 命令行(-a)的 annotate 功能轻松查看生成的 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 可以在 body 中使用,循环可以有 else 子句。

Cython 文件类型

Cython 中有三种文件类型:

  • 实现文件,带有.py.pyx后缀。
  • 定义文件,带有.pxd后缀。
  • 包含文件,带有.pxi后缀。

实现文件

顾名思义,实现文件包含函数,类,扩展类型等的实现。此文件几乎支持所有 python 语法。大多数情况下,.py文件可以在不更改任何代码的情况下重命名为.pyx文件,Cython 将保留 python 行为。

Cython 可以编译.py.pyx文件。如果只想使用 Python 语法,则文件名不重要,Cython 不会根据使用的后缀更改生成的代码。但是,如果想要使用 Cython 语法,则需要使用.pyx文件。

除了 Python 语法之外,用户还可以利用 Cython 语法(例如cdef)来使用 C 变量,可以将函数声明为cdefcpdef,并可以使用cimport导入 C 定义。在本页和 Cython 文档的其余部分中可以找到许多其他可用于实现文件的 Cython 功能。

如果相应的定义文件也定义了该类型,则对某些扩展类型的实现部分有对应的限制。

注意:编译.pyx文件时,Cython 首先检查相应的.pxd文件是否存在并首先处理它。它就像一个 Cython .pyx文件的头文件。您可以放入其他 Cython 模块将使用的内部函数。这允许不同的 Cython 模块在没有 Python 开销的情况下使用彼此的函数和类。要了解更多有关如何操作的信息,可以看 pxd 文件

定义文件

定义文件用于声明各种事物。

可以进行任何 C 声明,它也可以是 C / C ++文件中实现的 C 变量或函数的声明。这可以通过cdef extern from完成。有时,.pxd文件用作 C / C ++头文件到 Cython 可以理解的语法的转换。这允许 C / C ++变量和函数直接用于cimport的实现文件中。您可以在 外部 C 代码在Cython中使用C ++中阅读更多相关信息。

它还可以包含扩展类型的定义部分和外部库的函数声明。

它不能包含任何 C 或 Python 函数的实现,也不能包含任何 Python 类定义或任何可执行语句。如果想要访问cdef属性和方法,或从本模块中定义的cdef类继承时,这个定义文件是必须的。

注意:您不需要(也不应该)在声明文件 中声明任何内容为public 以使其可用于其他 Cython 模块;只要存在于定义文件中就已经满足这个要求。如果要为外部 C 代码提供某些内容,则需要声明为public

include 语句和包含文件

警告:include语句曾经被用于共享声明。请用 在 Cython 模块间共享声明 代替。

Cython 源文件可以使用 include 语句包含来自其他文件的材料,例如:

include "spamstuff.pxi"

从那一刻起,指定文件的内容已经囊括在在文本中。包含的文件可以包含在 include 语句出现的上下文中有效的任何完整语句或声明,包括其他 include 语句。包含文件的内容应该以缩进级别零开始,并且将被视为缩进到包含该文件的 include 语句的级别。但是,include 语句不能在模块范围之外使用,例如在函数或类主体内部。

注意:还有其他机制可用于将 Cython 代码拆分为单独的部分,在许多情况下可能更合适。参见 在 Cython 模块间共享声明

条件编译

某些功能可用于 Cython 源文件中的条件编译和编译时常量。

注意:这个特性很少用到。特别是,它不是一个将代码适配平台和环境的好方法。使用代码生成技术或者(更好的是)C编译器来完成这个适配。看这个例子, 与外部C代码互动 注意:在纯Python风格下,Cython目前不支持条件编译和编译时常量。这个应该不会改变了。

编译时定义

可以使用 DEF 语句定义编译期常量:

DEF FavouriteFood = u"spam"
DEF ArraySize = 42
DEF OtherArraySize = 2 * ArraySize + 17

DEF的右值必须是有效的编译期表达式。这些表达式由使用DEF语句定义的字面量和名称组成,使用任何 Python 表达式语法进行组合。

下面这些编译期名称已经被预定义了,对应于 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,reverse,round,set,slice,sorted,str,sum,tuple,xrange,zip

请注意,在 Python 2.x 或 3.x 下编译时,其中一些内置函数可能不可用,或者两者中的行为可能不同。

使用DEF定义的名称可以在标识符出现的任何地方使用,并且用其编译期值替换,就好像它在那时作为文字写入源中一样。为此,编译期表达式必须求值为intlongfloatbytesunicode(Py3 中的str)类型的 Python 值。

from __future__ import print_function

DEF FavouriteFood = u"spam"
DEF ArraySize = 42
DEF OtherArraySize = 2 * ArraySize + 17

cdef int a1[ArraySize]
cdef int a2[OtherArraySize]
print("I like", FavouriteFood)

条件语句

IF语句可用于在编译时有条件地包含或排除代码段。它的工作方式与 C 中的#if预处理程序指令类似:

IF UNAME_SYSNAME == "Windows":
    include "icky_definitions.pxi"
ELIF UNAME_SYSNAME == "Darwin":
    include "nice_definitions.pxi"
ELIF UNAME_SYSNAME == "Linux":
    include "penguin_definitions.pxi"
ELSE:
    include "other_definitions.pxi"

ELIFELSE子句是可选的。 IF语句可以出现在正常语句或声明可以出现的任何地方,并且它可以包含在该上下文中有效的任何语句或声明,包括DEF语句和其他IF语句。

IFELIF子句中的表达式必须是DEF语句的有效编译期表达式,尽管它们可以计算任何 Python 值,且结果的真实性以Python 方式确定。


我们一直在努力

apachecn/AiLearning

【布客】中文翻译组