python - Python 中的猴子修补类和实例

标签 python monkeypatching

我对以下差异感到困惑。假设我有这个类和一些用例:

class C:
    def f(self, a, b, c=None):
        print(f"Real f called with {a=}, {b=} and {c=}.")


my_c = C()
my_c.f(1, 2, c=3)  # Output: Real f called with a=1, b=2 and c=3.

我可以猴子修补它以进行这样的测试:

class C:
    def f(self, a, b, c=None):
        print(f"Real f called with {a=}, {b=} and {c=}.")


def f_monkey_patched(self, *args, **kwargs):
    print(f"Patched f called with {args=} and {kwargs=}.")


C.f = f_monkey_patched
my_c = C()
my_c.f(1, 2, c=3)  # Output: Patched f called with args=(1, 2) and kwargs={'c': 3}.

到目前为止一切顺利。但我只想修补一个实例,它以某种方式消耗了第一个参数:

class C:
    def f(self, a, b, c=None):
        print(f"Real f called with {a=}, {b=} and {c=}.")


def f_monkey_patched(self, *args, **kwargs):
    print(f"Patched f called with {args=} and {kwargs=}.")


my_c = C()
my_c.f = f_monkey_patched
my_c.f(1, 2, c=3)  # Output: Patched f called with args=(2,) and kwargs={'c': 3}.

为什么第一个参数被消费为 self 而不是实例本身?

最佳答案

Python 中的函数是描述符;当它们附加到一个类,但查找类的一个实例时,描述符协议(protocol)被调用,代表您生成一个绑定(bind)方法(所以 my_c.f,其中 f class 上定义,与您最初定义的实际函数 f 不同,并隐式传递 my_c 作为 self )。

如果你想做一个仅对特定实例隐藏类 f 的替换,但仍然像你期望的那样将实例作为 self 传递,你需要手动 将实例绑定(bind)到函数以使用(不可否认的非常有据可查的)types.MethodType 创建绑定(bind)方法:

from types import MethodType  # The class implementing bound methods in Python 3

# ... Definition of C and f_monkey_patched unchanged

my_c = C()
my_c.f = MethodType(f_monkey_patched, my_c)  # Creates a pre-bound method from the function and
                                             # the instance to bind to

被绑定(bind)后,my_c.f 现在将表现为一个不接受来自调用者的 self 的函数,但是当被调用时 self 将在构造 MethodType 时作为绑定(bind)到 my_c 的实例接收。


性能比较更新:

看起来,在性能方面,所有解决方案都非常相似,以至于在性能方面无关紧要(Kedar's 描述符协议(protocol)的显式使用和我对 MethodType 的使用是等效的,并且最快,但与 functools.partial 的百分比差异是如此之小,以至于在您正在进行的任何有用工作的重压下都无关紧要):

>>> # ... define C as per OP
>>> def f_monkey_patched(self, a):  # Reduce argument count to reduce unrelated overhead
...     pass

>>> from types import MethodType
>>> from functools import partial
>>> partial_c, mtype_c, desc_c = C(), C(), C()
>>> partial_c.f = partial(f_monkey_patched, partial_c)
>>> mtype_c.f = MethodType(f_monkey_patched, mtype_c)
>>> desc_c.f = f_monkey_patched.__get__(desc_c, C)
>>> %%timeit x = partial_c  # Swapping in partial_c, mtype_c or desc_c
... x.f(1)
...

我什至不打算为 IPython %%timeit 魔术提供准确的计时输出,因为它在不同的运行中会有所不同,即使在不涉及 CPU 节流的桌面上也是如此。我可以肯定地说的是 partial 确实慢了一点,但只慢了 ~1 ns(其他两个通常在 56-56.5 ns 内运行,partial 解决方案通常需要 56.5-57.5),并且需要大量的无关内容的配对(例如,从 %timeit 切换到从全局范围读取名称导致 dict 查找缓存到 %%timeit 中的本地名称以使用简单的数组查找)甚至获得可预测的差异。

重点是,它们中的任何一个都可以在性能方面发挥作用。我个人会推荐我的 MethodType 或 Kedar 对描述符协议(protocol)方法的显式使用(它们在最终结果 AFAICT 中是相同的;两者都产生相同的绑定(bind)方法类),无论哪个看起来更漂亮,因为它意味着绑定(bind)方法实际上是一个绑定(bind)方法(所以你可以提取.__self__.__func__就像你在任何绑定(bind)方法上构造正常方式,其中 partial 要求您切换到 .args[0].func 以获得相同的信息)。

关于python - Python 中的猴子修补类和实例,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/73545390/

相关文章:

Python获取字符串中的第x个单词

python - 向现有对象实例添加方法

javascript - "monkey patching"真的那么糟糕吗?

python - ElementTree 返回元素而不是 ElementTree

python - Pytransitions:是否可以更改机器在模型中注入(inject)的模型属性?

python - Pandas - 跳过 numpy 数组中给定的行数

需要 Python 正则表达式解释 - $ 字符用法

Python 猴子修补类方法导致多处理错误

python3 : bind method to class instance with . __get__(),它有效,但为什么呢?

ruby-on-rails - 猴子修补设计(或任何 Rails gem)