【问题标题】:Python C extension decoratorPython C 扩展装饰器
【发布时间】:2014-06-20 04:46:29
【问题描述】:

对于冗长的背景信息,我提前道歉。

我最近一直在玩 Python/C-API(Python 3.4)并且被难住了。我的目标是拥有一个可以用作函数/方法装饰器的 C 扩展。我有一个相当不错的 LRU 缓存原型,https://gist.github.com/pbrady/916495198910e7d7c713。虽然缓存可以工作并且速度非常快,但存在两个问题:

  1. my 装饰器返回的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 例程。

我能够完成 (1),但是覆盖函数 __call__ 方法并没有做任何事情,因为解释器通常不使用它。

我的问题(最后): 如何创建将调用 C 函数的 Python 函数(即 PyFunctionObject)?

或者,如果有更好的方法来做到这一点,我也会对此感兴趣。这主要是一个学习/有趣的练习,所以我对基于 Cython 的解决方案不太感兴趣。

【问题讨论】:

  • 我没有直接使用 Python/C-API,但也许(因为这是一个学习/有趣的练习)尝试在 python 中创建一个调用 C 函数的装饰器。或者创建一个简单的 C 装饰器,它返回正确的函数值/类型(取决于它的丑陋程度)。抱歉不知道解决办法。

标签: python c decorator python-c-api


【解决方案1】:

那么您的 (2) 步骤显然失败了,因为在类上查找特殊属性,而不是在实例上:

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 设为descriptor。这是 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 在构建类时检查确切的类型,这可能不起作用,并且实现可能不是微不足道的,因为解释器可能会假设您必须提供的有关函数对象的一些属性。 不,你不能子类化函数...

【讨论】:

  • 感谢您深思熟虑的答案。可悲的是,我没有足够的代表来投票。简单的包装器解决方案效果很好。我受到了大约 50% 的性能影响,但它仍然比纯 python 版本快 4-5 倍。我曾尝试过子类化功能,但遇到了问题。我认为这可能是由于函数类型没有 Py_TPFLAGS_BASETYPE。就描述符路由而言,这是否也适用于常规函数,还是我也需要非描述符版本?
  • @ptb 哦,我没有检查function 是否是可子类化的,所以如果没有 C 级别的严重黑客攻击,这条路线似乎并不实用。关于描述符,就描述符而言,它应该可以工作。您必须在 lrucache 上实现由普通函数调用的 __call__ 方法,而方法查找将首先调用 __get__ 并在返回的对象上使用 __call__
  • 我能够通过在 tp_descr_get 中添加一个简单的函数来使描述符版本工作。此外,通过对 doc 使用 getset 方法(就像在 descrobject.c 中所做的那样),我能够获得正确的文档字符串。完成的版本在github.com/pbrady/fastcache。谢谢你的帮助。
猜你喜欢
  • 1970-01-01
  • 2020-08-20
  • 2021-12-01
  • 1970-01-01
  • 1970-01-01
  • 2019-05-17
  • 2016-08-18
  • 2013-01-22
  • 2020-06-26
相关资源
最近更新 更多