大家好,我是正在实战各种 AI 项目的程序员晚枫。
你有没有见过这样的代码:obj.some_property 看起来像属性访问,但背后其实执行了计算? 或者 obj.load_user 请求一个属性,对象自动帮你从数据库加载?这些都是动态属性和特性的功劳。今天咱们把 __getattr__、__setattr__、__slots__ 这些魔法方法一网打尽。
🎯 getattr 和 __getattribute__:属性访问的拦截器 两者的关键区别 方法 触发时机 使用场景 __getattr__属性不存在时 延迟加载、动态属性 __getattribute__每次访问属性时 属性访问监控、权限控制
__getattr__:延迟加载 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class LazyObject : """延迟加载属性 —— 访问时才创建""" def __init__ (self ): self._data = {} def __getattr__ (self, name ): """属性不存在时调用""" if name.startswith('load_' ): key = name[5 :] value = f"Loaded {key} " setattr (self, name, value) return value raise AttributeError( f"'{self.__class__.__name__} ' has no attribute '{name} '" ) obj = LazyObject() print (obj.load_user) print (obj.load_user)
__getattribute__:属性访问的全局监控 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class MonitoredObject : """监控所有属性访问""" def __init__ (self, **kwargs ): for key, value in kwargs.items(): setattr (self, key, value) self._access_log = [] def __getattribute__ (self, name ): if name.startswith('_' ): return super ().__getattribute__(name) access_log = super ().__getattribute__('_access_log' ) access_log.append(name) return super ().__getattribute__(name) obj = MonitoredObject(name="Alice" , age=30 ) print (obj.name) print (obj.age) print (obj._access_log)
⚠️ 注意 :__getattribute__ 中必须用 super().__getattribute__() 访问属性,否则会无限递归!
✏️ setattr 和 __delattr__:属性修改和删除的拦截 __setattr__:控制属性赋值 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class ValidatedObject : """带验证的属性赋值""" def __setattr__ (self, name, value ): if name == 'age' and not isinstance (value, int ): raise TypeError("age 必须是整数" ) if name == 'age' and value < 0 : raise ValueError("age 不能为负数" ) if name == 'email' and '@' not in str (value): raise ValueError("email 格式不正确" ) super ().__setattr__(name, value) obj = ValidatedObject() obj.name = "Alice" obj.age = 30
__delattr__:控制属性删除 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class ProtectedObject : """受保护的属性删除""" def __init__ (self ): self.name = "Alice" self._internal = "secret" def __delattr__ (self, name ): if name.startswith('_' ): raise AttributeError(f"不能删除内部属性: {name} " ) super ().__delattr__(name) obj = ProtectedObject() del obj.name
💾 __slots__:内存优化的利器 为什么需要 __slots__? Python 对象默认用 __dict__ 存储属性,这是一个哈希表,占用大量内存。当你创建百万级实例时,内存浪费非常惊人。__slots__ 用固定数组替代哈希表,大幅节省内存。
基础用法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class Person : def __init__ (self, name, age, email ): self.name = name self.age = age self.email = email class EfficientPerson : __slots__ = ['name' , 'age' , 'email' ] def __init__ (self, name, age, email ): self.name = name self.age = age self.email = email p1 = Person("Alice" , 30 , "alice@example.com" ) p2 = EfficientPerson("Alice" , 30 , "alice@example.com" ) print (p1.__dict__) import sysprint (f"普通类: {sys.getsizeof(p1.__dict__)} 字节 + 对象 {sys.getsizeof(p1)} 字节" )print (f"__slots__: 对象 {sys.getsizeof(p2)} 字节(无 __dict__)" )
批量创建的性能对比 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import sysimport timeclass NormalPoint : def __init__ (self, x, y ): self.x = x self.y = y class SlotPoint : __slots__ = ['x' , 'y' ] def __init__ (self, x, y ): self.x = x self.y = y N = 1_000_000 start = time.time() normal_points = [NormalPoint(i, i*2 ) for i in range (N)] normal_time = time.time() - start normal_size = sum (sys.getsizeof(p) + sys.getsizeof(p.__dict__) for p in normal_points[:100 ]) start = time.time() slot_points = [SlotPoint(i, i*2 ) for i in range (N)] slot_time = time.time() - start slot_size = sum (sys.getsizeof(p) for p in slot_points[:100 ]) print (f"普通类: 创建耗时 {normal_time:.2 f} s, 估算100个实例内存 {normal_size/1024 :.1 f} KB" )print (f"__slots__: 创建耗时 {slot_time:.2 f} s, 估算100个实例内存 {slot_size/1024 :.1 f} KB" )print (f"内存节省: {(1 - slot_size/normal_size)*100 :.1 f} %" )
slots 的注意事项1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 class Base : __slots__ = ['a' ] class Child (Base ): __slots__ = ['b' ] c = Child() c.a = 1 c.b = 2 class BadChild (Base ): pass bc = BadChild() bc.a = 1 bc.extra = "动态属性" class Flexible : __slots__ = ['a' , '__dict__' ] f = Flexible() f.a = 1 f.extra = "动态"
🏗️ 实战案例:动态 ORM 模型 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 class Model : """简易 ORM 模型基类""" __slots__ = ['_fields' , '_dirty' ] def __init__ (self, **kwargs ): object .__setattr__(self, '_fields' , {}) object .__setattr__(self, '_dirty' , set ()) for key, value in kwargs.items(): self._fields[key] = value def __getattr__ (self, name ): if name in self._fields: return self._fields[name] raise AttributeError(f"'{self.__class__.__name__} ' 没有字段 '{name} '" ) def __setattr__ (self, name, value ): if name.startswith('_' ): object .__setattr__(self, name, value) else : self._fields[name] = value self._dirty.add(name) def save (self ): """保存脏数据""" if not self._dirty: print ("没有需要保存的数据" ) return print (f"保存字段: {self._dirty} " ) self._dirty.clear() def to_dict (self ): return dict (self._fields) class User (Model ): pass user = User(name="Alice" , age=30 , email="alice@example.com" ) print (user.name) print (user.age) user.age = 31 user.save() print (user.to_dict())
🎯 本讲总结 getattr :属性不存在时触发,适合延迟加载和动态属性。缓存后不再触发。
getattribute :每次属性访问都触发,适合全局监控。小心无限递归!
setattr / delattr :拦截属性赋值和删除,适合验证和权限控制。
slots :用固定数组替代 __dict__,百万级实例可节省 40%+ 内存。但牺牲了动态添加属性的灵活性。
实战场景 :ORM 模型、延迟加载、属性验证、内存优化。
📚 推荐教材 《Python 编程从入门到实践(第 3 版)》 | 《流畅的 Python(第 2 版)》 | 《CPython 设计与实现》
学习路线: 零基础 → 《从入门到实践》 → 《流畅的 Python》 → 本门课程 → 《CPython 设计与实现》
🎓 加入《流畅的 Python》直播共读营 学到这里,如果你想系统吃透这本书——欢迎加入我的直播共读课。
每周直播精讲,逐章拆解核心知识点 专属学习群,随时答疑交流 试运营特惠:499 元 → 299 元 👉 【立即报名《流畅的 Python》共读课】 :https://mp.weixin.qq.com/s/ivHJwn1nNx5ug4TFrapvGg
🔗 课程导航 ← 上一讲:描述符 | 下一讲:类元编程 →
💬 联系我 主营业务 :AI 编程培训、企业内训、技术咨询
🎓 AI 编程实战课程 想系统学习 AI 编程?程序员晚枫的 AI 编程实战课 帮你从零上手!