火柴人武林大会
156.74M · 2026-02-04
__xxattr__在 Python 里,点语法(obj.x)看似平淡无奇,背后却藏着一套可插拔的“钩子”——
__getattr__ / __setattr__ / __delattr__。
它们就像“魔法插槽”,能让你在“属性被触碰”的瞬间植入任意逻辑:查日志、做校验、走网络、懒加载……
本文用“四大模式”带你把官方文档里干巴巴的定义变成能跑、能改、能上线的产品级代码。
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)
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
__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
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(...)'
__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
表 → 类,行 → 实例,字段 → 属性。
利用 __getattr__ / __setattr__ 把对属性的访问翻译成 SQL。
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
| 问题 | 现象 | 解决 |
|---|---|---|
| 无限递归 | RecursionError | __setattr__ 里用 super() 或直接改 __dict__ |
| 性能下降 | 属性访问慢 3× | 缓存计算结果、用 __slots__ 减少字典开销 |
| 调试困难 | 断点进不去 | 在钩子内加 print / logging,或临时关闭自定义方法 |
graph TD
A[__getattr__] --> B[代理模式]
A --> C[延迟加载]
D[__setattr__] --> E[属性验证]
D --> F[ORM 脏跟踪]
G[__delattr__] --> H[保护只读属性]
__xxattr__ 不是炫技,而是“把本来要写的 100 个 if / try 藏到语言运行时”。
掌握这四大模式,你就能:
下次当你敲下 obj.x = y 时,别忘了:背后有一整块魔法世界,等你去点亮。