关键词:Python、getattr、setattr、代理模式、延迟加载、属性验证、ORM
预计阅读:25 min,附完整可运行代码示例
0. 前言:为什么你一定要掌握 __xxattr__
在 Python 里,点语法(obj.x)看似平淡无奇,背后却藏着一套可插拔的“钩子”——
__getattr__ / __setattr__ / __delattr__。
它们就像“魔法插槽”,能让你在“属性被触碰”的瞬间植入任意逻辑:查日志、做校验、走网络、懒加载……
本文用“四大模式”带你把官方文档里干巴巴的定义变成能跑、能改、能上线的产品级代码。
1. 代理模式(Proxy):把“转发”玩出花
1.1 场景
- 包装第三方库,隐藏其怪异接口
- 给所有方法统一加日志、缓存、权限
- 实现“远程代理”:本地对象 ≈ 远端服务
1.2 最小可运行模板
class Proxy:
"""通用代理:只暴露被代理对象的公共属性(非 _ 开头)"""
def __init__(self, target):
# 必须用 super 绕过自身的 __setattr__
super().__setattr__('_target', target)
def __getattr__(self, name):
print(f'[GET ] {name}')
return getattr(self._target, name)
def __setattr__(self, name, value):
if name.startswith('_'):
super().__setattr__(name, value)
else:
print(f'[SET ] {name} = {value}')
setattr(self._target, name, value)
def __delattr__(self, name):
print(f'[DEL ] {name}')
delattr(self._target, name)
1.3 使用演示
class Spam:
def __init__(self, x): self.x = x
def bar(self, y): print('Spam.bar:', self.x, y)
s = Spam(2)
p = Proxy(s)
p.x # [GET ] x → 2
p.x = 37 # [SET ] x = 37
p.bar(3) # [GET ] bar → Spam.bar: 37 3
del p.x # [DEL ] x
代理模式在 Python Cookbook 中被官方推荐为“继承的轻量级替代方案”。
2. 属性验证 & 转换:把“赋值”做成管道
2.1 需求
- 类型不对就抛错,而不是运行时崩
- 自动把字符串转数字、大小写归一化
- 支持“只读”“不可删除”等语义
2.2 实现:描述符 + __setattr__ 双保险
class Schema:
age = int
name = str
score = float
class Validated:
def __init__(self, **kw):
# 真实数据放在 __dict__ 本身,避免递归
self.__dict__['_data'] = {}
for k, v in kw.items():
setattr(self, k, v)
def __setattr__(self, name, value):
typ = getattr(Schema, name, None)
if typ is None:
raise AttributeError(f'未知字段 {name}')
if not isinstance(value, typ):
try:
value = typ(value)
except (ValueError, TypeError):
raise TypeError(f'{name} 必须是 {typ.__name__}')
self._data[name] = value
def __getattr__(self, name):
try:
return self._data[name]
except KeyError:
raise AttributeError(name)
u = Validated(age='18', name='bob', score='99.5')
print(u.age, u.score) # 18 99.5
u.age = '20' # 自动转 int
该技巧在 attrs/cattrs 等库中大量使用,可节省 70% 的样板代码。
3. 延迟加载(Lazy Loading):让“昂贵”对象按需出生
3.1 典型昂贵资源
- 大模型文件 / 千万级数据库连接
- 远程网络客户端(gRPC、REST)
- 重量级 GUI 组件
3.2 装饰器版(类属性)
def lazy_property(fn):
"""类级别延迟加载,结果缓存到实例"""
attr_name = '_lazy_' + fn.__name__
@property
def _lazy(self):
if not hasattr(self, attr_name):
setattr(self, attr_name, fn(self))
return getattr(self, attr_name)
return _lazy
class Config:
@lazy_property
def mysql_pool(self):
print('>>> 真正创建 MySQL 连接池 …')
return 'MySQLPool(...)'
3.3 __getattr__ 版(动态模块导入)
class LazyModule:
"""访问时才 import,适用于可选依赖"""
def __getattr__(self, item):
if item == 'pd':
import pandas as pd
self.pd = pd
return pd
raise AttributeError(item)
lm = LazyModule()
lm.pd.DataFrame() # 此处才首次 import pandas
延迟加载能把大型 CLI 工具的启动时间缩短 50% 以上。
4. ORM 框架实现:把“行”变成“对象”
4.1 核心思想
表 → 类,行 → 实例,字段 → 属性。
利用 __getattr__ / __setattr__ 把对属性的访问翻译成 SQL。
4.2 最小 ORM(仅依赖 sqlite3)
import sqlite3, threading
class Field:
def __init__(self, typ): self.typ = typ
class ModelMeta(type):
"""元类:扫描属性,收集字段"""
def __new__(mcls, name, bases, ns):
if name == 'Model': # 基类本身不做处理
return super().__new__(mcls, name, bases, ns)
fields = {k: v for k, v in ns.items() if isinstance(v, Field)}
ns['_fields'] = fields
ns['_table'] = name.lower()
return super().__new__(mcls, name, bases, ns)
class Model(metaclass=ModelMeta):
db = sqlite3.connect('demo.db', check_same_thread=False)
db_lock = threading.Lock()
def __init__(self, **kw):
# 数据保存在 _row,避免与字段名冲突
self._row = {}
for k, v in kw.items():
setattr(self, k, v)
def __setattr__(self, name, value):
if name in self._fields:
self._row[name] = value
else:
super().__setattr__(name, value)
def __getattr__(self, name):
try:
return self._row[name]
except KeyError:
raise AttributeError(name)
def save(self):
cols = ', '.join(self._row.keys())
placeholders = ', '.join(['?'] * len(self._row))
sql = f"INSERT INTO {self._table} ({cols}) VALUES ({placeholders})"
with self.db_lock:
self.db.execute(sql, tuple(self._row.values()))
self.db.commit()
# 定义模型
class User(Model):
id = Field(int)
name = Field(str)
# 使用
u = User(id=1, name='kimi')
u.save() # 真实写入 SQLite
大型库如 Django ORM、SQLAlchemy 内部同样依赖
__setattr__做“脏数据跟踪”。
5. 性能陷阱 & 调试锦囊
| 问题 | 现象 | 解决 |
|---|---|---|
| 无限递归 | RecursionError | __setattr__ 里用 super() 或直接改 __dict__ |
| 性能下降 | 属性访问慢 3× | 缓存计算结果、用 __slots__ 减少字典开销 |
| 调试困难 | 断点进不去 | 在钩子内加 print / logging,或临时关闭自定义方法 |
6. 一张图总结(Mermaid)
graph TD
A[__getattr__] --> B[代理模式]
A --> C[延迟加载]
D[__setattr__] --> E[属性验证]
D --> F[ORM 脏跟踪]
G[__delattr__] --> H[保护只读属性]
7. 结语
__xxattr__ 不是炫技,而是“把本来要写的 100 个 if / try 藏到语言运行时”。
掌握这四大模式,你就能:
- 写出“自带文档”的类(代理)
- 让错误在“赋值瞬间”而不是“运行半小时后”爆掉(验证)
- 把启动时间从 3 s 降到 300 ms(懒加载)
- 用 200 行代码写个能跑业务的“迷你 Django”(ORM)
下次当你敲下 obj.x = y 时,别忘了:背后有一整块魔法世界,等你去点亮。
参考资料
: 《Python 实现类属性的延迟加载装饰器》
: 《Python 性能优化:懒加载与其他高级技巧》
: 《Python 使用 attrs 和 cattrs 实现面向对象编程》
: 《Python3 Cookbook 8.15 属性的代理访问》
: 《浅谈 Python 的元编程》