Python的描述符

深入理解Python描述符

描述符

一、什么是描述符

   Python为开发者提供了一个非常强大的功能——描述符。那什么是描述符呢?通过查Python的官方文档,我们知道把实现了__get__()、__set__()和__delete__()中的其中任意一种方法的类称之为描述符,描述符的本质是新式类,并且被代理的类(即应用描述符的类)也是新式类。描述符的作用是用来代理一个类的属性,需要注意的是描述符不能定义在类的构造函数中,只能定义为类的属性,它只属于类的,不属于实例,我们通过查看实例和类的字典即可知晓。

   描述符是可以实现大部分Python类特性中最底层的数据结构的实现手段,我们常使用@classmethod@staticmethd@property、甚至是__slots__等属性都是通过描述符来实现的。它是很多高级库和框架的重要工具之一,是使用到装饰器或者元类的大型框架中的一个非常重要组件。在一般的开发中我们可能用不到描述符,但是我们如果想要开发一个大型的框架或者大型的系统,那使用描述符会起到如虎添翼的作用。它的加盟将会使得系统更加完美。下面将简单的介绍描述符的使用。

    class Descriptors:
        """
        数据描述符
        """
        def __init__(self, key, expected_type):
            """
            key:          用户传进的值
            expected_type:用户传进的值的类型
            """
            self.key = key
            self.expected_type = expected_type

        """
        描述符的三个内置属性的参数如下:
        ---------------------------------------------------
        self:     是描述符的对象,不是使用描述符类的对象
        instance: 这才是使用描述符类的对象
        owner:    是instance的类
        value:    是instance的值
        ---------------------------------------------------
        """

        def __get__(self, instance, owner):
            print("执行Descriptors的get")
            return instance.__dict__[self.key]    #将参数存入实例的字典

        def __set__(self, instance, value):
            print("执行Descriptors的set")
            #如果用户输入的值和值的类型不一致,则抛出TypeError异常
            if not isinstance(value, self.expected_type):
                raise TypeError("参数%s必须为%s"%(self.key, self.expected_type))
            instance.__dict__[self.key] = value   #为实例字典的key设值

        def __delete__(self, instance):
            print("执行Descriptors的delete")
            instance.__dict__.pop(self.key)       #删除实例字典的key

    class Light:

        #使用描述符
        name = Descriptors("name",str)
        price = Descriptors("price",float)

        def __init__(self, name, price):
            self.name = name
            self.price = price

    #设置两个参数,触发两次set的执行
    light = Light("电灯泡", 66.66)
    print(light.__dict__)
    light.name = "火箭筒"
    print(light.__dict__)
    """
    执行Descriptors的set
    执行Descriptors的set
    {'name': '电灯泡', 'price': 66.66}
    执行Descriptors的set
    {'name': '火箭筒', 'price': 66.66}
    """

 

二、描述符的种类及优先级

  描述符分为数据描述符和非数据描述符。把至少实现了内置属性__set__()和__get__()方法的描述符称为数据描述符;把实现了除__set__()以外的方法的描述符称为非数据描述符。之所以要区分描述符的种类,主要是因为它在代理类属性时有着严格的优先级限制。例如当使用数据描述符时,因为数据描述符大于实例属性,所以当我们实例化一个类并使用该实例属性时,该实例属性已被数据描述符代理,此时我们对该实例属性的操作是对描述符的操作。描述符的优先级的高低如下:

  类属性 > 数据描述符 > 实例属性 > 非数据描述符 > 找不到的属性触发__getattr__()

1. 类属性 > 数据描述符

  在以下实验中,使用 Light.name = "电灯泡" 语句没有触发set的执行说明类属性的优先级大于数据描述符的优先,此时相当于类属性覆盖了数据描述符,从而说明对类属性的一切操作都与描述符无关。

    class Descriptors:
        """
        数据描述符
        """
        def __get__(self, instance, owner):
            print("执行Descriptors的get")
        def __set__(self, instance, value):
            print("执行Descriptors的set")
        def __delete__(self, instance):
            print("执行Descriptors的delete")

    class Light:
        #使用描述符
        name = Descriptors()


    #测试
    Light.name               #执行描述符的get内置属性
    print(Light.__dict__)    #此时的name显示的是描述符的对象
    Light.name = "电灯泡"     #没有执行描述符的set内置属性
    print(Light.name)        #输出:电灯泡
    del Light.name           #没有执行描述符的delete内置属性
    print(Light.name)        #报错,因为Light类中的name被删了

 

