点击上方“Python爬虫与数据挖掘”,进行关注
回复“书籍”即可获赠Python从入门到进阶共10本电子书
今
日
鸡
汤
江东子弟多才俊,卷土重来未可知。——杜牧《题乌江亭》

作者:Python进阶者
关键词:Python描述符, 属性访问控制, get, set, delete, 属性描述符

开头引言:
大家好,我是Python进阶者。今天我们要深入探讨Python中一个强大但常被忽视的特性——描述符(Descriptor)。你是否曾经想要精细控制对象的属性访问?或者想要实现类似ORM中的字段验证?描述符正是解决这些问题的终极武器。虽然它看起来有些复杂,但一旦掌握,你将拥有前所未有的属性控制能力。
一、什么是描述符?
描述符是一个实现了特定协议(__get__, __set__, __delete__)的对象,它能够控制对其他对象属性的访问。
1.1 描述符协议
一个完整的描述符需要实现以下一个或多个方法:
classDescriptor: def__get__(self, instance, owner): """获取属性值时调用""" pass def__set__(self, instance, value): """设置属性值时调用""" pass def__delete__(self, instance): """删除属性时调用""" pass
1.2 描述符的类型
根据实现的方法,描述符分为两类:
- •
数据描述符:实现了
__set__或__delete__ - •
非数据描述符:只实现了
__get__
二、描述符的基本用法
2.1 最简单的只读描述符
classReadOnlyDescriptor: """只读描述符示例""" def__init__(self, initial_value): self._value = initial_value def__get__(self, instance, owner): returnself._value def__set__(self, instance, value): raise AttributeError("只读属性,不能修改") classMyClass: read_only_attr = ReadOnlyDescriptor("初始值") obj = MyClass() print(obj.read_only_attr) # 初始值 # obj.read_only_attr = "新值" # 会抛出 AttributeError
2.2 完整的读写描述符
classSimpleDescriptor: """简单的读写描述符""" def__init__(self, name): self.name = name self._values = {} # 存储每个实例的值 def__get__(self, instance, owner): if instance isNone: returnself returnself._values.get(id(instance)) def__set__(self, instance, value): self._values[id(instance)] = value def__delete__(self, instance): ifid(instance) inself._values: delself._values[id(instance)] classPerson: name = SimpleDescriptor("name") age = SimpleDescriptor("age") # 使用示例 p1 = Person() p1.name = "Alice" p1.age = 25 p2 = Person() p2.name = "Bob" p2.age = 30 print(p1.name, p1.age) # Alice 25 print(p2.name, p2.age) # Bob 30
三、描述符的实际应用场景
3.1 类型验证描述符
classTypedDescriptor: """类型验证描述符""" def__init__(self, name, expected_type): self.name = name self.expected_type = expected_type self._values = {} def__get__(self, instance, owner): if instance isNone: returnself returnself._values.get(id(instance)) def__set__(self, instance, value): ifnotisinstance(value, self.expected_type): raise TypeError(f"{self.name} 必须是 {self.expected_type.__name__} 类型") self._values[id(instance)] = value def__delete__(self, instance): ifid(instance) inself._values: delself._values[id(instance)] classUser: name = TypedDescriptor("name", str) age = TypedDescriptor("age", int) score = TypedDescriptor("score", (int, float)) # 支持多种类型 # 测试 user = User() user.name = "Alice"# 正确 user.age = 25# 正确 user.score = 95.5# 正确 try: user.age = "25"# 会抛出 TypeError except TypeError as e: print(f"错误: {e}")
3.2 延迟加载描述符
import time classLazyLoadDescriptor: """延迟加载描述符""" def__init__(self, func): self.func = func self._values = {} self._cache = {} def__get__(self, instance, owner): if instance isNone: returnself instance_id = id(instance) if instance_id notinself._cache: # 模拟耗时的计算或加载过程 print(f"计算 {self.func.__name__}...") time.sleep(1) # 模拟耗时操作 self._cache[instance_id] = self.func(instance) returnself._cache[instance_id] def__set__(self, instance, value): # 允许手动设置值,并清除缓存 instance_id = id(instance) self._values[instance_id] = value if instance_id inself._cache: delself._cache[instance_id] classDataProcessor: def__init__(self, data): self.data = data @LazyLoadDescriptor defprocessed_data(self): """模拟耗时的数据处理""" return [x * 2for x inself.data] # 使用示例 processor = DataProcessor([1, 2, 3, 4, 5]) print("第一次访问:") result1 = processor.processed_data # 会计算 print("第二次访问:") result2 = processor.processed_data # 从缓存获取 print("结果:", result1, result2) print("是否相同:", result1 is result2)
3.3 观察者模式描述符
classObservableDescriptor: """观察者模式描述符""" def__init__(self, name): self.name = name self._values = {} self._observers = {} # 存储每个实例的观察者 def__get__(self, instance, owner): if instance isNone: returnself returnself._values.get(id(instance)) def__set__(self, instance, value): old_value = self._values.get(id(instance)) self._values[id(instance)] = value # 通知观察者 ifid(instance) inself._observers: for callback inself._observers[id(instance)]: callback(old_value, value) defadd_observer(self, instance, callback): """添加观察者""" ifid(instance) notinself._observers: self._observers[id(instance)] = [] self._observers[id(instance)].append(callback) defremove_observer(self, instance, callback): """移除观察者""" ifid(instance) inself._observers: if callback inself._observers[id(instance)]: self._observers[id(instance)].remove(callback) classSettings: theme = ObservableDescriptor("theme") language = ObservableDescriptor("language") deftheme_changed(old_val, new_val): print(f"主题从 {old_val} 变为 {new_val}") deflanguage_changed(old_val, new_val): print(f"语言从 {old_val} 变为 {new_val}") # 使用示例 settings = Settings() settings.theme.add_observer(settings, theme_changed) settings.language.add_observer(settings, language_changed) settings.theme = "dark"# 会触发通知 settings.language = "zh"# 会触发通知 settings.theme = "light"# 会触发通知
四、描述符在框架中的应用
4.1 类似Django的ORM字段
classORMField: """类似Django的ORM字段描述符""" def__init__(self, field_type, default=None, null=False): self.field_type = field_type self.default = default self.null = null self._values = {} def__get__(self, instance, owner): if instance isNone: returnself instance_id = id(instance) if instance_id notinself._values: ifself.default isnotNone: returnself.default ifnotself.null: raise ValueError("该字段不能为空") returnNone returnself._values[instance_id] def__set__(self, instance, value): if value isNoneandnotself.null: raise ValueError("该字段不能为null") if value isnotNoneandnotisinstance(value, self.field_type): raise TypeError(f"必须是 {self.field_type.__name__} 类型") self._values[id(instance)] = value classModel: """基模型""" def__init__(self, **kwargs): for field_name inself._get_fields(): value = kwargs.get(field_name) if value isnotNone: setattr(self, field_name, value) def_get_fields(self): """获取所有描述符字段""" return [name for name indir(self.__class__) ifisinstance(getattr(self.__class__, name), ORMField)] def__repr__(self): fields = self._get_fields() field_values = {f: getattr(self, f) for f in fields} returnf"{self.__class__.__name__}({field_values})" classUser(Model): name = ORMField(str, null=False) age = ORMField(int, default=0) email = ORMField(str, null=True) # 使用示例 user1 = User(name="Alice", age=25) user2 = User(name="Bob", email="bob@example.com") print(user1) # User({'name': 'Alice', 'age': 25, 'email': None}) print(user2) # User({'name': 'Bob', 'age': 0, 'email': 'bob@example.com'}) try: user3 = User() # 会报错,因为name不能为null except ValueError as e: print(f"错误: {e}")
4.2 属性访问控制
classProtectedDescriptor: """受保护的属性描述符""" def__init__(self, name, read_only=False): self.name = name self.read_only = read_only self._values = {} def__get__(self, instance, owner): if instance isNone: returnself returnself._values.get(id(instance)) def__set__(self, instance, value): ifself.read_only: raise AttributeError(f"{self.name} 是只读属性") self._values[id(instance)] = value def__delete__(self, instance): raise AttributeError(f"不能删除 {self.name}") classConfig: api_key = ProtectedDescriptor("api_key", read_only=True) timeout = ProtectedDescriptor("timeout") # 使用示例 config = Config() config.timeout = 30# 正常设置 # 配置只读属性(通常在初始化时设置) Config.api_key._values[id(config)] = "secret-key" print(config.api_key) # secret-key # config.api_key = "new-key" # 会报错 # del config.timeout # 会报错
五、描述符与属性查找顺序
理解Python的属性查找顺序对于掌握描述符至关重要:
classDemo: def__init__(self): self.instance_attr = "实例属性" @property defcomputed_attr(self): return"计算属性" class_attr = "类属性" classDataDescriptor: """数据描述符""" def__get__(self, instance, owner): return"数据描述符" def__set__(self, instance, value): pass classNonDataDescriptor: """非数据描述符""" def__get__(self, instance, owner): return"非数据描述符" classTestClass: data_desc = DataDescriptor() non_data_desc = NonDataDescriptor() obj = TestClass() obj.instance_attr = "实例属性值" # 属性查找顺序: # 1. 数据描述符 (最高优先级) # 2. 实例属性 # 3. 非数据描述符 # 4. 类属性 # 5. __getattr__ (如果存在) print(obj.data_desc) # 数据描述符 (数据描述符) print(obj.instance_attr) # 实例属性值 (实例属性) print(obj.non_data_desc) # 非数据描述符 (非数据描述符)
六、最佳实践和注意事项
6.1 使用weakref避免内存泄漏
import weakref classSafeDescriptor: """使用weakref避免内存泄漏的描述符""" def__init__(self): self._data = weakref.WeakKeyDictionary() def__get__(self, instance, owner): if instance isNone: returnself returnself._data.get(instance) def__set__(self, instance, value): self._data[instance] = value classMyClass: attr = SafeDescriptor() obj = MyClass() obj.attr = "value" print(obj.attr) # value # 当obj被垃圾回收时,_data中的对应条目会自动清除
6.2 描述符的调试技巧
classDebugDescriptor: """带调试功能的描述符""" def__init__(self, name): self.name = name self._values = {} def__get__(self, instance, owner): print(f"获取 {self.name}") if instance isNone: returnself returnself._values.get(id(instance)) def__set__(self, instance, value): print(f"设置 {self.name} = {value}") self._values[id(instance)] = value def__delete__(self, instance): print(f"删除 {self.name}") ifid(instance) inself._values: delself._values[id(instance)] classDebugClass: attr = DebugDescriptor("attr") obj = DebugClass() obj.attr = "test"# 输出: 设置 attr = test value = obj.attr # 输出: 获取 attr
6.3 描述符与继承
classBaseDescriptor: def__init__(self, name): self.name = name self._values = {} def__get__(self, instance, owner): returnself._values.get(id(instance), f"默认{self.name}") def__set__(self, instance, value): self._values[id(instance)] = value classBaseClass: attr = BaseDescriptor("attr") classChildClass(BaseClass): pass# 继承描述符 obj1 = BaseClass() obj2 = ChildClass() obj1.attr = "基类值" obj2.attr = "子类值" print(obj1.attr) # 基类值 print(obj2.attr) # 子类值
七、描述符与装饰器的结合
from functools import wraps classMethodDescriptor: """方法描述符,用于方法装饰""" def__init__(self, func): self.func = func wraps(func)(self) def__get__(self, instance, owner): if instance isNone: returnself.func returnself.func.__get__(instance, owner) def__call__(self, *args, **kwargs): returnself.func(*args, **kwargs) defmethod_decorator(func): """将普通函数转换为方法描述符""" return MethodDescriptor(func) classMyClass: @method_decorator defmy_method(self): return"方法调用" obj = MyClass() print(obj.my_method()) # 方法调用
八、总结
描述符是Python中极其强大的工具,它们提供了对属性访问的精细控制:
主要优势:
- •
✅ 精确控制属性访问、设置和删除
- •
✅ 实现类型验证和约束
- •
✅ 支持延迟加载和缓存
- •
✅ 实现观察者模式和事件通知
- •
✅ 构建框架级别的功能(如ORM)
适用场景:
- •
需要验证或转换属性值时
- •
需要延迟计算或缓存属性时
- •
需要观察属性变化时
- •
构建框架或DSL时
注意事项:
- •
注意内存泄漏问题(使用weakref)
- •
理解属性查找顺序
- •
避免过度使用,保持代码简洁
互动话题:你在什么项目中用过描述符?遇到过什么有趣的挑战或应用场景?欢迎在评论区分享你的描述符使用经验!
下一篇预告:《生成器与协程:从yield到async/await的进化》 - 我们将探索Python中生成器和协程的发展历程,从基础的yield语句到现代的async/await语法。
【创作声明】
本文的核心大纲和部分基础内容由AI辅助生成,但包含了大量笔者的个人实践经验、独家案例和深度解读。所有配图均为笔者定制化AI生成/制作。旨在为大家提供最直观易懂的教程。感谢AI工具提升了我的创作效率。转载请注明出处。欢迎分享和关注,获取更多Python技术干货!
【提问补充】温馨提示,大家在群里提问的时候。可以注意下面几点:如果涉及到大文件数据,可以数据脱敏后,发点demo数据来(小文件的意思),然后贴点代码(可以复制的那种),记得发报错截图(截全)。代码不多的话,直接发代码文字即可,代码超过50行这样的话,发个.py文件就行。
大家在学习过程中如果有遇到问题,欢迎随时联系我解决(我的微信:2584914241),应粉丝要求,我创建了一些高质量的Python付费学习交流群和付费接单群,欢迎大家加入我的Python学习交流群和接单群!
小伙伴们,快快用实践一下吧!如果在学习过程中,有遇到任何问题,欢迎加我好友,我拉你进Python学习交流群共同探讨学习。

------------------- End -------------------
往期精彩文章推荐:

欢迎大家点赞,留言,转发,转载,感谢大家的相伴与支持
想加入Python学习群请在后台回复【入群】
万水千山总是情,点个【在看】行不行

被折叠的 条评论
为什么被折叠?



