python中 什么是描述符?

文章目录

    • 什么是描述符?
      • 动态查找
      • 管理属性
      • \_\_set_name__ 魔术方法
      • 描述符定义
      • 数据验证模块
      • 自定义验证
      • Goods类的验证
      • 总结
      • 参考文档

什么是描述符?

什么是描述符?

描述符 是python中一个高级的特性, 简单来说 是用来控制 对象的属性的赋值,查找以及删除的 协议

在了解描述符之前 最好 先了解一下 property 这个装饰器. 如果你不了解 property 可以先看下 python3中的特性property介绍

property 装饰器 可以实现对 对象的属性 进行控制,比如 对属性进行合法性校验,比如人的年龄 只能在 0-100 岁之间, 一些对象的属性 长度有限制等。

如果有了描述符 一切都变得更加自然。

来看下 官方的一个例子

class Ten:
    def __get__(self, obj, objtype=None):
        return 10


class Number:
    x = 5  # Regular class attribute
    y = Ten()  # Descriptor instance

x 是普通的类属性, y 是一个描述符的对象。

>>> num = Number()
>>> num.x
5
>>> num.y
10

当使用 num.y 会调用 描述符的 __get__ 的魔术方法 , 然后返回值.

动态查找

hello1.py

# -*- coding: utf-8 -*- 
import os

class DirectorySize:

    def __get__(self, obj, objtype=None):
        return len(os.listdir(obj.dirname))


class Directory:
    size = DirectorySize()  # Descriptor instance

    def __init__(self, dirname):
        self.dirname = dirname  # Regular instance attribute


if __name__ == '__main__':
    s = Directory('song')
    g = Directory('game')
    print(s.size)
    print(g.size)
    pass

python中 什么是描述符?_第1张图片

在 song, game 文件夹里随便放一些文件进行测试

>>> s = Directory('song')
... g = Directory('game')
>>> s.size
5
>>> g.size
6

这里就是通过 obj.dirname 来获取文件的size 的例子。

管理属性

来看一个 更加真实的例子,使用描述符来控制属性。

import logging

logging.basicConfig(level=logging.INFO)


class LoggedAgeAccess:

    def __get__(self, obj, objtype=None):
        value = obj._age
        logging.info('Accessing %r giving %r', 'age', value)
        return value

    def __set__(self, obj, value):
        logging.info('Updating %r to %r', 'age', value)
        obj._age = value


class Person:
    age = LoggedAgeAccess()  # Descriptor instance

    def __init__(self, name, age):
        self.name = name  # Regular instance attribute
        self.age = age  # Calls __set__()

    def birthday(self):
        self.age += 1  # Calls both __get__() and __set__()

我们来定义一个描述符,这个描述符 用来控制 Person 的年龄, 就可以使用上面的代码, 在描述符的方法 __set__ 进行判断 处理。

>>> mary = Person('Mary M', 30)  
INFO:root:Updating 'age' to 30
>>> dave = Person('David D', 40)
INFO:root:Updating 'age' to 40
>>> vars(mary)               # The actual data is in a private attribute
{'name': 'Mary M', '_age': 30}
>>> mary.age  
INFO:root:Accessing 'age' giving 30
30
>>> mary.birthday()
INFO:root:Accessing 'age' giving 30
INFO:root:Updating 'age' to 31
INFO:root:Accessing 'age' giving 31
>>> mary.age
INFO:root:Accessing 'age' giving 31
31

上面的代码 基本上可以 实现 对age 属性的判断, 但是有个小问题 ,就是说 我们看下 描述符类 有一个小的问题,

我在 obj._age 添加了一个 内部的属性 _age 绑定在 在描述符类里面。这个属性硬编码在 LoggedAgeAccess这个描述符中.

为了防止有 硬编码的 _age 这种情况,我们 需要实现 __set_name__ 魔术方法

# -*- coding: utf-8 -*- 

import logging

logging.basicConfig(level=logging.INFO)


class LoggedAccess:

    def __set_name__(self, owner, name):
        self.public_name = name
        self.private_name = '_' + name

    def __get__(self, obj, objtype=None):
        value = getattr(obj, self.private_name)
        logging.info('Accessing %r giving %r', self.public_name, value)
        return value

    def __set__(self, obj, value):
        logging.info('Updating %r to %r', self.public_name, value)
        setattr(obj, self.private_name, value)


