疑难解答

本节提供了一些关于常见错误的一般故障排除建议。如果您在使用 Cython 时遇到问题,可能值得阅读本节。如果您遇到我们尚未在此处涵盖的常见错误,我们感谢您提交 PR 将其添加到本节!

语言“混乱”的地方

出于必要,Cython 混合了 Python 的运行时解析动态行为和 C 的编译时静态定义解析行为,这两种行为略显奇怪。它们并不总是完美地结合在一起,经常导致混淆的地方往往是它们相遇的地方。

例如,对于 cdef class,Cython 可以直接访问 cdef 属性(作为简单的 C 查找)。但是,如果直接属性查找“失败”,Cython 不会产生错误消息 - 相反,它假设它能够在运行时通过标准 Python“从字典中查找字符串”机制来解析该属性。这两种机制在工作方式和返回值方面有很大不同(Python 机制只能返回 Python 对象,而直接查找可以返回几乎任何 C 类型)。

当一个名称被导入而不是“cimported”时,也会发生类似的情况 - Cython 不知道该名称来自哪里,因此将其视为一个普通的 Python 对象。

这种对 Python 行为的静默回退通常是混淆的根源。在最好的情况下,它会提供相同的整体行为,但速度稍慢(例如,通过 Python 机制而不是直接调用 C 来调用 cpdef 函数)。通常,它只会导致运行时出现 AttributeError 异常。偶尔,它可能会做一些完全不同的事情 - 调用与 cdef 方法同名的 Python 方法,或导致从 C++ 容器转换为 Python 容器。

这种双层行为可能不是从头开始设计语言的方式,但对于 Cython 的目标来说是必要的,即与 Python 兼容并允许 C 类型无缝使用。

AttributeErrors

无类型对象

导致 AttributeErrors 的一个常见原因是 Cython 不知道对象的类型

cdef class Counter:
    cdef int count_so_far

    ...

属性 count_so_far 只能从 Cython 代码访问,Cython 通过它为 Counter 定义的 C 结构体进行直接查找来访问它(即它非常快!)。现在尝试在两个 Counter 对象上运行以下 Cython 代码

def bigger_count(c1, c2):
    return c1.count_so_far < c2.count_so_far

这将导致 AttributeError,因为 Cython 不知道 c1c2 的类型。将它们类型化为 Counter c1Counter c2 可以解决问题

def bigger_count(c1, c2):
    return c1.count_so_far < c2.count_so_far

同一个问题的常见变体发生在全局对象上

def count_something():
    c = Counter()

    # code goes here!!!

    print(c.count_so_far)  # works

global_count = Counter()
print(global_count.count_so_far)  # AttributeError!

在函数内部,Cython 通常能够推断类型。因此,即使您没有告诉它,它也知道 c 是一个 Counter。但是,同样的情况适用于全局/模块范围。在这里,有一个强烈的假设,即您希望将对象公开为模块的 Python 属性(并且请记住,Python 属性可以从其他地方修改……),因此 Cython 本质上禁用了所有类型推断。因此,它不知道 global_count 的类型。

写入扩展类型

AttributeErrors 也会在写入 cdef class 时发生,通常在 __init__ 中。

cdef class Company:
    def __init__(self, staff):
        self.staff = staff  # AttributeError!

与普通类不同,cdef class 具有可以写入的固定属性列表,并且需要显式声明它们。例如

cdef class Company:
   cdef list staff
   # ...

(如果不想指定类型,可以使用 cdef staffcdef object staff)。如果需要添加任意属性,则可以添加 __dict__ 成员

cdef class Company:
   cdef dict __dict__
   def __init__(self, staff):
       self.staff = staff

这提供了额外的灵活性,但会失去使用扩展类型的一些性能优势。它还会对继承添加限制。

扩展类型类属性与实例属性

Python 中的一种常见模式(在 Cython 代码库本身中广泛使用)是使用覆盖类属性的实例属性

class Email:
    message = "hello"  # sensible default

    def actually_I_really_dislike_this_person(self):
        self.message = "go away!"

访问 message 时,Python 首先查找实例字典以查看它是否具有 message 的值,如果失败,则查找类字典以获取默认值。优点是

  • 它提供了一个简单明了的默认值,

  • 它可能通过在必要时不填充实例字典来节省一些内存(尽管现代版本的 Python 在实例之间共享常见属性的键方面非常出色),

  • 它节省了一些时间引用计数(与在构造函数中初始化默认值相比),

Cython 扩展类型不支持这种模式。您应该只在构造函数中设置默认值。如果您没有为 cdef 属性设置默认值,那么它们将被设置为“空”值(对于 Python 对象属性,为 None)。

自动类型转换的陷阱

Cython 会自动在某些 C/C++ 类型和 Python 类型之间生成类型转换。这些通常是不可取的。

首先,我们应该看看 Cython 生成了哪些转换

  • C struct 到/从 Python dict - 如果 struct 的所有元素本身都可以转换为 Python 对象,那么如果从返回 Python 对象的函数中返回 struct,它将被转换为 Python dict

    # taken from the Cython documentation
    cdef struct Grail:
        int age
        float volume
    
    def get_grail():
        cdef Grail g
        g.age = 100
        g.volume = 2.5
        return g
    
    print(get_grail())
    # prints something similar to:
    # {'age': 100, 'volume': 2.5}
    
  • C++ 标准库容器 到/从它们的 Python 等效项。一种常见的模式是使用类型为 std::vector 的参数的 def 函数。这将从 Python 列表自动转换

    from libcpp.vector cimport vector
    
    def print_list(vector[int] x):
        for xi in x:
            print(x)
    

大多数这些转换应该双向工作。

它们有一些不明显的缺点。

转换不是免费的

特别是对于 C++ 容器转换。考虑上面的 print_list 函数。该函数很有吸引力,因为对向量的迭代比对 Python 列表的迭代更快。但是,Cython 必须迭代输入列表的每个元素,检查它是否可以转换为 C 整数。因此,您实际上并没有节省任何时间 - 您只是将“昂贵”循环隐藏在函数签名中。

如果您在函数内部执行了足够的工作,这些转换可能会有所帮助。您还应该考虑在 Cython 代码中有一个单独的位置进行转换,作为您与 Python 的接口,然后将类型保留为 C++ 类型,并在多个 Cython 函数中对其进行操作。

在许多情况下,使用一维类型内存视图(int[:])并传入 array.array 或 Numpy 数组,而不是使用 C++ 向量,可能会更好。

更改不会传播回去

特别是对于通过属性(包括通过 cdef public 属性)公开给 Python 的 cdef classes 的属性。

例如

from libcpp.vector cimport vector

cdef class VecHolder:
    def __init__(self, max):
         self.value = list(range(max))  # just fill it for demo purposes

    cdef public vector[double] values

然后从 Python

vh = VecHolder(5)
print(vh.values)
# Output: [ 0, 1, 2, 3, 4 ]

vh.values[0] = 100
print(vh.values)
# Output: [ 0, 1, 2, 3, 4 ]

# However you can re-assign it completely
vh.values = []
print(vh.values)
# Output: []

本质上,你的 Python 代码修改了返回给它的 list,而不是修改用于生成 list 的底层 vector。这很不直观,我强烈建议不要将可转换类型作为属性暴露出来!