配套专栏:Python 全栈修炼之路 第 15 篇《描述符与属性访问控制》难度分布:⭐ → ⭐⭐ → ⭐⭐ → ⭐⭐⭐ → ⭐⭐⭐ → ⭐⭐⭐⭐核心覆盖:__get__/__set__/__delete__、数据/非数据描述符、属性查找优先级、property本质、弱引用存储、ORM 实战前言描述符是 Python 属性访问机制的底层核心,也是property、classmethod、staticmethod以及 Django/SQLAlchemy ORM 的实现基础。本练习精选 6 道编程题,从基础描述符到完整 ORM,层层递进,帮你彻底掌握描述符的精髓。题目一:基础描述符——类型检查与范围限制 ⭐📌 题目描述实现两个基础描述符:类型检查描述符和范围限制描述符:classPerson:name=Typed("name",str)age=Typed("age",int)score=Range("score",0,100)p=Person()p.name="Alice"p.age=25p.score=85print(f"{p.name},{p.age}岁, 分数{p.score}")# Alice, 25岁, 分数85p.age="25"# TypeError: 'age' 必须是 int 类型,而不是 strp.score=101# ValueError: 'score' 必须在 [0, 100] 范围内💡 编程思路这道题考察描述符协议基础:__get__、__set__和__delete__。Typed:在__set__中用isinstance()检查类型,用id(obj)作为 key 在字典中存储每个实例的值,避免所有实例共享同一值。Range:在__set__中检查数值是否在[min, max]范围内。obj is None:在__get__中处理通过类访问的情况(返回描述符自身)。🖥️ 参考代码classTyped:"""类型检查描述符。"""def__init__(self,name:str,expected_type:type):self.name=name self.expected_type=expected_type self._data:dict[int,any]={}def__get__(self,obj,objtype=None):ifobjisNone:returnselfreturnself._data.get(id(obj))def__set__(self,obj,value):ifnotisinstance(value,self.expected_type):raiseTypeError(f"'{self.name}' 必须是{self.expected_type.__name__}类型,"f"而不是{type(value).__name__}")self._data[id(obj)]=valuedef__delete__(self,obj):ifid(obj)inself._data:delself._data[id(obj)]def__repr__(self):returnf"Typed({self.name},{self.expected_type.__name__})"classRange:"""范围限制描述符。"""def__init__(self,name:str,min_value=None,max_value=None):self.name=name self.min_value=min_value self.max_value=max_value self._data:dict[int,any]={}def__get__(self,obj,objtype=None):ifobjisNone:returnselfreturnself._data.get(id(obj))def__set__(self,obj,value):ifself.min_valueisnotNoneandvalueself.min_value:raiseValueError(f"'{self.name}' 不能小于{self.min_value}")ifself.max_valueisnotNoneandvalueself.max_value:raiseValueError(f"'{self.name}' 不能大于{self.max_value}")self._data[id(obj)]=valuedef__delete__(self,obj):ifid(obj)inself._data:delself._data[id(obj)]classReadOnly:"""只读描述符(初始化后不可修改)。"""def__init__(self,name:str,value=None):self.name=name self._data:dict[int,any]={}self._initialized:set[int]=set()def__get__(self,obj,objtype=None):ifobjisNone:returnselfreturnself._data.get(id(obj))def__set__(self,obj,value):ifid(obj)inself._initialized:raiseAttributeError(f"'{self.name}' 是只读属性")self._data[id(obj)]=value self._initialized.add(id(obj))classPerson:name=Typed("name",str)age=Typed("age",int)score=Range("score",0,100)id=ReadOnly("id")def__init__(self,name,age,score,id):self.name=name self.age=age self.score=score self.id=iddef__repr__(self):returnf"Person(name='{self.name}', age={self.age}, score={self.score}, id={self.id})"# 测试if__name__=="__main__":print("=== 基础描述符 ===")p=Person("Alice",25,85,1001)print(p)print("\n=== 类型检查 ===")try:p.age="25"exceptTypeErrorase:print(f"类型错误:{e}")print("\n=== 范围限制 ===")try:p.score=101exceptValueErrorase:print(f"范围错误:{e}")print("\n=== 只读属性 ===")try:p.id=2002exceptAttributeErrorase:print(f"只读错误:{e}")print("\n=== 通过类访问 ===")print(f"Person.name:{Person.name}")print(f"Person.age:{Person.age}")print("\n=== 删除属性 ===")delp.scoreprint(f"删除后 score:{p.score}")print("\n所有测试通过 ✓")🔗 关联知识点知识点说明__get__(self, obj, objtype)获取属性值__set__(self, obj, value)设置属性值__delete__(self, obj)删除属性id(obj)字典存储避免实例间共享obj is None处理类访问数据描述符实现__get__+__set__,优先级最高题目二:数据描述符 vs 非数据描述符 ⭐⭐📌 题目描述通过实验验证数据描述符和非数据描述符的优先级差异:classDataDescriptor:"""数据描述符(__get__ + __set__)"""...classNonDataDescriptor:"""非数据描述符(仅 __get__)"""...classDemo:data_attr=DataDescriptor()non_data_attr=NonDataDescriptor()d=Demo()# 数据描述符:优先级高于实例属性d.data_attr="实例值"# 被描述符拦截print(d.data_attr)# "数据描述符的值"print(d.__dict__)# {}(没有 data_attr)# 非数据描述符:优先级低于实例属性d.non_data_attr="实例值"# 创建实例属性print(d.non_data_attr)# "实例值"(实例属性覆盖了描述符)print(d.__dict__)# {'non_data_attr': '实例值'}💡 编程思路这道题考察属性查找优先级链:数据描述符(__get__+__set__)→ 优先级最高,即使实例有同名属性也优先访问描述符。实例属性(obj.__dict__)→ 优先级次之。非数据描述符(仅__get__)→ 优先级低于实例属性,实例属性可以覆盖它。通过object.__setattr__强制创建实例属性,绕过描述符拦截。🖥️ 参考代码classDataDescriptor:"""数据描述符:__get__ + __set__"""def__init__(self,name):self.name=name self._data:dict[int,any]={}def__get__(self,obj,objtype=None):ifobjisNone:returnselfprint(f" [DataDescriptor.__get__] 访问{self.name}")returnself._data.get(id(obj),f"数据描述符的默认值")def__set__(self,obj,value):print(f" [DataDescriptor.__set__] 设置{self.name}={value!r}")self._data[id(obj)]=valuedef__delete__(self,obj):print(f" [DataDescriptor.__delete__] 删除{self.name}")ifid(obj)inself._data:delself._data[id(obj)]classNonDataDescriptor:"""非数据描述符:仅 __get__"""def__init__(self,name):self.name=namedef__get__(self,obj,objtype=None):ifobjisNone:returnselfprint(f" [NonDataDescriptor.__get__] 访问{self.name}")returnf"非数据描述符的值"classDemo:data_attr=DataDescriptor("data_attr")non_data_attr=NonDataDescriptor("non_data_attr")classPriorityDemo:"""属性查找优先级演示。"""defdemonstrate(self):d=Demo()print("=== 1. 初始访问 ===")print(f"data_attr:{d.data_attr}")print(f"non_data_attr:{d.non_data_attr}")print("\n=== 2. 尝试设置数据描述符 ===")d.data_attr="实例值"print(f"设置后 data_attr:{d.data_attr}")print(f"__dict__:{d.__dict__}")print("\n=== 3. 尝试设置非数据描述符 ===")d.non_data_attr="实例值"print(f"设置后 non_data_attr:{d.non_data_attr}")print(f"__dict__:{d.__dict__}")print("\n=== 4. 强制创建实例属性(绕过数据描述符) ===")object.__setattr__(d,"data_attr","强制实例值"
Python 描述符专项练习:6 道编程题从入门到精通
配套专栏:Python 全栈修炼之路 第 15 篇《描述符与属性访问控制》难度分布:⭐ → ⭐⭐ → ⭐⭐ → ⭐⭐⭐ → ⭐⭐⭐ → ⭐⭐⭐⭐核心覆盖:__get__/__set__/__delete__、数据/非数据描述符、属性查找优先级、property本质、弱引用存储、ORM 实战前言描述符是 Python 属性访问机制的底层核心,也是property、classmethod、staticmethod以及 Django/SQLAlchemy ORM 的实现基础。本练习精选 6 道编程题,从基础描述符到完整 ORM,层层递进,帮你彻底掌握描述符的精髓。题目一:基础描述符——类型检查与范围限制 ⭐📌 题目描述实现两个基础描述符:类型检查描述符和范围限制描述符:classPerson:name=Typed("name",str)age=Typed("age",int)score=Range("score",0,100)p=Person()p.name="Alice"p.age=25p.score=85print(f"{p.name},{p.age}岁, 分数{p.score}")# Alice, 25岁, 分数85p.age="25"# TypeError: 'age' 必须是 int 类型,而不是 strp.score=101# ValueError: 'score' 必须在 [0, 100] 范围内💡 编程思路这道题考察描述符协议基础:__get__、__set__和__delete__。Typed:在__set__中用isinstance()检查类型,用id(obj)作为 key 在字典中存储每个实例的值,避免所有实例共享同一值。Range:在__set__中检查数值是否在[min, max]范围内。obj is None:在__get__中处理通过类访问的情况(返回描述符自身)。🖥️ 参考代码classTyped:"""类型检查描述符。"""def__init__(self,name:str,expected_type:type):self.name=name self.expected_type=expected_type self._data:dict[int,any]={}def__get__(self,obj,objtype=None):ifobjisNone:returnselfreturnself._data.get(id(obj))def__set__(self,obj,value):ifnotisinstance(value,self.expected_type):raiseTypeError(f"'{self.name}' 必须是{self.expected_type.__name__}类型,"f"而不是{type(value).__name__}")self._data[id(obj)]=valuedef__delete__(self,obj):ifid(obj)inself._data:delself._data[id(obj)]def__repr__(self):returnf"Typed({self.name},{self.expected_type.__name__})"classRange:"""范围限制描述符。"""def__init__(self,name:str,min_value=None,max_value=None):self.name=name self.min_value=min_value self.max_value=max_value self._data:dict[int,any]={}def__get__(self,obj,objtype=None):ifobjisNone:returnselfreturnself._data.get(id(obj))def__set__(self,obj,value):ifself.min_valueisnotNoneandvalueself.min_value:raiseValueError(f"'{self.name}' 不能小于{self.min_value}")ifself.max_valueisnotNoneandvalueself.max_value:raiseValueError(f"'{self.name}' 不能大于{self.max_value}")self._data[id(obj)]=valuedef__delete__(self,obj):ifid(obj)inself._data:delself._data[id(obj)]classReadOnly:"""只读描述符(初始化后不可修改)。"""def__init__(self,name:str,value=None):self.name=name self._data:dict[int,any]={}self._initialized:set[int]=set()def__get__(self,obj,objtype=None):ifobjisNone:returnselfreturnself._data.get(id(obj))def__set__(self,obj,value):ifid(obj)inself._initialized:raiseAttributeError(f"'{self.name}' 是只读属性")self._data[id(obj)]=value self._initialized.add(id(obj))classPerson:name=Typed("name",str)age=Typed("age",int)score=Range("score",0,100)id=ReadOnly("id")def__init__(self,name,age,score,id):self.name=name self.age=age self.score=score self.id=iddef__repr__(self):returnf"Person(name='{self.name}', age={self.age}, score={self.score}, id={self.id})"# 测试if__name__=="__main__":print("=== 基础描述符 ===")p=Person("Alice",25,85,1001)print(p)print("\n=== 类型检查 ===")try:p.age="25"exceptTypeErrorase:print(f"类型错误:{e}")print("\n=== 范围限制 ===")try:p.score=101exceptValueErrorase:print(f"范围错误:{e}")print("\n=== 只读属性 ===")try:p.id=2002exceptAttributeErrorase:print(f"只读错误:{e}")print("\n=== 通过类访问 ===")print(f"Person.name:{Person.name}")print(f"Person.age:{Person.age}")print("\n=== 删除属性 ===")delp.scoreprint(f"删除后 score:{p.score}")print("\n所有测试通过 ✓")🔗 关联知识点知识点说明__get__(self, obj, objtype)获取属性值__set__(self, obj, value)设置属性值__delete__(self, obj)删除属性id(obj)字典存储避免实例间共享obj is None处理类访问数据描述符实现__get__+__set__,优先级最高题目二:数据描述符 vs 非数据描述符 ⭐⭐📌 题目描述通过实验验证数据描述符和非数据描述符的优先级差异:classDataDescriptor:"""数据描述符(__get__ + __set__)"""...classNonDataDescriptor:"""非数据描述符(仅 __get__)"""...classDemo:data_attr=DataDescriptor()non_data_attr=NonDataDescriptor()d=Demo()# 数据描述符:优先级高于实例属性d.data_attr="实例值"# 被描述符拦截print(d.data_attr)# "数据描述符的值"print(d.__dict__)# {}(没有 data_attr)# 非数据描述符:优先级低于实例属性d.non_data_attr="实例值"# 创建实例属性print(d.non_data_attr)# "实例值"(实例属性覆盖了描述符)print(d.__dict__)# {'non_data_attr': '实例值'}💡 编程思路这道题考察属性查找优先级链:数据描述符(__get__+__set__)→ 优先级最高,即使实例有同名属性也优先访问描述符。实例属性(obj.__dict__)→ 优先级次之。非数据描述符(仅__get__)→ 优先级低于实例属性,实例属性可以覆盖它。通过object.__setattr__强制创建实例属性,绕过描述符拦截。🖥️ 参考代码classDataDescriptor:"""数据描述符:__get__ + __set__"""def__init__(self,name):self.name=name self._data:dict[int,any]={}def__get__(self,obj,objtype=None):ifobjisNone:returnselfprint(f" [DataDescriptor.__get__] 访问{self.name}")returnself._data.get(id(obj),f"数据描述符的默认值")def__set__(self,obj,value):print(f" [DataDescriptor.__set__] 设置{self.name}={value!r}")self._data[id(obj)]=valuedef__delete__(self,obj):print(f" [DataDescriptor.__delete__] 删除{self.name}")ifid(obj)inself._data:delself._data[id(obj)]classNonDataDescriptor:"""非数据描述符:仅 __get__"""def__init__(self,name):self.name=namedef__get__(self,obj,objtype=None):ifobjisNone:returnselfprint(f" [NonDataDescriptor.__get__] 访问{self.name}")returnf"非数据描述符的值"classDemo:data_attr=DataDescriptor("data_attr")non_data_attr=NonDataDescriptor("non_data_attr")classPriorityDemo:"""属性查找优先级演示。"""defdemonstrate(self):d=Demo()print("=== 1. 初始访问 ===")print(f"data_attr:{d.data_attr}")print(f"non_data_attr:{d.non_data_attr}")print("\n=== 2. 尝试设置数据描述符 ===")d.data_attr="实例值"print(f"设置后 data_attr:{d.data_attr}")print(f"__dict__:{d.__dict__}")print("\n=== 3. 尝试设置非数据描述符 ===")d.non_data_attr="实例值"print(f"设置后 non_data_attr:{d.non_data_attr}")print(f"__dict__:{d.__dict__}")print("\n=== 4. 强制创建实例属性(绕过数据描述符) ===")object.__setattr__(d,"data_attr","强制实例值"