class Person:
    name = LoggedAccess()  # First descriptor instance
    age = LoggedAccess()  # Second descriptor instance

    def __init__(self, name, age):
        self.name = name  # Calls the first descriptor
        self.age = age  # Calls the second descriptor

    def birthday(self):
        self.age += 1

def __set_name__(self, owner, name) 这里 owner 是指 Person ,name 就是 定义在Person的 描述符实例的名称,'name' ,'age'

两个字符串类型的数据 。owner 是指 描述符放在类属性里对应的类。

我们发现 name 这个描述符对象里面 就有两个值键值对 ,分别是 public_name,private_name 这个就是通过 __set_name__

>>> vars(vars(Person)['name'])
{'public_name': 'name', 'private_name': '_name'}
>>> vars(vars(Person)['age'])
{'public_name': 'age', 'private_name': '_age'}

__set_name__ 魔术方法

__set_name__ 是什么时候执行的呢? 我们来添加点日志 看看

# -*- coding: utf-8 -*- 
import logging

logging.basicConfig(level=logging.INFO)


class LoggedAccess:
    print("log:00000000000 ")

    def __set_name__(self, owner, name):
        print(f"log:11111111111 ,{owner=},{name=}")
        self.public_name = name
        self.private_name = '_' + name

    def __get__(self, obj, objtype=None):
        value = getattr(obj, self.private_name)
        print("log:2222222222222 ")

        logging.info('Accessing %r giving %r', self.public_name, value)
        return value

    def __set__(self, obj, value):
        logging.info('Updating %r to %r', self.public_name, value)
        print("log:3333333333333333333 ")

        setattr(obj, self.private_name, value)


class Person:
    print("log:4444444444444 ")

    age = LoggedAccess()  # Second descriptor instance

    print("log:55555555555555 ")

    def __init__(self, name):
        print("log:66666666666666 ")
        self.name = name  # Calls the first descriptor
        print("log:7777777777777 ")

    def birthday(self):
        self.age += 1

结果如下:

log:00000000000 
log:4444444444444 
log:55555555555555 
log:11111111111 ,owner=,name='age'

Person 类的 类属性 创建完成后 会自动执行 __set_name__ 方法, 这个魔术方法 对应的参数 owner 为 Person 类, name 为 具体的名称 这里 是 age

通过实现 魔术方法 __set_name__ 就可以 实现 将 _age , _name 从 描述符类中独立出去了。

这个方法 是自动执行的

Automatically called at the time the owning class owner is created. The object has been assigned to name in that class:

在创建拥有类的所有者时自动调用。该对象已被分配到该类中的名称。

class A:
    x = C()  # Automatically calls: x.__set_name__(A, 'x')

如果类变量的赋值 ,是在类创建之后完成的, __set_name__ 则不会自动调用的。 如有有需要,可以手动进行调用,如下:

class A:
   pass

c = C()
A.x = c                  # The hook is not called
c.__set_name__(A, 'x')   # Manually invoke the hook

更详细的 可以看官方文档 set_name

描述符定义

如果 一个类实现了魔术方法 __get__(), __set__(), or __delete__(). 我们就说 这个是 描述符类 。

__set_name__ 这个方法 可以实现 ,也可以不用实现 . 描述符可以有一个__set_name__()方法。这个方法只用于描述符需要知道它被创建的类或者它被分配到的类变量的名字的情况。(如果这个方法存在,即使该类不是描述符,也会被调用)。

这个方法是自动执行的。

在属性查找过程中,描述符会被点运算符所调用。如果描述符被vars(some_class)[descriptor_name]间接访问,描述符实例会被返回而不被调用。

数据验证模块

来做一个数据 验证模块的实现,这里就是使用描述符来做

定义一个抽象基类,这个基类是一个描述符, __set__(obj,value) 方法中 使用抽象方法,来验证value 值的合法性。

这个抽象方法 由子类进行实现

from abc import ABC, abstractmethod


class Validator(ABC):

    def __set_name__(self, owner, name):
        self.private_name = '_' + name

    def __get__(self, obj, owner=None):
        return getattr(obj, self.private_name)

    def __set__(self, obj, value):
        self.validate(value)
        setattr(obj, self.private_name, value)

    @abstractmethod
    def validate(self, value):
        pass

