Python C 扩展装饰器

4 投票
1 回答
710 浏览
提问于 2025-04-18 10:27

首先,抱歉我说的有点多背景信息。

最近我在玩Python/C-API(Python 3.4),遇到了一些问题。我的目标是创建一个C扩展,可以用作函数或方法的装饰器。我有一个相对不错的原型,做了一个LRU缓存,具体内容可以在这个链接找到:https://gist.github.com/pbrady/916495198910e7d7c713。虽然这个缓存工作得很好,而且速度很快,但我遇到了两个问题:

  1. 我的装饰器返回的type不是函数,也就是说:

    >>> from lrucache import lrucache
    >>> @lrucache()
    ... def f(a, b):
    ...    return a+b
    ...
    >>> type(f)
    >>> <class 'lrucache.cache'>
    

    因此,这个装饰器在方法上不能正常工作——看起来self丢失了,因为Python在看到我的类时并没有创建实例方法(这也很合理)。

  2. 把装饰函数的__doc__复制到我的缓存类中,并不会影响help显示的信息。

为了解决这些问题,我的想法是简单地返回用户的函数,并做一些修改,而不是返回一个新的自定义类对象。这些修改包括:

  1. 给函数添加一个__wrapped__属性,并指向原函数。

  2. 重写__call__属性,这样函数调用就会指向一个自定义的C例程。

我成功完成了第一个修改,但重写函数的__call__方法没有任何效果,因为解释器一般不使用这个方法。

我的问题(终于来了): 我该如何创建一个Python函数(也就是一个PyFunctionObject),使其能够调用一个C函数?

或者,如果有更好的方法,我也很感兴趣。这个主要是一个学习和娱乐的练习,所以我对基于Cython的解决方案不是很感兴趣。

1 个回答

4

首先,你的第二步显然是失败的,因为特殊属性是从类中查找的,而不是从实例中查找的:

In [1]: class Test:
   ...:     def __call__(self):
   ...:         print('Called class attribute!')
   ...:         

In [2]: t = Test()

In [3]: def new_call():
   ...:     print('Called instance attribute!')
   ...:     

In [4]: t.__call__ = new_call

In [5]: t()
Called class attribute!

而且你不想修改 function 类的 __call__ 方法,因为这样会大幅改变 Python 代码的语义。

据我所知,创建一个 PyFunctionObject 的方法只有一个,那就是调用 PyFunction_New。但这个方法的问题在于,它需要一个 PyCodeObject 作为参数,而要创建这样的对象,你可以调用:

  • PyCode_NewEmpty:这个方法会创建一个 无效 的代码对象。它仅用于创建你不关心代码内容的框架对象。
  • PyCode_New:这个方法会创建一个字节码对象。这个方法有 14 个参数,其中一个是 PyObject *code,它应该是一个可读的缓冲区,里面包含字节码的二进制表示。
  • Py_CompileStringObject:这似乎是唯一合理的解决方案。基本上,这就是内置的 compile 函数。

一旦你创建了代码对象,创建函数就很简单了。不过要注意,这样做不会带来任何性能上的提升,因为函数对象的主体是被解释执行的。

不过,还有其他解决方案可以解决你的问题:

  • 你可以把你的 lrucache 做成一个 描述符。这就是 Python 对方法的处理方式,自动将 self 作为第一个参数传递,所以你可以模仿这种行为。

  • 你可以写一个非常简单的 Python 模块,导入你的 C 扩展,并提供一个这样的函数:

    from functools import wraps
    
    from _lrucache import cache
    
    def lrucache(func):
        cached_func = cache(func)
    
        @wraps(func)
        def wrapper(*args, **kwargs):
            return cached_func(*args, **kwargs)
    
        return wrapper
    

    这样你就可以通过 Python 代码绕过整个问题。

  • 你可以创建一个函数类型的子类,并返回那个对象。不过,如果 Python 在构建类时检查确切的类型,这可能就不行了,而且实现起来可能也不简单,因为解释器可能会假设函数对象有一些属性,而这些你需要提供。 不,你不能对函数进行子类化...

撰写回答