疑难解答¶
本节提供了一些关于常见错误的一般故障排除建议。如果您在使用 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 不知道 c1
和 c2
的类型。将它们类型化为 Counter c1
和 Counter 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 staff
或 cdef 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
到/从 Pythondict
- 如果struct
的所有元素本身都可以转换为 Python 对象,那么如果从返回 Python 对象的函数中返回struct
,它将被转换为 Pythondict
# 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
。这很不直观,我强烈建议不要将可转换类型作为属性暴露出来!