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

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

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

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

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 时,别忘了:背后有一整块魔法世界,等你去点亮。


本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:alixiixcom@163.com