使用Cython时需要注意的事项


即使您阅读了官方文档,该文档也会从您的耳朵和耳朵中溜走,所以我想在这里进行总结。

def,cdef,cpdef

老实说,当我第一次看到它时,我想知道如何正确使用它,但是可以控制范围,可以这么说:

<表格>

范围

def

cdef

cpdef


<身体>

从Python可见

△(可以在.pyx文件中使用)


从C可见

×




定义为def

的Python函数

首先,def用于Python函数定义。 " Python函数"不能做的一件事是"从C函数中调用"。在C语言端进行处理时,不能调用Python函数(例如,在cdef函数内部)。它不能作为回调函数传递给C库。

cdef

的C函数定义

如果要定义可以从C语言端调用的函数,请使用cdef。 Cython将此代码转换为C代码,然后进行编译。换句话说,这就像"用Python语法编写C代码"。

由于它是

,因此cdef函数中需要以下各项:

  • 所有变量的类型说明(基于C语言)
  • 所有函数调用均等效于cdef(cdef或C库函数)

使用cdef

进行变量定义

如果要使用C语言类型定义变量,请基本上使用cdef。在这种情况下,不仅可以在cdef C函数中,而且可以在def中的Python函数中执行变量cdef

Python函数中用cdef定义变量的优点是将代码的那部分翻译成C算法。在Python上添加int a+b时,与调用int.__add__(a, b)几乎有相同的开销,但是在C语言上,它直接写为a+b。当使用while循环执行许多增量和条件表达式时,这种开销就会发挥作用。

另一个优点是,例如,如果您事先知道Python变量pyobj的类型为int(请参见下文),Cython将"将代码cdef int a = pyobj分配给C中的int"。可以说它将被自动转换为。这允许Python和C在Cython上无缝组合。

使用cpdef

的混合函数定义

到目前为止,我们可以看到defcdef可以分别用于定义Python和C函数。但是,根据情况,您可能需要创建一个可以处理Python对象的C语言函数(回调函数等)。在这种情况下使用cpdefcpdef函数是可以在Python中具有实体的同时从C语言端调用的函数。但是,有一些开销是以Python实体的灵活性为代价的。

cdef类别

函数一样,您可以在类定义中使用cdef。这就像一个多毛的结构,基本上是从Cython侧使用的。

  • 从Python方面,您可以像创建常规类一样创建实例。
  • 如果在.pyx文件中写入def,则实例方法基本上等效于cdef
  • 实例方法也可以从Python端称为built-in function
  • 方法和实例变量基本上都只能在Cython上定义。实例变量的初始化和释放由功能__cinit____dealloc__(不太可能调用__init____del__)完成。

    • 通过cpdef方法,它变成了可以在Python端重写的方法(但是会慢一些)。
    • 通过声明实例变量public,Python会将其视为只读变量。
    • 如果要读取或写入实例变量,请使其成为属性。使用@property@xxx.setter装饰器限定方法。此方法不必是cpdef,对于def似乎很好。

调用C语言库

为了使用外部库,有必要以Cython可以理解的方式描述头文件中定义的内容(函数原型,结构等)。大致有两种方法,但是两种方法的符号相同。

如何在.pyx文件中写入

这是更简单的方法。作为图像,感觉像是"将库的符号直接导入到.pyx文件所对应的名称空间中"。

对于标准库,它看起来像这样:

1
2
3
4
5
6
cdef extern from "<math.h>":  # 標準ライブラリの場合
    DEF HUGE_VAL = 1e500      # #defineマクロ ('='に注意)
    double exp(double x)      # 関数プロトタイプ
    # 使う関数、マクロだけ定義しておけばよい

# この状態で、同じ.pxdファイル内で HUGE_VALやexpを使うことができる

对于其他(普通)库:

1
2
3
4
5
6
7
8
9
10
11
12
13
cdef extern from "spam.h":
    DEF ARTIFICIAL = 1        # #defineマクロ

    ctypedef int fattype_t    # typedef

    int spam_counter          # グローバル変数

    void order_spam(int tons) # 関数プロトタイプ

    struct spam:              # 構造体定義
       fattype_t fat_type
       double    fat_content
       # アクセスする必要があるメンバだけ定義しておけばよい

如何声明到.pxd文件

Cython允许您创建一个扩展名为.pxd(pyrex定义)的专门用于C库的extern声明的文件。您所要做的与上面相同,只是写入.pxd文件,而不是直接写入.pyx文件。

更像是在模块化名称空间中定义C库符号。这样可以方便地以cimport ...格式从任何.pyx文件读取。

例如,如下定义libspam.pxd

1
2
3
4
5
# in: libspam.pxd
cdef extern from "spam.h":
    void order_spam(int tons)
    struct spam:
        ...

这样,可以从同一目录中的.pyx文件中引用C库符号,如下所示:

1
2
3
4
5
6
# in: spamconsumer.pyx
cimport libspam # libspam.pxdの読み込み

libspam.order_spam(20)   # spam.hの関数order_spamの呼び出し

cdef libspam.spam *STOCK # spam.hの構造体spamの参照

对于由各种.pyx文件引用的库,创建.pxd文件很方便。实际上,我认为cimport numpy对于在Cython端准备的numpy隐式加载.pxd

注意1:关于宏

似乎需要使用可由Cython解释的表达式来定义

宏。因此,在某些情况下,您必须引用头文件并复制立即值...可以将宏定义函数表示为原型吗?还是应该定义NAN这样的值?有这样的事情,但是我不确定,因为我还没有尝试过。

