猴子补丁:将类中的方法替换为函数

3 投票
1 回答
1014 浏览
提问于 2025-04-18 14:27

我有一些傻乎乎的代码,其中有个猴子匹配的部分。下面的例子只是为了自学,并不是用在实际项目中。

class MyClass:

    def some_method(self):
        print("some_method call")
        self.yet_another_method()

    def yet_another_method(self):
        print('yet_another_method call')


def some_function(self):
    print("some function call")
    self.yet_another_method()

obj = MyClass()
obj.some_method()
obj.some_method = some_function
obj.some_method()

当我执行这段代码时,我遇到了以下错误:

TypeError: non_class_some_method() missing 1 required positional argument: 'self'

很明显,Python解释器不能隐式地把obj传递给some_function。但是当我进行代码检查并查看字节码时,我发现方法的表现形式是相似的(替换前后都是这样)。

import dis
import inspect


class MyClass:

    def some_method(self):
        print("some_method call")
        self.yet_another_method()

    def yet_another_method(self):
        print('yet_another_method call')


def some_function(self):
    print("some function call")
    self.yet_another_method()

obj = MyClass()
dis.dis(obj.some_method)
print(inspect.getargspec(obj.some_method))
obj.some_method = some_function
print("======================================================================")
dis.dis(obj.some_method)
print(inspect.getargspec(obj.some_method))

结果是:

  8           0 LOAD_GLOBAL              0 (print) 
              3 LOAD_CONST               1 ('some_method call') 
              6 CALL_FUNCTION            1 (1 positional, 0 keyword pair) 
              9 POP_TOP              

  9          10 LOAD_FAST                0 (self) 
             13 LOAD_ATTR                1 (yet_another_method) 
             16 CALL_FUNCTION            0 (0 positional, 0 keyword pair) 
             19 POP_TOP              
             20 LOAD_CONST               0 (None) 
             23 RETURN_VALUE         
ArgSpec(args=['self'], varargs=None, keywords=None, defaults=None)
======================================================================
 16           0 LOAD_GLOBAL              0 (print) 
              3 LOAD_CONST               1 ('some function call') 
              6 CALL_FUNCTION            1 (1 positional, 0 keyword pair) 
              9 POP_TOP              

 17          10 LOAD_FAST                0 (self) 
             13 LOAD_ATTR                1 (yet_another_method) 
             16 CALL_FUNCTION            0 (0 positional, 0 keyword pair) 
             19 POP_TOP              
             20 LOAD_CONST               0 (None) 
             23 RETURN_VALUE         
ArgSpec(args=['self'], varargs=None, keywords=None, defaults=None)

请问有没有人能解释一下为什么会这样?

1 个回答

4

大部分答案在这里:https://wiki.python.org/moin/FromFunctionToMethod。简单来说:

  • def 关键字总是会生成一个 function 对象。
  • method 其实就是一个很薄的可调用的包装器,它把函数、类和实例包裹起来。
  • 这个包装器只有在函数是类的属性时才会被创建,而不是在它是实例的属性时。

为了让你的代码正常工作(在每个实例上进行猴子补丁),你需要手动调用那个可以把函数、类和实例变成 method 的机制。当使用新式类时,最简单的解决办法是直接在函数上调用描述符协议,也就是:

obj.some_method = some_function.__get__(obj, type(obj))

如果你使用的是像你例子中那样的旧式类(这其实不是个好主意),你可以使用 types.MethodType,但实际上,除非你被一些遗留代码困住了,否则最好把你的类改成新式类(也就是从 object 继承)。

撰写回答