现在我们要求 自定义自己验证器 ,有这个验证类

自定义验证

这里有三个实用的数据验证工具。

  1. OneOf 验证一个值是否是一组受限制的选项之一。
  2. Number验证一个值是intfloat 。可以选择验证一个值是否在一个给定的最小值或最大值之间。
  3. String验证一个值是一个str 。可以选择验证给定的最小或最大长度。它也可以验证一个用户定义的谓词 。

对应第一个验证器OneOf ,相对比较简单 只要继承抽象类,重写validate 方法 就可以了,只要判断 value 是不是在 options 里面即可,如果不在 Options 里面 直接抛出异常即可。

from typing import List, Union, Tuple, Set


class OneOf(Validator):

    def __init__(self, options: Union[List, Tuple, Set]):
        self.options = set(options)

    def validate(self, value):
        if value not in self.options:
            raise ValueError(f'Expected {value!r} to be one of {self.options!r}')

写个简单的类进行测试

class A:
    kind = OneOf(options=['wood', 'metal', 'plastic'])

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

这里定义 kind 描述符 ,要求 kind 只能从 这三个里面取值 ['wood', 'metal', 'plastic']

在Consle 里测试一下

>>> 
... class A:
...     kind = OneOf(options=['wood', 'metal', 'plastic'])
... 
...     def __init__(self, kind):
...         self.kind = kind
...         
>>> 
>>> A(kind=10)
Traceback (most recent call last):
  File "", line 1, in <module>
  File "", line 6, in __init__
  File "", line 20, in __set__
  File "", line 35, in validate
ValueError: Expected 10 to be one of {'wood', 'metal', 'plastic'}
>>> A(kind='Metal')
Traceback (most recent call last):
  File "", line 1, in <module>
  File "", line 6, in __init__
  File "", line 20, in __set__
  File "", line 35, in validate
ValueError: Expected 'Metal' to be one of {'wood', 'metal', 'plastic'}
>>> A(kind='metal')
<__main__.A object at 0x7f958c89adc0>
>>> A(kind='wood')
<__main__.A object at 0x7f958c8a6d90>
>>> A(kind='plastic')
<__main__.A object at 0x7f958c8a9c40>

我们发现 这个验证器 已经非常好的成功验证了value 的值 是否在 options 里面 如果不在会抛出异常。

来看下 第二个验证器如何实现

Number验证一个值是intfloat。可以选择验证一个值是否在一个给定的最小值或最大值之间。

class Number(Validator):
    # 1. `Number`验证一个值是[`int`]或[`float`]
    # 可以选择验证一个值是否在一个给定的最小值或最大值之间。

    def __init__(self, min_value=None, max_value=None):
        self.min_val = min_value
        self.max_val = max_value

    def validate(self, value):

        if not isinstance(value, (int, float)):
            raise TypeError(f'Expected {value!r} to be an int or float')

        if self.min_val is not None:
            if value < self.min_val:
                raise ValueError(f"{value} less than min_value:{self.min_val},type:{type(value)}")

        if self.max_val is not None:
            if value > self.max_val:
                raise ValueError(f"{value} greater than max_value:{self.max_val},type:{type(value)}")


class A:
    quantity = Number(min_value=10)

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


if __name__ == '__main__':
    a = A(quantity=9)

第三个类的实现

String验证一个值是一个str。可以选择验证给定的最小或最大长度。它也可以验证一个用户定义的谓词。

思路还是一样的. 实现 validate 抽象类即可

class String(Validator):

    def __init__(self, minsize=None, maxsize=None, predicate=None):
        self.minsize = minsize
        self.maxsize = maxsize
        self.predicate = predicate

    def validate(self, value):
        if not isinstance(value, str):
            raise TypeError(f'Expected {value!r} to be an str')
        if self.minsize is not None and len(value) < self.minsize:
            raise ValueError(
                f'Expected {value!r} to be no smaller than {self.minsize!r}'
            )
        if self.maxsize is not None and len(value) > self.maxsize:
            raise ValueError(
                f'Expected {value!r} to be no bigger than {self.maxsize!r}'
            )
        if self.predicate is not None and not self.predicate(value):
            raise ValueError(
                f'Expected {self.predicate} to be true for {value!r}'
            )

来测试一下

