Skip to content

故障排查

原文: 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并不知道 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)  # 有效

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 和 Python dict 之间 - 在 struct 在一个返回Python对象的函数中返回时,如果 struct 的所有成员都可以被转换为Python对象,那么 struct 会被转换为 Python dict。 ```python

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对应对象 - 一个常见的做法是使用一个 def 函数但是参数类型设置为 std::vector。这将自动转换为Python的list对象。 ```python from libcpp vector cimport vector

def 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 而不是用潜在的生成 listvector。这足够反直觉以至于我真的不推荐可转换的类型暴露为属性。



回到顶部