我对以下差异感到困惑。假设我有这个类和一些用例:
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/