故障排查
原文: https://cython.readthedocs.io/en/latest/src/userguide/troubleshooting.html
这个文档提供了一些关于常见错误的故障排查建议。如果你用Cython时遇到了问题,那么有必要读一下。如果你遇到了一个常见的、但是这里没有提到的错误。我们很欢迎你给这个章节提Pull Request。
语言的混乱区
当然,Cython是一个奇怪的混合物:它有Python运行时解析、动态的行为;它也有C一样编译器解析、静态确定的行为。这些肯定不能完美的结合在一起,这两者交汇的地方也通常是经常让人混淆的地方。
比如说,对于一个 cdef class
, Cython可以直接访问它的 cdef
属性(通过一个简单的C查找)。但是,如果直接属性查找没找到,Cython不会产生任何错误信息 -- 相反,它会假设能够在运行的时候通过标准Python的“从字典中查找字符串“的方式能够解析这个属性。这两个机制在工作方式和返回值方面都相当不同(Python机制只能返回Python对象,C直接查找可以反悔几乎任何C类型)。
当一个名称被引入( import
) 而不是 ( cimport
) 的时候也会发生类似的事 - Cython并不知道名称从哪里来 - 因此它假设这是一个普通的Python对象。
这种默默的的失败然后交给Python的行为经常会造成混乱。最好的情况下,它的总体行为完全一致,但是稍慢(比如通过Python机制调用 cpdef
函数而不是直接调用C函数)。很多时候,它只会在运行期间引发一个 AttributeError
异常。偶尔,它会做点不同的事 -- 调用和 cdef
方法名称相同的Python方法 或者引发 C ++容器到Python容器的类型转换。
这种双层的行为机制大概不是用来从零设计一门语言的,但是对于Cython的目标来说很有必要 -- 既兼容Python又允许相当流畅的使用C类型。
属性错误
未标记类型的对象
遇到 AttributeError
的一个常见原因是Cython不知道你的对象的类型:
cdef class Counter:
cdef int count_so_far
...
count_so_far 这个属性只在Cyton代码中可以访问,Cython通过在它定义的 Counter
结构中通过C直接查找(真的很快!)。现在尝试对一组 Counter
对象运行下面这个方法。
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) # 有效
global_count = Counter()
print(global_count.count_so_far) # 属性错误
在函数中,Cython通常能够完成类型推理。所以即时你不告诉它,它也知道 c
是一个 Counter
。但是在 全局和模块 的层面就不是这样了。这里有一个非常强的假设:你希望所有对象都是Python模块的属性(请记得Python属性可以在任何地方被修改),因此Cython会根本上禁止类型推理。因此它并不知道 global_count
的类型。
写入扩展类型
AttributeError
也可以在写入 cdef class
的时候发生,通常是在 __init__
中:
cdef class Company:
def __init__(self, staff):
self.staff = staff # AttributeError!
和正常的Python类不同,你可以写入cdef
类的属性列表是固定的。你需要显式的声明它们。
cdef class Company:
cdef list staff
# ...
如果你不想声明类型的话,可以用 cdef staff
或者 cdef object staff
。 如果你要随意添加属性的能力,你可以添加一个 __dict__
成员(译者注:这也是Python的做法)。
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对象的函数中返回时,如果struct
的所有成员都可以被转换为Python对象,那么struct
会被转换为 Pythondict
。 ```pythontaken 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对应对象 - 一个常见的做法是使用一个
def
函数但是参数类型设置为std::vector
。这将自动转换为Python的list对象。 ```python from libcpp vector cimport vectordef print_list(vector[int] x): for xi in x: print(x) ```
大多数转换方式都支持双向转换。
他们有一些并不明显的缺点。
转换不是没有代价的
特别是对于C ++标准库的容器。考虑上面这个 print_list
函数。这个函数非常诱人因为在vector上遍历比在Python列表上快的多。但是Cython在你的输入列表的每个元素上遍历,以检查是否能转换为C整型。因此,你并没有节省时间 - 只是把比较慢的循环藏在了函数签名中。
这些转换当你在函数内做了足够多的工作的时候是值得的。你应该考虑在你的Cython代码中设置一个地方写发生类型转换给Python提供接口,然后让类型作为C ++类型并用这些类型在不同的Cython函数上工作。
变化无法反响传播
特别是,通过特征(包括 cdef public
属性)暴露给Python的 cdef classes
的属性
from libcpp.vector cimport vector
cdef class VecHolder:
def __init__(self, max):
self.value = list(range(max)) # 仅出于演示目的
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 ]
# 你可以完全重新赋值
vh.values = []
print(vh.values)
# Output: []
基本上你的Python代码修改了返回给他的 list
而不是用潜在的生成 list
的 vector
。这足够反直觉以至于我真的不推荐可转换的类型暴露为属性。