2. 数据描述符 > 实例属性

  在以下实验中,数据描述符的优先级大于实例属性的优先级,此时实例属性name被数据描述符所覆盖,而price没有描述符代理,所以它任然是实例属性。

    class Descriptors:
        """
        数据描述符
        """
        def __get__(self, instance, owner):
            print("执行Descriptors的get")
        def __set__(self, instance, value):
            print("执行Descriptors的set")
        def __delete__(self, instance):
            print("执行Descriptors的delete")

    class Light:

        #使用描述符
        name = Descriptors()

        def __init__(self, name, price):
            self.name = name
            self.price = price


    #使用类的实例对象来测试
    light = Light("电灯泡",60)       #执行描述符的set内置属性
    light.name                      #执行描述符的get内置属性
    print(light.__dict__)           #查看实例的字典,不存在name
    print(Light.__dict__)           #查看类的字典,存在name(为描述符的对象)
    del light.name                  #执行描述符的delete内置属性

 

3. 实例属性 > 数据描述符

  在以下实验中,如果我们的实例属性中使用了非数据描述符,就不能对其进行复制操作。可见非数据描述符应该应用于不需要设置值的属性或者函数中。上述的设计没有多大的意义,只是增加对描述符的理解。

    class Descriptors:
        """
        非数据描述符
        """
        def __get__(self, instance, owner):
            print("执行Descriptors的set")
        def __delete__(self, instance):
            print("执行Descriptors的delete")
    
    class Light:

        #使用描述符
        name = Descriptors()

        def __init__(self, name, price):
            self.name = name
            self.price = price

    #测试
    light = Light("电灯泡",60)   #报错,描述符中没有__set__()方法

  经以下实验证明在该类中并没有set方法,所以该类是一个非数据描述符,Python中一切皆对象,函数也是一个对象,既然是对象那也可以是类实例化所得到的结果。函数在类中本身也是一种属性(函数属性),描述符在应用的时候也是被实例化为一个属性。

    class Descriptors:
        """
        非数据描述符
        """
        def func(self):
            print("世界的变化真快!近日00后都已经开始在街头打小三了")

    d = Descriptors()
    d.func()
    print(hasattr(Descriptors.func,"__set__"))     #False
    print(hasattr(Descriptors.func,"__get__"))     #True
    print(hasattr(Descriptors.func,"__delete__"))  #False
    d.func = "函数也是属性,也可以赋值,没毛病"
    print(d.func)
    del d.func
    d.func

 

三、描述符模拟系统的内置属性

  此利用描述符的原理,我们完全可以自定义模拟@classmethod@staticmethd@property等属性。实现这种类似系统的属性,我们还需要装饰器作为修饰,结合装饰器做成一个描述符。下面将简单的介绍使用描述符模拟系统自带的装饰器。

1. 模拟 @classmethod

    class Imitate_classmethod:
        """
            使用描述符模拟@classmethod
    """
        def __init__(self, func):
            self.func = func

        def __get__(self, instance, owner):
            #对传进函数进行加工,最后返回该函数
            def machining_func(*args, **kwargs):
                print("函数加工处理后,返回实例的类")
                return self.func(owner, *args, **kwargs)
            return machining_func

    class Test:

        book_name = "从你的世界路过"

        #使用装饰器的结果等价于将函数变为属性:
        # book = Imitate_classmethod( book )
        @Imitate_classmethod
        def book_1(cls):
            print("这本书的名字是:%s"%cls.book_name)

        @Imitate_classmethod
        def book_2(cls, price):
            print("这本书的名字是:%s;价格是 %s"%(cls.book_name, price))

    #测试
    Test.book_1()
    Test.book_2(28.5)
    """
    运行输出:
    ---------------------------------------------
    函数加工处理后,返回实例的类
    这本书的名字是:从你的世界路过
    函数加工处理后,返回实例的类
    这本书的名字是:从你的世界路过;价格是 28.5
    ---------------------------------------------
    """

 

2. 模拟 @staticmethod

  staticmethod方法与classmethod方法的区别在于classmethod方法在使用需要传进一个类的引用作为参数。而staticmethod则不用。

    class Imitate_staticmethod:
        """
        使用描述符模拟@staticmethod
        """
        def __init__(self, func):
            self.func = func

        def __get__(self, instance, owner):
            #对传进函数进行加工,最后返回该函数
            def machining_func(*args, **kwargs):
                print("函数加工处理后,返回实例的类")
                return self.func(*args, **kwargs)
            return machining_func

    class Test:

        @Imitate_staticmethod
        def static_func(*args):
            print("您输入的是:",*args)
    
    #测试
    Test.static_func("柠檬","香蕉")
    test = Test()
    test.static_func(110, 112)
    """
    运行输出:
    -------------------------------------------------------------------
    函数加工处理后,返回实例的类
    您输入的是: 柠檬 香蕉
    函数加工处理后,返回实例的类
    您输入的是: 110 112
    -------------------------------------------------------------------
    """

 

