文章目录

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

什么是描述符?

什么是描述符?

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

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

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

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

来看下 官方的一个例子

class Ten:def __get__(self, obj, objtype=None):return 10class Number:x = 5  # Regular class attributey = Ten()  # Descriptor instance

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

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

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

动态查找

hello1.py

# -*- coding: utf-8 -*-
import osclass DirectorySize:def __get__(self, obj, objtype=None):return len(os.listdir(obj.dirname))class Directory:size = DirectorySize()  # Descriptor instancedef __init__(self, dirname):self.dirname = dirname  # Regular instance attributeif __name__ == '__main__':s = Directory('song')g = Directory('game')print(s.size)print(g.size)pass

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

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

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

管理属性

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

import logginglogging.basicConfig(level=logging.INFO)class LoggedAgeAccess:def __get__(self, obj, objtype=None):value = obj._agelogging.info('Accessing %r giving %r', 'age', value)return valuedef __set__(self, obj, value):logging.info('Updating %r to %r', 'age', value)obj._age = valueclass Person:age = LoggedAgeAccess()  # Descriptor instancedef __init__(self, name, age):self.name = name  # Regular instance attributeself.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 logginglogging.basicConfig(level=logging.INFO)class LoggedAccess:def __set_name__(self, owner, name):self.public_name = nameself.private_name = '_' + namedef __get__(self, obj, objtype=None):value = getattr(obj, self.private_name)logging.info('Accessing %r giving %r', self.public_name, value)return valuedef __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 instanceage = LoggedAccess()  # Second descriptor instancedef __init__(self, name, age):self.name = name  # Calls the first descriptorself.age = age  # Calls the second descriptordef 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 logginglogging.basicConfig(level=logging.INFO)class LoggedAccess:print("log:00000000000 ")def __set_name__(self, owner, name):print(f"log:11111111111 ,{owner=},{name=}")self.public_name = nameself.private_name = '_' + namedef __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 valuedef __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 instanceprint("log:55555555555555 ")def __init__(self, name):print("log:66666666666666 ")self.name = name  # Calls the first descriptorprint("log:7777777777777 ")def birthday(self):self.age += 1

结果如下:

log:00000000000
log:4444444444444
log:55555555555555
log:11111111111 ,owner=<class '__main__.Person'>,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:passc = 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, abstractmethodclass Validator(ABC):def __set_name__(self, owner, name):self.private_name = '_' + namedef __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)@abstractmethoddef validate(self, value):pass

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

自定义验证

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

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

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

from typing import List, Union, Tuple, Setclass 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 "<input>", line 1, in <module>File "<input>", line 6, in __init__File "<input>", line 20, in __set__File "<input>", line 35, in validate
ValueError: Expected 10 to be one of {'wood', 'metal', 'plastic'}
>>> A(kind='Metal')
Traceback (most recent call last):File "<input>", line 1, in <module>File "<input>", line 6, in __init__File "<input>", line 20, in __set__File "<input>", 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_valueself.max_val = max_valuedef 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 = quantityif __name__ == '__main__':a = A(quantity=9)

第三个类的实现

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

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

class String(Validator):def __init__(self, minsize=None, maxsize=None, predicate=None):self.minsize = minsizeself.maxsize = maxsizeself.predicate = predicatedef 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 = nameself.kind = kindself.quantity = quantity

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