注2:关于结构

  • 您只需要定义要访问的成员,因此,如果您有一个将用作黑盒的结构,则可以将其声明为struct Handle: pass(无需定义成员函数)。

  • 只要在相应的头文件中定义了符号,如果在typdef struct foo { ... } Foo;中定义了该符号,则将编译以下任何内容:

  • 定义struct foo: ...,然后定义ctypdef foo Foo

  • 直接定义为struct foo: ...
  • 注3:构建参数

    的描述

    对于不是

    标准库的库,除非明确为setuptools指定Extension选项,否则将无法编译或链接。

    下面是Cython文档中的模板:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    from setuptools import Extension, setup
    from Cython.Build import cythonize

    extensions = [
        # 第1引数がモジュール名、
        # 第2引数がモジュールに必要なコードファイル(.pyx, .c, ...)の配列
        Extension("primes", ["primes.pyx"],
            include_dirs=[...], # インクルードディレクトリの配列
            libraries=[...],    # ライブラリ名の配列(gccの'-l'オプションに渡す名前)
            library_dirs=[...]),# ライブラリディレクトリの配列(gccの'-L'オプション)

        # Everything but primes.pyx is included here.
        Extension("*", ["*.pyx"],
            include_dirs=[...],
            libraries=[...],
            library_dirs=[...]),
    ]
    setup(
        name="My hello app",
        ext_modules=cythonize(extensions),
    )

    实际上,Extension只是写食谱的容器。构建工作本身由setup()中显示的cythonize函数完成。

    正如我在其他地方所写的,您可以使用numpy.get_include()pkg-config <lib> --cflags之类的命令在某种程度上自动化此处的输入。取决于用例,有可能让用户在最坏的情况下设置环境变量,然后在生成的脚本中查看它。

    删除Python的所有原始检查机制

    Python具有多种检查机制,这些机制赋予了Python灵活性。但是,在追求处理速度时这可能是个问题。

    类型检查

    在Python中,似乎没有类型,可以说对象的属性访问仅由getattr()call()组成。即使它是intfloat,也不会改变。您应该尽可能使用C中定义的变量。

    通过在定义

    函数时指定参数类型,可以保证它是与cdef等效的类型(C本机类型或cdef类):

    1
    2
    3
    4
    5
    def pydef_add(int a, int b):
        return a + b

    cdef int cdef_add(int a, int b):
        return a + b

    例如,在numpy的情况下,C类型信息由Cython(?)准备,因此可以使用它。使用cimport关键字读取类型信息:

    1
    2
    3
    4
    5
    6
    cimport numpy as cnumpy

    ...

    cdef cnumpy.ndarray arr # 配列ポインタの定義
    cdef cnumpy.float64_t value = 0.0 # numpy.float64型の変数の定義

    不检查

    每当调用名称指向None时,Python都会进行隐式检查。您可以使用@cython.nonecheck(False)装饰器关闭此检查。但是,请注意,如果在此处传递None,该程序将崩溃。

    除此之外,还有一个修饰符not None以确保该参数不为None:

    1
    2
    def complicated(cnumpy.ndarray array not None):
       pass

    阵列检查

    Python在访问数组时自动检查下标是否溢出。如果您的代码保证"没有上溢或下溢",则可以使用@cython.boundscheck(False)装饰器关闭此检查。

    另外,当使用NumPy数组时,您可以预先声明一些关于数组形状的声明,这样可以简化过程(Cython会在编译时执行):

    1
    2
    def calc_max(cnumpy.ndarray[cnumpy.float64_t, ndim=1] vec):
        ...

    如上例所示,您可以指定数组中使用的数据类型和数据的维数。通过在此处添加上面的@cython.boundscheck(False),代码将更加有效(接近C本机)

    当心GIL

    对GIL

    的感官理解

    Python具有一种称为全局解释器锁(GIL)的机制,我认为这仅是Python速度缓慢的一半原因。这是对以下原则的要求:

    无论您有多少个Python线程,一个Python进程中只有一个Python解释器。

    从某种意义上讲,这是用于维护进程内Python环境唯一性的重要机制。因此,为防止许多线程在访问一个解释器时陷入混乱,只有运行中的线程会获得解释器的锁,而其他线程正在等待该锁。这是GIL。

    减少Python环境

    中的处理

    这就是为什么当您执行Python代码时,一定要获得GIL(甚至从Cython代码中获得)。即使您正在使用本机线程,此处也会发生GIL冲突,因此处理速度肯定会放慢。

    由于它是

    ,因此重要的是"尽可能多地替换C本机处理(仅使用cdef等效项)"和"在C本机处理期间不获取GIL"以加快处理速度。在Cython中,以with nogil:开头的上下文会导致在没有GIL的情况下执行该上下文。相反,还有上下文规范with gil:

    不要迷失在GIL中(请注意回调函数)

    换句话说,上面的观点是,每当使用Python对象时都需要一个GIL。实际上,如果要在回调函数中使用Python对象,请小心。

    一些与硬件相关的回调函数可能不是Python进程产生的线程。然后,在回调环境中不知道解释器的地址,并且当我尝试在其中使用Python对象时,程序崩溃,并显示"找不到GIL!"错误。

    在这种情况下,您似乎必须启动一个可以从Python看到的线程(无论是Python线程还是PyQt线程),并等待使用Condition.wait()等在其中发生回调。

    结论

    Cython很方便,但是很难理解幕后发生的事情的编程模型。仍然有许多功能(ctypedefdef,读取C头文件等),但是我们只是希望您能理解基本知识。