是否有一种内置的方法来使用CPython内置功能来使任意可调用对象表现为未绑定的类方法



在Python 2中,可以将任意可调用对象转换为类的方法。重要的是,如果可调用对象是用C实现的CPython内置对象,您可以使用它来创建用户定义类的方法,这些类本身是C层的,调用时不调用字节码。

如果你依赖GIL提供"无锁"同步,这偶尔会很有用;由于GIL只能在op代码之间交换,如果代码中特定部分的所有步骤都可以被推入C,则可以使其自动行为。

在Python 2中,可以这样做:

import types
from operator import attrgetter
class Foo(object):
    ... This class maintains a member named length storing the length...
    def __len__(self):
        return self.length  # We don't want this, because we're trying to push all work to C
# Instead, we explicitly make an unbound method that uses attrgetter to achieve
# the same result as above __len__, but without no byte code invoked to satisfy it
Foo.__len__ = types.MethodType(attrgetter('length'), None, Foo)

在Python 3中,不再有未绑定的方法类型,并且types.MethodType只接受两个参数并且只创建绑定的方法(这对于Python的特殊方法如__len__, __hash__等是没有用的,因为特殊方法通常直接查找类型,而不是实例)。

在Py3中是否有一些我错过的方法来完成这个?

我看过的东西:

  1. functools.partialmethod(似乎没有C实现,所以它不符合要求,并且在Python实现和比我需要的更通用的目的之间,它的,在我的测试中大约占用5 us, vs. ~200-300 ns用于直接Python定义或Py2中的attrgetter,开销增加了大约20倍)
  2. 试图使attrgetter或类似的遵循非数据描述符协议(不可能的AFAICT,不能在__get__或类似的猴子补丁)
  3. 试图找到一种方法来子类化attrgetter给它一个__get__,但当然,__get__需要以某种方式委托给C层,现在我们回到了我们开始的地方
  4. (特定于attrgetter用例)使用__slots__首先使成员成为描述符,然后尝试以某种方式将数据的结果描述符转换为跳过绑定和获取实际值的最后一步的东西,使其可调用,因此实际值检索被延迟

我不能发誓我没有错过任何这些选项。有人有解决办法吗?完全hack是允许的;我知道我在做病态的事。理想情况下,它应该是灵活的(允许您从class, Python内置函数(如hex, len等)或任何其他未在Python层定义的可调用对象中创建一些行为类似未绑定方法的东西)。重要的是,它需要附加到类上,而不是每个实例上(既可以减少每个实例的开销,也可以在特殊方法下正确工作,在大多数情况下,这些方法绕过实例查找)。

最近找到了一个(可能仅限CPython)解决方案。作为直接调用CPython api的ctypes hack,它有点难看,但它工作,并获得所需的性能:

import ctypes
from operator import attrgetter
make_instance_method = ctypes.pythonapi.PyInstanceMethod_New
make_instance_method.argtypes = (ctypes.py_object,)
make_instance_method.restype = ctypes.py_object
class Foo:
    # ... This class maintains a member named length storing the length...
    # Defines a __len__ method that, at the C level, fetches self.length
    __len__ = make_instance_method(attrgetter('length'))

它在某种程度上是对Python 2版本的改进,因为它不需要定义类来为它创建一个未绑定的方法,您可以通过简单的赋值在类体中定义它(Python 2版本必须在Foo.__len__ = types.MethodType(attrgetter('length'), None, Foo)中显式引用Foo两次,并且只能在class Foo完成定义之后)。

另一方面,它实际上并没有在CPython 3.7 AFAICT上提供性能优势,至少在这里它取代def __len__(self): return self.length的简单情况下没有;事实上,在Foo的实例上通过len(instance)访问__len__, ipython%%timeit微基准测试显示,当__len__通过__len__ = make_instance_method(attrgetter('length'))定义时,len(instance)的速度要慢10%。这可能是attrgetter本身的一个工件,由于CPython没有将其移动到"FastCall"协议(在3.8中称为"Vectorcall",当时它是半公开的,供临时第三方使用),因此开销略高,而用户定义的函数在3.7中已经从中受益。以及每次必须动态选择是否执行点或不点属性查找以及单个或多个属性查找(Vectorcall可以通过选择适合在构造时执行的获取的__call__实现来避免这种情况)增加了普通方法所避免的更多开销。它应该在更复杂的情况下获胜(例如,如果要检索的属性是像self.contained.length这样的嵌套属性),因为attrgetter的开销在很大程度上是固定的,而Python中的嵌套属性查找意味着更多的字节代码,但是现在,它并不经常有用。

如果他们有机会为Vectorcall优化operator.attrgetter,我会重新基准测试并更新这个答案。

相关内容

  • 没有找到相关文章

最新更新