>>> Component('Widget', 'metal', 5)  # Blocked: 'Widget' is not all uppercase
Traceback (most recent call last):File "<input>", line 1, in <module>File "/Users/frank/code/py_proj/study-fastapi/mydescriptor/damo4.py", line 94, in __init__self.name = nameFile "/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 validateraise 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 "<input>", line 1, in <module>File "/Users/frank/code/py_proj/study-fastapi/mydescriptor/damo4.py", line 95, in __init__self.kind = kindFile "/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 validateraise 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 "<input>", line 1, in <module>File "/Users/frank/code/py_proj/study-fastapi/mydescriptor/damo4.py", line 96, in __init__self.quantity = quantityFile "/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 validateraise 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 "<input>", line 1, in <module>File "/Users/frank/code/py_proj/study-fastapi/mydescriptor/damo4.py", line 96, in __init__self.quantity = quantityFile "/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 validateraise 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 = nameself.weight = weightself.price = pricedef __repr__(self):return f"{self.__class__.__name__}(name={self.name!r},weight={self.weight},price={self.price})"@propertydef weight(self):return self._weight@weight.setterdef weight(self, value):if value < 0:raise ValueError(f"expected value > 0, but now value:{value}")self._weight = value@propertydef price(self):return self._price@price.setterdef 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, abstractmethodclass Validator(ABC):"""验证器  也是一个描述符的抽象类"""def __set_name__(self, owner, name):self.private_name = '_' + namedef __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)@abstractmethoddef validate(self, value):passclass Number(Validator):# 1. `Number`验证一个值是`int`或 `float`# 可以选择验证一个值是否在一个给定的最小值或最大值之间。def __init__(self, min_value=None, max_value=None):self.min_val = min_valueself.max_val = max_valuedef 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 = nameself.weight = weightself.price = pricedef __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 "<input>", 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 validateif 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 "<input>", 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 validateif 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中 什么是描述符?相关推荐

  1. python中的self描述符__set__和__get__简单总结

    1. python中的self用法总结 class Student(object):def __init__(self, name, score):self.name = nameself.score ...

  2. python中@property以及描述符descriptor详解

    python一直以代码简洁优雅而著称,这篇文章介绍的小技巧,就是如何优雅地对一个类的属性进行赋值和取值.不过不仅仅如此,本文章还为类属性的查找顺序,以及装饰器在类方法的使用打下了基础. 文章目录 待解 ...

  3. python中换行的转义符_详解Python中的各种转义符\n\r\t

    Python中的各种转义符\n\r\t 转义符 描述 \ 续行符(在行尾时) \\ 反斜杠符号 ' 单引号 " 双引号 \a 响铃 \b 退格(Backspace) \e 转义 \000 空 ...

  4. 90 % 的 Python 开发者不知道的描述符应用

    好吧,我承认我标题党了.但是这篇文章的知识点,你有极大的可能并不知道. 前段时间,我写了一篇描述符的入门级文章,从那些文章里你知道了如何定义描述符,且明白了描述符是如何工作的. 如果你还未学习,可以点 ...

  5. Linux中文件描述符1,linux内核中的文件描述符(一)--基础知识简介

    原标题:linux内核中的文件描述符(一)--基础知识简介 Kernel version:2.6.14 CPU architecture:ARM920T Author:ce123(http://blo ...

  6. Linux中的文件描述符与打开文件之间的关系

    1. 概述 在Linux系统中一切皆可以看成是文件,文件又可分为:普通文件.目录文件.链接文件和设备文件.文件描述符(file descriptor)是内核为了高效管理已被打开的文件所创建的索引,其是 ...

  7. linux c中的文件描述符与打开文件之间的关系

    转载请说明出处:http://blog.csdn.net/cywosp/article/details/38965239 1. 概述     在Linux系统中一切皆可以看成是文件,文件又可分为:普通 ...

  8. Linux中对文件描述符的操作(FD_ZERO、FD_SET、FD_CLR、FD_ISSET

    在Linux中,内核利用文件描述符(File Descriptor)即文件句柄,来访问文件.文件描述符是非负整数.打开现存文件或新建文件时,内核会返回一个文件描述符.读写文件也需要使用文件描述符来指定 ...

  9. linux内核中的文件描述符(四)--fd的分配--get_unused_fd

    linux内核中的文件描述符(四)--fd的分配--get_unused_fd Kernel version:2.6.14 CPU architecture:ARM920T Author:ce123( ...

最新文章

  1. PostgreSQL中的数据库实例、模式、用户(角色)、表空间
  2. 洛谷P5733、P5734、P5735、P5739题题解(Java语言描述)
  3. sql2000数据库备份文件还原到sql2005
  4. P9:卷积神经网络的工程实践技巧
  5. 亚洲诚信亮相2018天翼智能生态博览会
  6. nginx 安全加固心得
  7. paip.使用泛型时未能找到类型或命名空间名称“T
  8. 在计算机领域黑箱,计算机模拟电学黑箱
  9. IAR(For STM32) 安装,配置,工程创建,下载,调试
  10. 2 OsgEarth中实现PBR材质流程总结
  11. 云原生 -- contour + envoy部署
  12. python名人问题_Python 思考录 练习01
  13. Echarts实现模拟航线
  14. macOS Big Sur 安装配置QT creator教程
  15. 伪装计算机主机,求告知怎么伪装电脑配置
  16. 如何用python提取音频
  17. 从键盘输入一元二次方程的三个系数,求其两个实根(考虑判别式可能为负的情况,还需要考虑二次项系数a和一次项系数b 是否为0的情况)。
  18. Altium Designer(AD)多边形铺铜
  19. Redis 多服务器集群搭建
  20. PLC,DSP,ARM,单片机有什么区别?

热门文章

  1. java progressbar swt_Eclipse-SWT学习之进度条的SWT实现
  2. 新人报到 圣诞问候
  3. eclipse如何做java游戏,:#急需基于eclipse的JAVA小游戏源代码!!!#-南开游戏网...
  4. 35岁还能转型网工吗?
  5. solr入门之自定义排序之构建自己的权重计算方法及相应的排序字段
  6. linux 回收子线程 和取消(杀死)线程
  7. 一键捕获,自由分享:开源截图工具盘点
  8. warmp启动图标黄色
  9. 基于Python中tkinter做的可视化简易计算器(代码有注释,利于理解)
  10. Thumbnails图片压缩