2

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

如果您依赖 GIL 提供“无锁”同步,这有时会很有用;由于 GIL 只能在操作代码之间交换,如果代码的特定部分中的所有步骤都可以推送到 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我们,而直接约 200-300 ns Python 定义或attrgetter在 Py2 中,开销增加了大约 20 倍)
  2. 试图使attrgetter之类遵循非数据描述符协议(不可能的 AFAICT,不能在 a__get__等中进行猴子补丁)
  3. 试图找到一种方法来子类化attrgetter给它一个__get__,但当然,__get__需要以某种方式委托给 C 层,现在我们回到了我们开始的地方
  4. (特定于attrgetter用例)__slots__首先使用使成员成为描述符,然后尝试以某种方式从数据的结果描述符转换为跳过绑定的最后一步并将实际值获取为使其可调用的东西的东西所以真正的价值检索被推迟了

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

4

1 回答 1

1

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

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两次Foo.__len__ = types.MethodType(attrgetter('length'), None, Foo),并且仅在class Foo完成定义之后)。

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

如果他们有时间优化operator.attrgetterVectorcall,我将重新设置基准并更新这个答案。

于 2019-11-14T20:53:24.993 回答