揭秘Python中的描述符及实战应用

在Python中,描述符是一种强大的面向对象特性,它隐藏在许多内置类型的底层机制中,如类属性、实例属性以及@property装饰器等。理解描述符的工作原理能帮助我们更深入地掌握Python的面向对象编程,并实现诸如数据验证、属性封装等功能丰富的自定义类型。
在这里插入图片描述

一、什么是描述符

描述符是一个实现了__get__(), __set__()__delete__()特殊方法的类。这三个方法分别对应于获取属性值、设置属性值和删除属性值的操作。当一个类的属性是描述符实例时,对这个属性的访问将会调用描述符的方法而非直接访问其存储值。

例如,下面是一个简单的描述符类实现:

class Descriptor:
    def __init__(self, initial_value=None):
        self._value = initial_value

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return self._value

    def __set__(self, instance, value):
        self._value = value

    def __delete__(self, instance):
        del self._value

二、描述符的工作流程

  • 属性获取:当你尝试从一个实例中获取属性时,Python会首先查找该属性是否为描述符。如果是,则调用__get__()方法并返回结果。

  • 属性设置:当你尝试给一个实例的属性赋值时,Python同样先检查该属性是否为描述符,若是,则调用__set__()方法来处理新的值。

  • 属性删除:若要删除属性,Python则会寻找并执行__delete__()方法。

描述符的__get__(), __set__()__delete__()方法都接受三个参数:self(描述符实例本身)、instance(拥有描述符属性的类实例)和owner(拥有描述符属性的类)。其中instance为None表示访问的是类属性而非实例属性。

例如,在__get__()方法中,根据instance是否为None来决定返回描述符自身的引用还是实例的实际值。这种设计允许描述符同时应用于类属性和实例属性。

三、描述符的应用场景

Python 中描述符的应用场景非常广泛,主要用于实现对属性的访问控制、数据验证、懒加载、缓存以及其他自定义行为。

3.1 属性验证

当我们需要确保属性只能设定为某一特定类型或范围时,可以通过重写描述符的 __set__() 方法来实现属性赋值前的验证。

class PositiveNumberDescriptor:
    def __init__(self):
        self.value = None

    def __get__(self, obj, objtype=None):
        return self.value

    def __set__(self, obj, value):
        if not isinstance(value, int) or value < 0:
            raise ValueError("Value must be a non-negative integer")
        self.value = value

class PositiveNumbers:
    positive_number = PositiveNumberDescriptor()

pn = PositiveNumbers()
pn.positive_number = 5  # 正确赋值
pn.positive_number = -3  # 引发 ValueError 错误

3.2 懒加载 (Lazy Loading)

当某个属性的计算成本较高时,我们可以在首次访问时才计算它的值,后续访问则直接返回缓存的结果,只有在属性值改变时才重新计算值。

class CachedDescriptor:
    def __init__(self, compute_value):
        self.compute_value = compute_value
        self.cached_value = None

    def __get__(self, obj, objtype=None):
        if self.cached_value is None:
            self.cached_value = self.compute_value(obj)
        return self.cached_value

    def __set__(self, obj, value):
        self.cached_value = value

class CachedComputation:
    cached_complicated_result = CachedDescriptor(compute_complex_computation)

cc = CachedComputation()
print(cc.cached_complicated_result)  # 首次访问时计算并缓存结果

3.3 属性封装与访问权限控制

描述符可以用来实现私有变量的封装,对外只暴露getter和setter方法。

class EncapsulatedAttributeDescriptor:
    def __init__(self, default=None):
        self._value = default

    def __get__(self, obj, objtype=None):
        return self._value

    def __set__(self, obj, value):
        self._value = value

class MyClass:
    private_attribute = EncapsulatedAttributeDescriptor()

my_instance = MyClass()
my_instance.private_attribute = "secret"  # 设置值
print(my_instance.private_attribute)  # 获取值

3.4 @property 装饰器

Python 的 @property 装饰器本质上就是创建了一个描述符,它允许我们将方法像属性一样调用。

class Person:
    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name

    @property
    def full_name(self):
        return f"{self.first_name} {self.last_name}"

p = Person("Alice", "Smith")
print(p.full_name)  # 使用 property 访问方法如同访问属性

3.5 数据同步与通知机制

描述符还可以用于实现数据同步或触发事件通知。每当属性值发生变化时,可以通过描述符的 __set__() 方法触发相关的更新逻辑或发送通知。

import weakref

class NotifyingDescriptor:
    def __init__(self):
        self.value = None
        self.callbacks = weakref.WeakSet()  # 存储回调函数的弱引用集合

    def __get__(self, obj, objtype=None):
        return self.value

    def __set__(self, obj, value):
        self.value = value
        for callback in self.callbacks:
            callback(obj, value)  # 触发所有注册的回调函数

    def register_callback(self, callback):
        self.callbacks.add(callback)

class ObservableObject:
    data = NotifyingDescriptor()