3. 模拟 @property

  在以下实验中,我们将描述符的回调结果存入对象字典中的好处是我们以后再执行函数时就不会每一次都触发描述的运行,从而提高程序的效率。这样,我们有再执行同样的函数时,解释器会先检索对象的字典,如果字典存在上次执行结果的值,那就不用触发描述符的运行了。在这个实验中必须强调的一点是描述符的优先级,我们想让程序的描述符不能覆盖实例属性就必须使用非数据描述符。所以因需求不同,描述符的优先级也不同。

    class Imitate_property:
        """
        使用描述符模拟property
        """
        def __init__(self, func):
            self.func = func

        def __get__(self, instance, owner):
            if instance is None:
                return self
            #回调传入的函数,将运行结果保存在变量res中
            res = self.func(instance)
            #为函数名func.__name__设置一个值res后存入对象的字典中
            setattr(instance, self.func.__name__, res)
            return res

    class Test:

        def __init__(self, value):
            self.value = value

        @Imitate_property
        def function(self):
            return self.value**2

    test = Test(2)
    print(test.function)  #输出:4
    print(test.__dict__)  #输出:{'value': 2, 'function': 4}
    print(test.function)  #输出:4
    print(test.function)  #输出:4
    print(test.__dict__)  #输出:{'value': 2, 'function': 4}

 

 

 

 

 

转载于:https://www.cnblogs.com/Lynnblog/p/9033455.html

### Python 描述符的概念 在 Python 中,描述符是一种特殊类型的对象,它允许开发者通过实现特定的协议方法来控制属性的行为。这种机制使得我们能够在访问、修改删除某个属性时执行自定义逻辑[^1]。 具体来说,描述符是实现了 `__get__`、`__set__` `__delete__` 方法之一的对象。其中: - **`__get__(self, instance, owner)`**: 当读取属性时被调用。 - **`__set__(self, instance, value)`**: 当设置属性值时被调用。 - **`__delete__(self, instance)`**: 当删除属性时被调用。 根据是否实现 `__set__` 方法,描述符分为两类: - 数据描述符 (Data Descriptor): 同时实现了 `__get__` 和 `__set__` 方法。 - 非数据描述符 (Non-data Descriptor): 只实现了 `__get__` 方法。 当访问一个属性时,Python 会优先检查是否存在相应的描述符,并按以下顺序查找: 1. 对象实例字典 (`__dict__`); 2. 类中的数据描述符; 3. 如果不存在数据描述符,则再次检查实例字典; 4. 类中的非数据描述符; 5. 父类的字典[^2]。 --- ### Python 描述符的使用方法 #### 定义描述符类 要创建一个描述符,需编写一个包含上述方法的类。以下是具体的例子: ```python class IntegerDescriptor: """这是一个简单的描述符类""" def __init__(self, default_value=0): self.value = default_value def __get__(self, instance, owner): if instance is None: return self return self.value def __set__(self, instance, value): if not isinstance(value, int): raise ValueError("Only integers are allowed.") self.value = value def __delete__(self, instance): del self.value ``` 在这个示例中,`IntegerDescriptor` 是一个数据描述符,因为它同时实现了 `__get__` 和 `__set__` 方法。该描述符强制要求所赋值的内容必须为整型。 #### 将描述符应用于类 下面是如何将上面定义的描述符应用到另一个类中: ```python class Point: x = IntegerDescriptor(0) # 使用描述符管理 'x' 属性 y = IntegerDescriptor(0) # 使用描述符管理 'y' 属性 p = Point() print(p.x) # 输出: 0 [^1] p.x = 10 print(p.x) # 输出: 10 try: p.x = "not an integer" except ValueError as e: print(e) # 输出: Only integers are allowed. ``` 在此代码片段中,尝试给 `Point` 的 `x` 属性分配字符串值时触发异常,因为描述符验证了输入类型。 --- ### 文件描述符的应用场景 尽管文件描述符前面提到的属性描述符不同,但它也是 Python 编程的重要概念之一。文件描述符通常由操作系统提供,用来标识已打开的文件资源。在 Python 中,可以利用 `os.open()` 获取文件描述符,并将其传递给 `open()` 函数以进一步操作文件[^4]。 下面是使用文件描述符的一个简单案例: ```python import os fd = os.open('example.txt', os.O_RDWR | os.O_CREAT) # 打开/创建文件并返回文件描述符 f = open(fd, 'r+') # 利用文件描述符初始化文件对象 f.write('Hello, world!') f.seek(0) content = f.read() print(content) # 输出: Hello, world! f.close() os.close(fd) ``` 此脚本展示了如何先通过 `os.open()` 获得文件描述符,再借助标准库函数完成文件写入和读取的操作。 --- ### 总结 描述符作为一种强大的工具,在 Python 开发中有广泛用途,比如实现动态属性、封装复杂逻辑以及增强程序的安全性和可维护性。无论是基本的数据校验还是更复杂的定制功能,都可以依赖于这一特性得以实现。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值