Monkey Patch —— 从一个 toy extension of Pi Agent 中学到的东西

概要

  • Monkey patch
  • Python function object's __get__ method
  • Python method's self parameter

正文

这两天闲得没事捣鼓 Pi Agent 的插件,最初写了一个特简单的 subagent tool,最后还是去搞 tui 的事了(果然搞 ui 美化什么的最有即时反馈最能吸引我[1])。

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
# main.py
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
# original.py
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
# patch.py
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 对象[2],既然是对象,那肯定有各种属性和方法,可以用 .__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()

# 运行结果
# <class 'function'>
# <class 'method'>
# 3

所以我们可以这么理解类定义中的方法的 self 参数:这东西没什么特殊,就是一个普通的函数的参数罢了,叫别的名字也行,如果你希望这个函数作为普通实例方法被实例调用,那么它需要预留第一个参数来接收被绑定的实例,这个参数惯例用 self。也就是说,类方法定义时不写参数也行,但是这样的代价就是不能用实例调用这个方法。

  1. 我小时候也喜欢捣鼓手机主题什么的... ↩
  2. Python 的设计逻辑就是万物皆对象,我们可以通过 dir(o: object) 查看它通常可用的属性和方法名称。 ↩

Monkey Patch —— 从一个 toy extension of Pi Agent 中学到的东西
https://wtx17.github.io/2026/08/15/从一个-toy-extension-of-Pi-Agent-中学到的东西/
作者
wtx
发布于
2026年8月15日
更新于
2026年11月6日
许可协议