
简介这份PDF资料聚焦Python中四个常用魔法方法——setattr、getattr、delattr__与__call面向已掌握Python基础语法、希望深入理解对象属性访问机制与可调用对象设计的中级开发者。内容围绕属性设置、缺失属性拦截、属性删除以及实例像函数一样被调用等场景展开结合代码示例说明如何重写这些方法以实现动态属性、数据校验与定制化行为并延伸至getattr工厂模式、装饰器与类函数对象等实际用法。资源包共1个PDF文件约40KB篇幅精炼便于快速查阅与对照练习。目前已有670人学习适合在阅读源码或设计类结构时遇到属性控制、动态派发等问题时参考帮助读者把魔法方法从语法认知转化为可落地的编码技巧。1. 属性拦截四件套为什么你写的__getattr__有时候根本不触发如果你写过 Python 的 ORM、配置中心、RPC 代理或者序列化框架大概率绕不开__setattr__、__getattr__、__delattr__、__call__这四个方法。它们常被叫做「魔术方法」但更准确的说法是「属性访问与调用行为的拦截钩子」。问题在于很多人第一次用就翻车明明定义了__getattr__访问已有属性时它却不执行明明写了__setattr__初始化时却报RecursionError。这不是玄学是 Python 属性查找协议在按自己的规则走。这份用法示例资源核心价值就是把四个钩子的触发时机、调用顺序、互相配合的边界讲清楚并给出可直接抄的代码骨架。它适合两类人一类是正在写框架、需要动态代理属性的开发者另一类是被__getattr__和__getattribute__搞混、想彻底理清属性查找链的进阶学习者。下面我按「先立规则、再动手、最后排坑」的顺序拆一遍代码都能直接跑。2. 四个钩子的触发规则从属性查找链到调用协议2.1__getattr__与__getattribute__的分工Python 访问obj.x时真正执行的是type(obj).__getattribute__(obj, x)。这个方法负责查找实例字典、类字典、描述符等。只有当它抛出AttributeError时解释器才会退一步去调用__getattr__。所以__getattr__是「兜底」不是「首选」。class Demo: def __init__(self): self.existing 1 def __getattr__(self, name): # 只有常规查找失败才会走到这里 print(f__getattr__ 兜底: {name}) return fdefault_{name} d Demo() print(d.existing) # 1不会触发 __getattr__ print(d.missing) # 触发 __getattr__输出 default_missing逻辑说明existing在实例字典里能找到__getattribute__直接返回__getattr__没机会执行。missing找不到抛AttributeError才轮到__getattr__。参数name是字符串形式的属性名返回值就是这次访问的结果。如果你把__getattr__写成无条件返回那所有不存在的属性都会「假装存在」调试时会很难受。2.2__setattr__的递归陷阱与正确写法__setattr__拦截所有属性赋值包括__init__里的self.x 1。如果你在里面直接写self.name value就会再次触发__setattr__无限递归直到RecursionError。正确做法是调用父类的__setattr__或直接操作__dict__。class SafeSet: def __setattr__(self, name, value): # 白名单校验非法属性名直接拒绝 if name.startswith(_): raise AttributeError(f不允许设置私有属性: {name}) print(f设置 {name} {value}) super().__setattr__(name, value) # 交给 object 处理避免递归 s SafeSet() s.age 18 # 正常 try: s._secret 1 # 被拦截 except AttributeError as e: print(e)逻辑说明super().__setattr__是object.__setattr__它会把值写进实例__dict__不会再次触发自定义的__setattr__。参数name是属性名字符串value是赋的值。常见变体是用self.__dict__[name] value效果类似但super()更稳妥能兼容带__slots__的类。2.3__delattr__的清理时机__delattr__在del obj.x时触发。它和__setattr__一样容易递归因为del self.x会再次进入__delattr__。典型用途是资源释放前的校验比如禁止删除关键字段。class Protected: def __init__(self): self.id 1 self.cache {} def __delattr__(self, name): if name id: raise AttributeError(id 不允许删除) print(f删除 {name}) super().__delattr__(name) p Protected() del p.cache # 正常 try: del p.id # 被拦截 except AttributeError as e: print(e)逻辑说明super().__delattr__(name)真正从__dict__移除属性。参数name是待删除属性名。注意如果属性本来就不存在super().__delattr__会抛AttributeError你可以在自定义逻辑里先判断hasattr但hasattr本身又会触发__getattr__要小心连锁反应。2.4__call__让实例变成可调用对象__call__和属性拦截不是一类但它常和前面三个配合使用比如实现一个「可调用的配置代理」。定义了__call__的实例可以像函数一样加括号执行。class Adder: def __init__(self, base): self.base base def __call__(self, x, y0): return self.base x y add Adder(10) print(add(5)) # 15 print(add(5, 2)) # 17 print(callable(add)) # True逻辑说明__call__的参数完全自定义self是实例本身。callable(add)返回True说明实例可调用。常见坑是把__call__写成类方法或静态方法那样调用行为会不符合预期。3. 组合实战用四个钩子搭一个带校验的配置对象3.1 需求拆解与类骨架假设我们要一个配置对象属性只能设置预设白名单内的键读取不存在的键时返回默认值而不是报错删除键时记录日志对象本身还能像函数一样被调用返回当前配置快照。这个场景把四个钩子全用上了。class Config: ALLOWED {host, port, timeout} def __init__(self, **kwargs): # 先初始化内部字典避免 __setattr__ 拦截 object.__setattr__(self, _data, {}) object.__setattr__(self, _log, []) for k, v in kwargs.items(): setattr(self, k, v) def __setattr__(self, name, value): if name not in self.ALLOWED: raise AttributeError(f不允许的配置项: {name}) self._log.append(fset {name}{value}) self._data[name] value def __getattr__(self, name): # 只有 _data 里没有才走到这里 if name in self.ALLOWED: return None raise AttributeError(name) def __delattr__(self, name): if name not in self._data: raise AttributeError(name) self._log.append(fdel {name}) del self._data[name] def __call__(self): return dict(self._data)逻辑说明__init__里用object.__setattr__直接写_data和_log绕开自定义__setattr__否则白名单校验会把内部属性也拦掉。__setattr__只允许ALLOWED里的键写入_data并记日志。__getattr__对白名单内但未设置的键返回None其他键抛AttributeError。__delattr__从_data删除并记日志。__call__返回配置快照。3.2 运行验证与输出解读c Config(hostlocalhost, port8080) print(c.host) # localhost print(c.timeout) # None白名单内但未设置 print(c()) # {host: localhost, port: 8080} c.timeout 30 print(c()) # 多了 timeout del c.port print(c()) # port 被删除 try: c.debug True # 不在白名单 except AttributeError as e: print(e) # 不允许的配置项: debug逻辑说明c.timeout访问时_data里没有触发__getattr__因为timeout在ALLOWED里返回None。c.debug True被__setattr__拦截。整个对象还能c()调用拿到当前配置。参数层面ALLOWED是类属性所有实例共享如果要做实例级白名单得在__init__里单独存一份。3.3 和__getattribute__的边界对比有人会问为什么不直接重写__getattribute__因为它的拦截范围太大连self._data这种内部访问都会走你的逻辑稍不注意就递归或性能下降。__getattr__只在找不到时触发适合做默认值兜底。常见做法是需要统一拦截所有读取时用__getattribute__需要兜底时用__getattr__。两者同时存在时__getattribute__先执行它抛AttributeError后__getattr__才接手。钩子触发时机典型用途递归风险__getattr__常规查找失败后默认值、动态代理低__getattribute__每次属性读取全局拦截、审计高__setattr__每次属性赋值校验、日志高__delattr__每次属性删除保护、清理高__call__实例加括号调用工厂、代理无4. 避坑排查四个钩子最容易翻车的五个场景4.1 现象__getattr__对已有属性不生效原因__getattr__只在__getattribute__抛AttributeError后调用。属性已存在时查找成功不会走到兜底。解决如果确实要拦截所有读取改用__getattribute__并在其中显式调用super().__getattribute__(name)同时用try/except AttributeError包住避免破坏正常查找。4.2 现象__setattr__里赋值导致RecursionError原因在__setattr__内部写self.x value会再次触发自身。解决用super().__setattr__(name, value)或object.__setattr__(self, name, value)。如果类有__slots__object.__setattr__同样适用但属性名必须在__slots__里。4.3 现象hasattr判断结果和预期相反原因hasattr(obj, name)内部调用getattr如果__getattr__对任意名字都返回一个值而不抛异常hasattr永远返回True。解决在__getattr__里对不认识的属性明确raise AttributeError(name)不要无脑返回None或默认对象。4.4 现象__delattr__删除不存在的属性时报错信息混乱原因super().__delattr__(name)在属性不存在时抛AttributeError如果你在自定义逻辑里先做了其他操作异常堆栈会指向你的代码。解决删除前用if name not in self.__dict__: raise AttributeError(name)做前置判断保持异常语义清晰。4.5 现象__call__和__init__参数混淆原因__init__在实例创建时执行一次__call__在每次obj()时执行。有人把初始化逻辑写进__call__导致重复初始化。解决__init__只做一次性的状态准备__call__做每次调用的业务逻辑。如果实例需要可调用且每次调用有独立状态考虑在__call__里创建局部变量或返回新对象。5. 进阶技巧用__getattr__做惰性加载与__call__做链式代理5.1 惰性加载把重资源推迟到第一次访问实际项目里配置对象可能关联数据库连接、大文件句柄。用__getattr__做惰性加载只有真正访问时才初始化。class LazyLoader: def __init__(self): object.__setattr__(self, _loaded, {}) def __getattr__(self, name): if name heavy: print(首次加载 heavy 资源) value {data: list(range(1000))} self._loaded[name] value return value raise AttributeError(name) def __setattr__(self, name, value): self._loaded[name] value def __call__(self, key): return self._loaded.get(key, not loaded) ll LazyLoader() print(ll.heavy[data][:3]) # 触发加载 print(ll(heavy)) # 通过 __call__ 查询逻辑说明__getattr__里判断name heavy才加载加载后写入_loaded下次访问直接命中__dict__不再触发__getattr__。__call__提供另一种查询入口。参数key是字符串返回已加载的值或提示。注意_loaded本身用object.__setattr__初始化避免递归。5.2 链式代理__call__返回自身实现连续调用class Chain: def __init__(self): self._steps [] def __call__(self, step): self._steps.append(step) return self # 返回自身支持连续调用 def __getattr__(self, name): if name result: return - .join(self._steps) raise AttributeError(name) c Chain() print(c(read)(parse)(write).result) # read - parse - write逻辑说明__call__每次追加步骤并返回self所以可以连续加括号。__getattr__提供result属性汇总。参数step是任意步骤标识。这种模式在流式 API、查询构建器里很常见。坑在于返回self会让所有中间结果都指向同一实例如果需要不可变链得每次返回新对象。5.3 验证清单与我的习惯每次写完这四个钩子我会强制走一遍检查__getattr__是否对未知属性抛AttributeError__setattr__是否用super()或object.__setattr____delattr__是否处理了不存在的情况__call__是否和__init__职责分离hasattr、getattr默认值、copy、pickle是否还能正常工作。尤其是pickle它依赖__reduce_ex__和__getstate__如果__getattr__拦截了这些内部方法名反序列化会直接失败。从那以后我每次加属性钩子都会先跑一遍pickle.loads(pickle.dumps(obj))和copy.deepcopy(obj)确认没被自己的兜底逻辑带偏。希望帮到你。本文还有配套的精品资源点击获取