描述符是 Python 语言中一个强大的特性,它隐藏在编程语言的底层,为许多神奇的魔法提供了便利。
假设你需要一个学生类,来记录考试的分数。简单写如下:
class Student:
def __init__(self, name, math):
self.name = name
self.math = math
但是稍后发现分数为负值或者大于100是不合理的,上面的代码对输入参数没有任何检查。如下:
>>> stu = Student("a", -90)
>>> stu.math
-90
于是修改代码做限制:
class Student:
def __init__(self, name, math):
self.name = name
if math < 0 or math > 100:
raise ValueError('math score must >= 0 and < 100')
self.math = math
但这样也没解决问题,因为分数虽然在初始化时不能为负,但后续修改时还是可以输入非法值:
>>> stu = Student("a", 90)
>>> stu.math
90
>>> stu.math = -100
>>> stu.math
-100
为了解决以上问题,可以使用@property
。代码如下:
class Student:
def __init__(self, name, math):
self.name = name
self._math = math
@property
def math(self):
# self.math 取值
return self._math
@math.setter
def math(self, value):
# self.math 赋值
if value < 0 or value > 100:
raise ValueError('math score must >= 0 and < 100')
self._math = value
简单来说就是 @property 接管了对 math 属性的直接访问,而是将对应的取值赋值转交给 @property 封装的方法。
虽然 @property 已经表现得比较完美了,但是它最大的问题是不能重用。如果要同时保存其他课程的成绩,这个类就会变成这样:
class Student:
def __init__(self, name, math, chinese, english):
self.name = name
self.math = math
self.chinese = chinese
self.english = english
@property
def math(self):
return self._math
@math.setter
def math(self, value):
if 0 <= value <= 100:
self._math = value
else:
raise ValueError("Valid value must be in [0, 100]")
@property
def chinese(self):
return self._chinese
@chinese.setter
def chinese(self, value):
if 0 <= value <= 100:
self._chinese = value
else:
raise ValueError("Valid value must be in [0, 100]")
@property
def english(self):
return self._english
@english.setter
def english(self, value):
if 0 <= value <= 100:
self._english = value
else:
raise ValueError("Valid value must be in [0, 100]")
虽然外部调用时依然简洁,但掩盖不了类内部的臃肿。
描述符就可以很好的解决上面的代码重用问题。
描述符,一个实现了 描述符协议 的类就是一个描述符。
什么描述符协议:在类里实现了 __get__()
、__set__()
、__delete__()
其中至少一个方法。
__get__
:用于访问属性。它返回属性的值,若属性不存在、不合法等都可以抛出对应的异常。__set__
:将在属性分配操作中调用。不会返回任何内容。__delete__
:控制删除操作。不会返回内容。__get__
方法中有三个参数:
__set__
方法中也有三个参数:
前面的例子中,我们把分数抽象为描述符,代码改为如下:
class Score:
def __init__(self, default=0):
self._score = default
def __set__(self, instance, value):
if not isinstance(value, int):
raise TypeError('Score must be integer')
if not 0 <= value <= 100:
raise ValueError('Valid value must be in [0, 100]')
self._score = value
def __get__(self, instance, owner):
return self._score
def __delete__(self):
del self._score
class Student:
math = Score(0)
chinese = Score(0)
english = Score(0)
def __init__(self, name, math, chinese, english):
self.name = name
self.math = math
self.chinese = chinese
self.english = english
def __repr__(self):
return "" .format(
self.name, self.math, self.chinese, self.english
)
当从 Student 的实例访问 math、chinese、english这三个属性的时候,都会经过 Score 类里的三个特殊的方法。这里的 Score 避免了 使用Property 出现大量的代码无法复用的尴尬。
描述符给我们带来的编码上的便利,它在实现 保护属性不受修改、属性类型检查 的基本功能,同时有大大提高代码的复用率。
描述符可以用一句话概括:描述符是可重用的属性,它把函数调用伪装成对属性的访问。
描述符分两种:
__get__
和 __set__
两种方法的描述符__get__
一种方法的描述符数据描述器和非数据描述器的区别在于:它们相对于实例的字典的优先级不同。
如果实例字典中有与描述符同名的属性,如果描述符是数据描述符,优先使用数据描述符,如果是非数据描述符,优先使用字典中的属性。
看下面一个例子:
数据描述符
class DataDes:
def __init__(self, default=0):
self._score = default
def __set__(self, instance, value):
self._score = value
def __get__(self, instance, owner):
print("访问数据描述符里的 __get__")
return self._score
# 非数据描述符
class NoDataDes:
def __init__(self, default=0):
self._score = default
def __get__(self, instance, owner):
print("访问非数据描述符里的 __get__")
return self._score
class Student:
math = DataDes(0)
chinese = NoDataDes(0)
def __init__(self, name, math, chinese):
self.name = name
self.math = math
self.chinese = chinese
def __getattribute__(self, item):
print("调用 __getattribute__")
return super(Student, self).__getattribute__(item)
def __repr__(self):
return "" .format(
self.name, self.math, self.chinese)
上面例子中,math 是数据描述符,而 chinese 是非数据描述符。从下面的验证中,可以看出,当实例属性和数据描述符同名时,会优先访问数据描述符(如下面的math),而当实例属性和非数据描述符同名时,会优先访问实例属性(__getattribute__
)。
>>> std = Student('xm', 88, 99)
>>>
>>> std.math
调用 __getattribute__
访问数据描述符里的 __get__
88
>>> std.chinese
调用 __getattribute__
99
当访问对象的某个属性时,其查找链简单来说就是:
__dict__
中查找此属性。描述符有一个非常迷惑人的特性:在同一个类中每个描述符仅实例化一次,也就是说所有实例共享该描述符实例。
看下面一个例子:
class NonNegative:
"""检查输入值不能为负"""
def __get__(self, instance, owner=None):
return self.value
def __set__(self, instance, value):
if value < 0:
raise ValueError(f'{self.name} score must >= 0')
# 数据被绑定在描述符实例上
# 由于描述符实例是共享的
# 因此数据也只有一份被共享
self.value = value
class Score:
math = NonNegative()
def __init__(self, math):
self.math = math
score_1 = Score(10)
score_2 = Score(20)
# 所有对象共享同一个描述符实例
print(score_1.math, score_2.math)
# 输出: 20 20
score_1.math = 30
print(score_1.math, score_2.math)
# 输出: 30 30
修改某个实例的值后,所有实例跟着一起改变了。这通常不是你想要的结果。
要破除这种共享状态,比较好的解决方式是将数据绑定到使用描述符的对象实例上,就像开头的例子所做的那样:
class NonNegative:
"""检查输入值不能为负"""
def __init__(self, name):
self.name = name
def __get__(self, instance, owner=None):
return instance.__dict__.get(self.name)
def __set__(self, instance, value):
if value < 0:
raise ValueError(f'{self.name} score must >= 0')
# 数据被绑定在描述符附加的对象上
# 因此保持了对象之间的数据隔离
instance.__dict__[self.name] = value
class Score:
math = NonNegative('math')
def __init__(self, math):
self.math = math
唯一有些不爽的是,为了给数据属性规定一个名字,在定义描述符的时候 NonNegative(‘math’) 还得传递 math 这个名字进去,有点多此一举。
幸好 Python 3.6 为描述符引入了 __set_name__
方法,现在你可以这样:
class NonNegative:
# 注意这里
# __init__ 也没有了
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner=None):
return instance.__dict__.get(self.name)
def __set__(self, instance, value):
if value < 0:
raise ValueError(f'{self.name} score must >= 0')
instance.__dict__[self.name] = value
class Score:
# NonNegative() 不需要带参数以规定属性名了
math = NonNegative()
def __init__(self, math):
self.math = math
用描述符实现一个规范的验证器。
首先定义一个仅具有基础功能的验证器抽象基类:
from abc import ABC, abstractmethod
class Validator(ABC):
"""验证器抽象基类"""
def __set_name__(self, owner, name):
self.private_name = '_' + name
def __get__(self, instance, owner=None):
return getattr(instance, self.private_name)
def __set__(self, instance, value):
self.validate(value)
setattr(instance, self.private_name, value)
@abstractmethod
def validate(self, value):
pass
Validator 描述符类定义了 validate 方法,用于子类覆写以执行具体的验证逻辑。__get__
和 __set__
表明这是类是数据描述符。
写好这个基类,接下来就可以写实际用到的验证器子类了。
比如写两个子类:
class OneOf(Validator):
"""字符串单选验证器"""
def __init__(self, *options):
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 Number(Validator):
"""数值类型验证器"""
def validate(self, value):
if not isinstance(value, (int, float)):
raise TypeError(f'Expected {value!r} to be an int or float')
OneOf 用于确保输入值为固定的某种类型。Number 用于确保输入值必须为数值型。它们均以 Validator 为父类,并实现了 validate 方法。
像这样使用它们:
class Component:
kind = OneOf('wood', 'metal', 'plastic')
quantity = Number()
def __init__(self, kind, quantity):
self.kind = kind
self.quantity = quantity
实际操作试试效果:
>>> Component('abc', 100)
# 失败,'abc' 不在选择范围中
ValueError: Expected 'abc' to be one of {'metal', 'plastic', 'wood'}
>>> Component('wood', 'notNum')
# 失败,'notNum' 不是数值型
TypeError: Expected 'notNum' to be an int or float
>>> Component('wood', 100)
# 成功,参数均合法
Out[25]: <__main__.Component at 0x13df8059640>
再试试赋值:
>>> c = Component('wood', 100)
>>> c.kind = 'abc'
ValueError: Expected 'abc' to be one of {'metal', 'plastic', 'wood'}
>>> c.kind
'wood'
>>> c.kind = 'metal'
>>> c.kind
'metal'
>>> c.quantity = 'haha'
TypeError: Expected 'haha' to be an int or float
>>> c.quantity = 20
>>> c.quantity
20
很顺利的实现了验证器的功能。
利用元类和描述符实现ORM的功能。
元类参考文章:python 元类
import numbers
class Field:
pass
class CharField(Field):
# 数据描述符
# 好处在于可以在各方法中校验传入值的合理性
def __init__(self, col_name, max_length):
if col_name is None or not isinstance(col_name, str):
raise ValueError("col_name must be given as str")
if max_length is None or not isinstance(max_length, numbers.Integral):
raise ValueError("max_length must be given as int")
self._col_name = col_name
self._max_length = max_length
def __get__(self, instance, owner):
# return getattr(instance, self._col_name)
return instance.fields[self._col_name]
def __set__(self, instance, value):
# 这里如果col_name和数据描述符对应的名字一样的话,如name=CharField(col_name="name",10)
# 用setattr(instance, self._col_name, value)即user.name=value会再次进入此__set__方法,导致无限递归
instance.fields[self._col_name] = value
class IntField(Field):
def __init__(self, col_name, min_length, max_length):
self._col_name = col_name
self._min_length = min_length
self._max_length = max_length
def __get__(self, instance, owner):
return instance.fields[self._col_name]
def __set__(self, instance, value):
if value is None or (not isinstance(value, numbers.Integral)):
raise ValueError("value must be given as int")
instance.fields[self._col_name] = value
class ModelMetaClass(type):
# 元类:这个类主要用来实例化我们定义的Model的时候,提前做一些事
def __new__(cls, cls_name, base_class, attrs):
if cls_name == "Model":
return super().__new__(cls, cls_name, base_class, attrs)
fields = {}
for k, v in attrs.items():
if isinstance(v, Field):
fields[k] = v
attrs["fields"] = fields
_meta = {}
attrs_meta = attrs.get("Meta", None)
if attrs_meta is not None and isinstance(attrs_meta, type):
_meta["tb_name"] = getattr(attrs_meta, "tb_name", cls_name)
del attrs["Meta"]
else:
_meta["tb_name"] = cls_name.lower()
attrs["_meta"] = _meta
return super().__new__(cls, cls_name, base_class, attrs)
class Model(metaclass=ModelMetaClass):
# 这个类用来把类属性设置为示例属性
def __init__(self, **kwargs):
self.fields = {}
for k, v in kwargs.items():
setattr(self, k, v)
# def more_func(self):
# pass
class User(Model):
name = CharField(col_name="name", max_length=10)
sex = CharField(col_name="sex", max_length=1)
age = IntField(col_name="age", min_length=1, max_length=10)
class Meta:
tb_name = "User"
class Company(Model):
name = CharField(col_name="name", max_length=10)
address = CharField(col_name="address", max_length=1)
# class Meta:
# tb_name = "Company"
if __name__ == "__main__":
user = User(name="boy1", age=5, sex="男")
user1 = User(name="girl1", age=6, sex="女")
company = Company(name="com", address="China")
print(User.__dict__)
print(user.__dict__)
print(user1.__dict__)
print(Company.__dict__)
print(company.__dict__)
参考:
https://mp.weixin.qq.com/s/fOKzt-XQ4AefZuYfdsmVrQi888888888
https://mp.weixin.qq.com/s/XQPYkEHqUOkatw3JOuB1_A