概要
- Monkey patch
- Python function object's
__get__ method
- Python method's
self parameter
正文
这两天闲得没事捣鼓 Pi Agent 的插件,最初写了一个特简单的 subagent tool,最后还是去搞 tui 的事了(果然搞 ui 美化什么的最有即时反馈最能吸引我)。
tui extension 主要是搞工具输出的样式,因为我目前用的 zentui extension 没有动工具输出的样式,我想弄得跟 user 消息风格一样,所以启动了 vibe coding。看 GLM-5.3 写出来的代码,发现是这样的:
1 2 3
| export default function (_pi: ExtensionAPI) => { ... }
|
也就是这个函数根本没用到 ExtensionAPI!我以为所有的拓展都应该走类似 pi.registerTool() 这样的调用,才能实现功能。
那这是怎么回事呢?我直接对 GLM-5.3 就是一个问,回答是 monkey patch。这手段简单来说就是修改先前定义好的类,下面我用自己比较熟悉的 python 来讲讲:
首先看文件结构:
1 2 3 4
| . ├── main.py ├── original.py └── patch.py
|
内容分别如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| import original import patch
sth = original.SomeClass(1,2) sth.someMethod()
patch.patch()
anoth = original.SomeClass(1,2) anoth.someMethod()
sth.someMethod()
|
1 2 3 4 5 6 7 8
| class SomeClass: def __init__(self, a, b) -> None: self.a = a self.b = b
def someMethod(self): print("I did something.")
|
1 2 3 4 5 6 7 8 9 10 11 12
| import original
def patch(): cls = original.SomeClass original_method = cls.someMethod
def patched_method(self): print("I did some other things.") original_method(self)
cls.someMethod = patched_method
|
很直观,看一下大概就懂了,运行 main.py 的结果是
1 2 3 4 5
| I did something. I did some other things. I did something. I did some other things. I did something.
|
就是可以对定义好的类进行修改,这个修改会改变所有实例的行为。
这引出一些对 python 运行原理的大致理解:
- 导入的模块中的类、函数等,内存中只有一份(
patch.patch() 所更改的 SomeClass,与 original.SomeClass 是同一个。)
- 当调用某个实例
obj 的方法 .m() 时,程序会从 type(obj).__dict__ 中找相应的方法来执行。
但是我又有一个新问题了,这个 self 参数是干啥用的啊,这上面一顿操作下来,把类的方法当作普通函数对象看,基本上可以理解,就是 self 这东西咋这么扎眼呢?
这时候就靠 GPT 大人了,我总结一下相关的知识。
首先要理解 Python 中的 function 对象,既然是对象,那肯定有各种属性和方法,可以用 .__dict__ 查看其独有的属性和方法:
1 2 3 4
| def foo(): return type(foo) print(type(foo).__dict__)
|
运行结果:
1 2 3
| function
{'__new__': <built-in method __new__ of type object at 0x104a2e338>, '__repr__': <slot wrapper '__repr__' of 'function' objects>, '__call__': <slot wrapper '__call__' of 'function' objects>, '__get__': <slot wrapper '__get__' of 'function' objects>, '__closure__': <member '__closure__' of 'function' objects>, '__doc__': <member '__doc__' of 'function' objects>, '__globals__': <member '__globals__' of 'function' objects>, '__module__': <member '__module__' of 'function' objects>, '__builtins__': <member '__builtins__' of 'function' objects>, '__code__': <attribute '__code__' of 'function' objects>, '__defaults__': <attribute '__defaults__' of 'function' objects>, '__kwdefaults__': <attribute '__kwdefaults__' of 'function' objects>, '__annotations__': <attribute '__annotations__' of 'function' objects>, '__dict__': <attribute '__dict__' of 'function' objects>, '__name__': <attribute '__name__' of 'function' objects>, '__qualname__': <attribute '__qualname__' of 'function' objects>, '__type_params__': <attribute '__type_params__' of 'function' objects>}
|
我们着重看 __get__ 这个方法。事实上类里面定义的方法就是一个普通的函数,不过这个函数实现了特定的 __get__ 方法,因此成了方法。当我们通过类 Cls 的实例 obj 调用方法 .m() 时,python 大致会做:
- 从
Cls.__dict__ 中找到 'm'
- 调用
m.__get__(obj, Cls) 返回一个 bound method,这个方法与原方法基本上并无二致,除了把第一个参数与 obj 绑定
- 用上面返回的 bound method 接受参数,执行动作
看下面的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class A: def __init__(x, a: int) -> None: x.a = a def foo(x): print(x.a)
obj = A(3)
print(type(A.foo)) print(type(obj.foo)) obj.foo()
|
所以我们可以这么理解类定义中的方法的 self 参数:这东西没什么特殊,就是一个普通的函数的参数罢了,叫别的名字也行,如果你希望这个函数作为普通实例方法被实例调用,那么它需要预留第一个参数来接收被绑定的实例,这个参数惯例用 self。也就是说,类方法定义时不写参数也行,但是这样的代价就是不能用实例调用这个方法。