class Component:
	
    name = String(minsize=3, maxsize=10, predicate=str.isupper)
    kind = OneOf(options=['wood', 'metal', 'plastic'])
    quantity = Number(min_value=0)

    def __init__(self, name, kind, quantity):
        self.name = name
        self.kind = kind
        self.quantity = quantity

在console 里面 我们来测试一下,自己定义的描述符验证器的效果吧

>>> Component('Widget', 'metal', 5)  # Blocked: 'Widget' is not all uppercase
Traceback (most recent call last):
  File "", line 1, in <module>
  File "/Users/frank/code/py_proj/study-fastapi/mydescriptor/damo4.py", line 94, in __init__
    self.name = name
  File "/Users/frank/code/py_proj/study-fastapi/mydescriptor/damo4.py", line 20, in __set__
    self.validate(value)
  File "/Users/frank/code/py_proj/study-fastapi/mydescriptor/damo4.py", line 82, in validate
    raise ValueError(
ValueError: Expected <method 'isupper' of 'str' objects> to be true for 'Widget'
>>> Component('WIDGET', 'metle', 5)     # Blocked: 'metle' is misspelled
Traceback (most recent call last):
  File "", line 1, in <module>
  File "/Users/frank/code/py_proj/study-fastapi/mydescriptor/damo4.py", line 95, in __init__
    self.kind = kind
  File "/Users/frank/code/py_proj/study-fastapi/mydescriptor/damo4.py", line 20, in __set__
    self.validate(value)
  File "/Users/frank/code/py_proj/study-fastapi/mydescriptor/damo4.py", line 35, in validate
    raise ValueError(f'Expected {value!r} to be one of {self.options!r}')
ValueError: Expected 'metle' to be one of {'metal', 'plastic', 'wood'}
>>> Component('WIDGET', 'metal', -5)  # Blocked: -5 is negative
Traceback (most recent call last):
  File "", line 1, in <module>
  File "/Users/frank/code/py_proj/study-fastapi/mydescriptor/damo4.py", line 96, in __init__
    self.quantity = quantity
  File "/Users/frank/code/py_proj/study-fastapi/mydescriptor/damo4.py", line 20, in __set__
    self.validate(value)
  File "/Users/frank/code/py_proj/study-fastapi/mydescriptor/damo4.py", line 54, in validate
    raise ValueError(f"{value} less than min_value:{self.min_val},type:{type(value)}")
ValueError: -5 less than min_value:0,type:<class 'int'>

>>> Component('WIDGET', 'metal','frank') # Blocked: 'frank' isn't a number
Traceback (most recent call last):
  File "", line 1, in <module>
  File "/Users/frank/code/py_proj/study-fastapi/mydescriptor/damo4.py", line 96, in __init__
    self.quantity = quantity
  File "/Users/frank/code/py_proj/study-fastapi/mydescriptor/damo4.py", line 20, in __set__
    self.validate(value)
  File "/Users/frank/code/py_proj/study-fastapi/mydescriptor/damo4.py", line 50, in validate
    raise TypeError(f'Expected {value!r} to be an int or float')
TypeError: Expected 'frank' to be an int or float    
>>> c = Component('WIDGET', 'metal', 5)  # Allowed:  The inputs are valid

Goods类的验证

在这篇文章 python3中的特性property介绍 中有写过 Goods类对属性的验证

在文章中 有以下类似的代码:

这里进行校验 weight ,price 都为数字,且大于0 这个验证逻辑,当时是使用 property 特性的装饰器来写的,代码如下:

class Goods:

    def __init__(self, name, weight, price):
        """

        :param name: 商品名称
        :param weight:  重量
        :param price: 价格
        """
        self.name = name
        self.weight = weight
        self.price = price

    def __repr__(self):

        return f"{self.__class__.__name__}(name={self.name!r},weight={self.weight},price={self.price})"

    @property
    def weight(self):
        return self._weight

    @weight.setter
    def weight(self, value):
        if value < 0:
            raise ValueError(f"expected value > 0, but now value:{value}")

        self._weight = value

    @property
    def price(self):
        return self._price

    @price.setter
    def price(self, value):
        if value < 0:
            raise ValueError(f"expected value > 0, but now value:{value}")
        self._price = value

从这里可以看出 使用描述符的 只要保证 weight , price 大于0 即可

现在 有了描述符 之后, 我们把这个验证的逻辑写到描述符里面,看起来是不是清晰一些呢?

# -*- coding: utf-8 -*-
"""
@Time    : 2022/10/22 20:15
@Author  : Frank
@File    : demo5.py
"""
from abc import ABC, abstractmethod


class Validator(ABC):
    """
    验证器  也是一个描述符的抽象类
    """

    def __set_name__(self, owner, name):
        self.private_name = '_' + name

    def __get__(self, obj, owner=None):
        return getattr(obj, self.private_name)

    def __set__(self, obj, value):
        self.validate(value)
        setattr(obj, self.private_name, value)

    @abstractmethod
    def validate(self, value):
        pass



class Number(Validator):
    # 1. `Number`验证一个值是`int`或 `float`
    # 可以选择验证一个值是否在一个给定的最小值或最大值之间。
    def __init__(self, min_value=None, max_value=None):
        self.min_val = min_value
        self.max_val = max_value

    def validate(self, value):

        if not isinstance(value, (int, float)):
            raise TypeError(f'Expected {value!r} to be an int or float')

        if self.min_val is not None:
            if value < self.min_val:
                raise ValueError(f"{value} less than min_value:{self.min_val},type:{type(value)}")

        if self.max_val is not None:
            if value > self.max_val:
                raise ValueError(f"{value} greater than max_value:{self.max_val},type:{type(value)}")


class Goods:
    weight = Number(min_value=0)
    price = Number(min_value=0)

    def __init__(self, name, weight, price):
        """

        :param name: 商品名称
        :param weight:  重量
        :param price: 价格
        """
        self.name = name
        self.weight = weight
        self.price = price

    def __repr__(self):
        return f"{self.__class__.__name__}(name={self.name!r},weight={self.weight},price={self.price})"


if __name__ == '__main__':
    goods = Goods(name='apple', weight=10, price=3)

在 Console 里面进行测试

>> goods = Goods(name='apple',weight=10,price=3)
>>> goods = Goods(name='apple',weight=10,price='3')
Traceback (most recent call last):
  File "", line 1, in <module>
  File "/Users/frank/code/py_proj/study-fastapi/mydescriptor/demo5.py", line 63, in __init__
    def __repr__(self):
  File "/Users/frank/code/py_proj/study-fastapi/mydescriptor/demo5.py", line 20, in __set__
    setattr(obj, self.private_name, value)
  File "/Users/frank/code/py_proj/study-fastapi/mydescriptor/demo5.py", line 39, in validate
    if self.min_val is not None:
TypeError: Expected '3' to be an int or float
>>> goods = Goods(name='apple',weight=-10,price=9)
Traceback (most recent call last):
  File "", line 1, in <module>
  File "/Users/frank/code/py_proj/study-fastapi/mydescriptor/demo5.py", line 62, in __init__
  File "/Users/frank/code/py_proj/study-fastapi/mydescriptor/demo5.py", line 20, in __set__
    setattr(obj, self.private_name, value)
  File "/Users/frank/code/py_proj/study-fastapi/mydescriptor/demo5.py", line 43, in validate
    if self.max_val is not None:
ValueError: -10 less than min_value:0,type:<class 'int'>
>>> goods = Goods(name='apple',weight=10,price=9)

发现测试代码符合我们的预期,运行的非常完美了。

这样只需要实现 Number 这个描述符,这个描述符对数据校验,判断数据的最大,最小值,以及数据类型为 int, float 类型。然后把这个描述对象定义在需要验证的 类属性上面 即可。

总结

​ 本文简单介绍了一下描述符的使用场景, 实际上还有很多场景可以使用描述符来实现,详细的内容可以参考官方的文档。描述符在Python语言中是比较高级的特性,方便我们来控制 对象的属性赋值,删除,修改等操作。这篇文章也算是把之前文章python3中的特性property介绍 遗留的问题做了详细的介绍, 这个话题是比较不好理解的内容,希望我这篇文章能给大家带来一些对描述符的理解。 加油,同学们!

参考文档

python3中的特性property介绍

implementing-descriptors

descriptor.html#primer

python-descriptor-in-detail

_set_name_

分享快乐,留住感动. '2022-10-22 20:39:27' --frank

你可能感兴趣的:(python基础&进阶,python,开发语言)