描述符协议:精确控制属性访问

点击上方“Python爬虫与数据挖掘”,进行关注

回复“书籍”即可获赠Python从入门到进阶共10本电子书

江东子弟多才俊,卷土重来未可知。——杜牧《题乌江亭》


图片

作者:Python进阶者

关键词:Python描述符, 属性访问控制, getsetdelete, 属性描述符

开头引言

大家好,我是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学习群请在后台回复【入群

万水千山总是情,点个【在看】行不行

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值