什么是描述符?
描述符 是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
在 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__
是什么时候执行的呢? 我们来添加点日志 看看
# -*- 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
现在我们要求 自定义自己验证器 ,有这个验证类
这里有三个实用的数据验证工具。
OneOf
验证一个值是否是一组受限制的选项之一。Number
验证一个值是int
或 float
。可以选择验证一个值是否在一个给定的最小值或最大值之间。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
验证一个值是int
或float
。可以选择验证一个值是否在一个给定的最小值或最大值之间。
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
在这篇文章 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_