Python 的动态特性让程序在运行时也能修改对象的结构和行为,这在很多语言中是做不到的。其中猴子补丁、动态属性和动态方法是三个典型的用法,它们很灵活,但用不好也容易造成混乱。
猴子补丁(Monkey Patching)
猴子补丁指的是在运行时动态修改类或模块的方法、属性,而无需改动原始源代码。常用于临时修复第三方库的 bug、在不允许修改源码的情况下调整行为,或者在不破坏原有接口的前提下扩展功能。
示例:替换模块函数
import json
# 保存原始函数
_original_dumps = json.dumps
def _patched_dumps(obj, **kwargs):
# 强制设定 ensure_ascii=False,输出中文而非 Unicode 转义
kwargs['ensure_ascii'] = False
return _original_dumps(obj, **kwargs)
# 打补丁
json.dumps = _patched_dumps
# 后续所有调用 json.dumps 都会走补丁逻辑
print(json.dumps({'name': '张三'}))
# 输出: {"name": "张三"} (而不是 {"name": "\u5f20\u4e09"})
示例:修补类方法
class Target:
def greet(self):
return "Hello"
def new_greet(self):
return "你好"
# 运行时替换方法
Target.greet = new_greet
t = Target()
print(t.greet()) # 输出: 你好
何时使用猴子补丁?
- 修复第三方库的紧急 bug,在等待官方修复期间作为临时方案。
- 为遗留代码补充功能,而无法修改原代码。
- 编写测试时用 mock 替换有副作用的函数。
注意事项:
- 补丁会影响所有使用该模块的代码,极易引入隐蔽的全局副作用。
- 强烈建议在打补丁时保存原始引用,并在不需要时恢复,或者使用
unittest.mock.patch等工具安全地操作。 - 过度使用会让代码变得难以理解和调试,应该作为最后手段。
动态属性
Python 对象的属性和方法实际上都存储在内部的 dict 字典里,你可以在运行时自由地添加、修改和删除。
直接添加实例属性
class User:
def __init__(self, name):
self.name = name
u = User("张三")
u.age = 30 # 运行时动态添加属性
print(u.age) # 30
实例属性是挂在具体对象上的,不影响类定义。
通过 setattr / getattr 操作属性
setattr(u, 'email', 'zhangsan@example.com')
print(getattr(u, 'email')) # zhangsan@example.com
当属性名来自变量时,这种方式更方便。
动态创建类属性(影响所有新实例)
User.role = 'admin' # 直接在类上添加属性
u2 = User("李四")
print(u2.role) # admin
防误删 / 防动态添加:slots
class Fixed:
__slots__ = ('name', 'age') # 只允许这两个属性
f = Fixed()
f.name = '小红'
f.email = 'test@test.com' # AttributeError
使用 slots 可以阻止动态添加属性,同时节省内存,适合需要严格控制属性且创建大量实例的场景。
动态方法
动态方法是指在运行时给某个实例或类绑定一个新的函数,让它像普通方法一样工作。这比猴子补丁更局部化,通常是给特定实例临时添加行为。
给实例绑定方法
class Dog:
def __init__(self, name):
self.name = name
def bark(self):
return f"{self.name} says Woof!"
d = Dog("旺财")
# 直接赋值不会自动传递 self,需要使用 types.MethodType 包装
import types
d.bark = types.MethodType(bark, d)
print(d.bark()) # 旺财 says Woof!
注意:这样绑定的方法只存在于该实例,其他实例不受影响。
给类绑定方法
Dog.run = lambda self: f"{self.name} is running"
d2 = Dog("来福")
print(d2.run()) # 来福 is running
给类绑定方法会影响所有实例(包括之前创建的),这与猴子补丁完全相同。
使用 getattr / getattribute 动态生成方法
class Router:
def __getattr__(self, name):
if name.startswith('handle_'):
def handler(*args, **kwargs):
return f"Handled {name} with args={args}"
return handler
raise AttributeError(name)
r = Router()
print(r.handle_login('user', 'pass')) # Handled handle_login with args=('user', 'pass')
这种模式在框架开发中很常见,比如根据 URL 动态分发处理器,不用预先定义所有方法。
实战建议
- 能用静态定义解决的问题,不要用动态。动态特性虽然强大,但会降低代码可读性和 IDE 智能提示的准确性。
- 猴子补丁永远先考虑正规替代:fork 并修复第三方库、使用继承 + 重写、使用配置参数等。
- 动态属性适合:处理不确定结构的数据(如 JSON 反序列化后的对象包装)、框架内部逻辑(如 ORM 的字段描述符)、编写测试和 mock 工具。
- 动态方法适合:插件化架构、运行时行为扩展、简化重复代码的模式生成。
- 保持动态修改的范围尽量小和明确,并在代码中留下必要的注释,说明为什么要这么做。
掌握这些技巧,你就能在必要时跳出常规 OOP 的框架,以更灵活的方式组织代码。但请记住:能力越大,责任越大,动态特性用在对的地方是蜜糖,滥用则可能变成整个项目的维护噩梦。