def on_data_change(obj, new_value):
    print(f"Data changed to {new_value}")

obs_obj = ObservableObject()
obs_obj.data.register_callback(on_data_change)
obs_obj.data = 123  # 改变数据时,触发回调函数

3.6 动态属性调整

描述符可以用于实现动态属性调整,即在运行时更改属性的行为。例如,可以根据环境变化或配置更改来启用或禁用某些属性的读写权限

class DynamicPropertyDescriptor:
    def __init__(self, read_enabled=True, write_enabled=True):
        self.read_enabled = read_enabled
        self.write_enabled = write_enabled
        self.value = None

    def __get__(self, obj, objtype=None):
        if self.read_enabled:
            return self.value
        else:
            raise AttributeError("Reading is disabled")

    def __set__(self, obj, value):
        if self.write_enabled:
            self.value = value
        else:
            raise AttributeError("Writing is disabled")

class DynamicObject:
    dynamic_property = DynamicPropertyDescriptor()

dynamic_obj = DynamicObject()
dynamic_obj.dynamic_property = 10  # 写入值,此时写入功能已启用
print(dynamic_obj.dynamic_property)  # 读取值,此时读取功能也已启用

dynamic_obj.dynamic_property.write_enabled = False  # 禁止写入
try:
    dynamic_obj.dynamic_property = 20  # 尝试写入会引发异常
except AttributeError as e:
    print(e)

dynamic_obj.dynamic_property.read_enabled = False  # 禁止读取
try:
    print(dynamic_obj.dynamic_property)  # 尝试读取会引发异常
except AttributeError as e:
    print(e)

3.7 数据库访问

在ORM(对象关系映射)库中,描述符常被用来处理数据库记录与实体对象之间的映射关系,确保每次访问或修改属性时,相应的数据库操作也被正确执行。

# 这是一个简化的例子,真实情况下ORM库会更复杂
class DatabaseFieldDescriptor:
    def __init__(self, column_name):
        self.column_name = column_name

    def __get__(self, obj, objtype=None):
        # 假设这里是从数据库查询对应列的值
        return obj._db_get_value(self.column_name)

    def __set__(self, obj, value):
        # 假设这里是对数据库中对应列的值进行更新
        obj._db_update_value(self.column_name, value)

class User:
    id = DatabaseFieldDescriptor('id')
    username = DatabaseFieldDescriptor('username')

user = User()
user.username = 'alice'  # 修改用户名时,会同步更新到数据库对应的列

3.8 资源管理

描述符也可以用于管理有限的资源,如文件句柄、网络连接等,确保资源在使用后能得到及时释放。

import contextlib

class ManagedResourceDescriptor:
    def __init__(self):
        self.resource = None

    def __enter__(self):
        self.acquire_resource()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.release_resource()

    def __get__(self, obj, objtype=None):
        if self.resource is None:
            self.acquire_resource()
        return self.resource

    def acquire_resource(self):
        # 假设这里是打开或创建资源的逻辑
        self.resource = open("resource.txt", "r")

    def release_resource(self):
        # 假设这里是关闭或释放资源的逻辑
        if self.resource:
            self.resource.close()
            self.resource = None

class ResourceConsumer:
    managed_resource = ManagedResourceDescriptor()

consumer = ResourceConsumer()
with consumer.managed_resource as resource:
    # 使用资源
    content = resource.read()

# 资源会在with语句块结束时自动释放

四、结论

描述符在Python中扮演着重要角色,是Python中用于控制属性访问的核心机制之一,它为我们提供了精细的属性访问控制能力。通过灵活运用描述符,我们可以实现高级的对象行为,无论是数据验证、缓存机制、依赖关系建立,还是资源管理等方面,描述符都能够提供强大的支持。通过恰当运用描述符,我们可以提升代码的可读性、复用性和效率,特别是在大型软件系统中,这有助于形成更好的架构设计和实现良好的模块化。

在这里插入图片描述


在这里插入图片描述
在这里插入图片描述

往期精彩文章

  1. 好家伙,Python自定义接口,玩得这么花

  2. 哎呀我去,Python多重继承还能这么玩?

  3. 太秀了!Python魔法方法__call__,你试过吗?

  4. Python函数重载6种实现方式,从此告别手写if-else!

  5. 嗷嗷,Python动态创建函数和类,是这么玩的啊

  6. Python混入类Mixin,远比你想象的更强大!

  7. Python -c原来还能这么用,学到了!

  8. Python模块导入,别out了,看看这些高级玩法!

  9. Python定时任务8种实现方式,你喜欢哪种!

  10. python文件:.py,.ipynb, pyi, pyc, pyd, pyo都是什么文件?

  11. Python也能"零延迟"通信吗?ZeroMQ带你开启高速模式!

  12. 掌握Python 这10个OOP技术,代码想写不好都难!

  • 18
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

南风以南

如给您带来些许明朗,赏一杯香茗

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值