【问题标题】:Instance method aliases to builtin-functions in PythonPython中内置函数的实例方法别名
【发布时间】:2021-07-22 20:54:08
【问题描述】:

为了在 Python 中尽可能高效地编写优先级队列的面向对象实现,我遇到了一个有趣的行为。以下代码工作正常

from heapq import heappush


class PriorityQueue(list):
    __slots__ = ()

    def push(self, item):
        heappush(self, item)

但是,我真的不想为调用heappush 编写包装方法,因为调用该函数会产生额外的开销。我推断由于heappush 签名使用list 作为第一个参数,同时将push 类属性与heappush 函数别名,后者成为一个成熟的类实例方法。但是,我的假设结果是错误的,下面的代码给出了错误。

from heapq import heappush


class PriorityQueue(list):
    __slots__ = ()
    push = heappush


PriorityQueue().push(0)
# TypeError: heappush expected 2 arguments, got 1

但是转到cpython heapq 源代码,只需将heappush 实现复制到范围并应用相同的逻辑即可。

from heapq import _siftdown


def heappush(heap, item):
    """Push item onto heap, maintaining the heap invariant."""
    heap.append(item)
    _siftdown(heap, 0, len(heap) - 1)


class PriorityQueue(list):
    __slots__ = ()
    push = heappush


pq = PriorityQueue()
pq.push(0)
pq.push(-1)
pq.push(3)
print(pq)
# [-1, 0, 3]
  • 第一个问题:为什么会这样? Python 如何决定哪个函数适合绑定为实例方法,哪个不适合?
  • 第二个问题:cpython/Lib/heapq.py 中的heappushheapq 模块中的实际heappush 有什么区别?它们实际上是不同的,因为以下代码给出了错误
from dis import dis
from heapq import heappush


dis(heappush)
# TypeError: don't know how to disassemble builtin_function_or_method objects
  • 第三个问题:如何强制Python绑定原生heappush作为实例方法?一些元类魔法?

谢谢!

【问题讨论】:

    标签: python oop methods metaclass python-3.9


    【解决方案1】:

    发生的情况是 Python 在标准库中提供了许多算法的纯 Python 实现,即使它包含相同算法的加速本机代码实现

    heapq 库就是其中之一 - 如果您选择链接到的文件,但接近尾声,您将看到代码 sn-p,它查看本机版本是否可用,并覆盖 Python 版本,这有你复制粘贴的代码 - https://github.com/python/cpython/blob/76cd81d60310d65d01f9d7b48a8985d8ab89c8b4/Lib/heapq.py#L580

    try:
        from _heapq import *
    except ImportError:
        pass
    ...
    

    heappush 的原生版本被加载到模块中,没有简单的方法来获取对原始 Python 函数的引用,只能获取实际的文件源代码。

    现在,重点是:为什么原生函数不能作为类方法工作? heappush 的类型是 builtin_function_or_method,与纯 Python 函数的 function 形成对比 - 主要区别之一是第二种对象类型具有 __get__ 方法。这个__get__ 使Python 定义的函数作为“描述符”工作:当从实例中检索属性时调用__get__ 方法。对于普通函数,此调用记录self参数,并在实际函数调用时注入。

    因此,很容易编写一个“instancemethod”装饰器,它可以让内置函数像 Python 函数一样工作并且可以作为方法使用。但是,创建部分函数或 lambda 函数的开销应该超过您试图消除的额外函数调用的开销 - 因此您不应该从中获得速度提升,尽管它可能仍然读起来更优雅:

    class instancemethod:
        def __init__(self, func):
            self.func = func
        def __get__(self, instance, owner):
            return lambda *args, **kwargs: self.func(instance, *args, **kwargs)
    
    import heapq
    
    class MyHeap(list):
        push = instancemethod(heapq.heappush)
    

    【讨论】:

    • 感谢您的全面回答。是的,您建议的代码确实会引发错误。但是为什么我的第一个代码 sn-p 中的本机 Python heappush 可以正常工作?从逻辑上讲,还应该检查selflist 类型之间的对应关系。
    • 抱歉,self.func 调用的第一个参数必须是instance - 我写过self - 那是因为它没有通过isinstance(x, list) 内部检查。该解决方案确实有效(尽管如前所述,它应该比简单的换行还要慢)
    【解决方案2】:

    也许是 python 调用函数的方式。当您尝试print(type(heappush)) 时,您会注意到不同之处。

    对于问题1,用于识别哪个函数是哪个类型的装饰器(即staticmethodclassmethod)就像调用和处理函数并将处理后的函数返回到该名称。所以确定的数据应该在函数的某个属性中。等我找到它在哪里,问题3就可以解决了。

    对于问题2,当你导入内置函数时,它的类型是builtin_function_or_method。但是如果你复制并粘贴它,它是在你的代码中定义的,所以它只是function。这可能会导致解释器将其称为静态方法而不是实例方法。

    【讨论】:

    • 你得到了正确的线索,但没有完全切中要害 - 当然,函数类型的变化不是因为它被“复制并粘贴”到不同的模块
    • 您知道原因(在您的回答中)。但是为了澄清,这就像你在解释器中实现一个函数作为一些“python”代码的副本。当您导入“本机”代码时。但公平地说,在一般情况下。感谢您的澄清和解释。
    猜你喜欢
    • 1970-01-01
    • 2011-04-03
    • 1970-01-01
    • 1970-01-01
    • 2020-02-03
    • 2020-12-11
    • 1970-01-01
    • 1970-01-01
    • 2016-02-04
    相关资源
    最近更新 更多