目录

  • 引入
  • 什么是描述符
  • 描述符类型
  • 共享陷阱
  • 应用场景
    • 验证器
    • ORM

描述符是 Python 语言中一个强大的特性,它隐藏在编程语言的底层,为许多神奇的魔法提供了便利。

引入

假设你需要一个学生类,来记录考试的分数。简单写如下:

class Student:def __init__(self, name, math):self.name = nameself.math = math

但是稍后发现分数为负值或者大于100是不合理的,上面的代码对输入参数没有任何检查。如下:

>>> stu = Student("a", -90)
>>> stu.math
-90

于是修改代码做限制:

class Student:def __init__(self, name, math):self.name = nameif 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 = nameself._math = math@propertydef math(self):# self.math 取值return self._math@math.setterdef 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 = nameself.math = mathself.chinese = chineseself.english = english@propertydef math(self):return self._math@math.setterdef math(self, value):if 0 <= value <= 100:self._math = valueelse:raise ValueError("Valid value must be in [0, 100]")@propertydef chinese(self):return self._chinese@chinese.setterdef chinese(self, value):if 0 <= value <= 100:self._chinese = valueelse:raise ValueError("Valid value must be in [0, 100]")@propertydef english(self):return self._english@english.setterdef english(self, value):if 0 <= value <= 100:self._english = valueelse:raise ValueError("Valid value must be in [0, 100]")

虽然外部调用时依然简洁,但掩盖不了类内部的臃肿。

描述符就可以很好的解决上面的代码重用问题。

什么是描述符

描述符,一个实现了 描述符协议 的类就是一个描述符。

什么描述符协议:在类里实现了 __get__()__set__()__delete__() 其中至少一个方法。

  • __get__:用于访问属性。它返回属性的值,若属性不存在、不合法等都可以抛出对应的异常。
  • __set__:将在属性分配操作中调用。不会返回任何内容。
  • __delete__:控制删除操作。不会返回内容。

__get__ 方法中有三个参数:

  • self· :描述符实例
  • instance :描述符所附加的对象的实例
  • owner :描述符所附加的对象的类型

__set__ 方法中也有三个参数:

  • self :描述符实例
  • instance :描述符所附加的对象的实例
  • value :当前准备赋的值

前面的例子中,我们把分数抽象为描述符,代码改为如下:

class Score:def __init__(self, default=0):self._score = defaultdef __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 = valuedef __get__(self, instance, owner):return self._scoredef __delete__(self):del self._scoreclass Student:math = Score(0)chinese = Score(0)english = Score(0)def __init__(self, name, math, chinese, english):self.name = nameself.math = mathself.chinese = chineseself.english = englishdef __repr__(self):return "<Student: {}, math:{}, chinese: {}, english:{}>".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 = defaultdef __set__(self, instance, value):self._score = valuedef __get__(self, instance, owner):print("访问数据描述符里的 __get__")return self._score# 非数据描述符
class NoDataDes:def __init__(self, default=0):self._score = defaultdef __get__(self, instance, owner):print("访问非数据描述符里的 __get__")return self._scoreclass Student:math = DataDes(0)chinese = NoDataDes(0)def __init__(self, name, math, chinese):self.name = nameself.math = mathself.chinese = chinesedef __getattribute__(self, item):print("调用 __getattribute__")return super(Student, self).__getattribute__(item)def __repr__(self):return "<Student: {}, math:{}, chinese: {},>".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

当访问对象的某个属性时,其查找链简单来说就是:

  1. 首先在对应的数据描述符中查找此属性。
  2. 如果失败,则在对象的 __dict__中查找此属性。
  3. 如果失败,则在非数据描述符中查找此属性。
  4. 如果失败,再去别的地方查找。(本文就不展开了)

共享陷阱

描述符有一个非常迷惑人的特性:在同一个类中每个描述符仅实例化一次,也就是说所有实例共享该描述符实例。

看下面一个例子:

class NonNegative:"""检查输入值不能为负"""def __get__(self, instance, owner=None):return self.valuedef __set__(self, instance, value):if value < 0:raise ValueError(f'{self.name} score must >= 0')# 数据被绑定在描述符实例上# 由于描述符实例是共享的# 因此数据也只有一份被共享self.value = valueclass Score:math = NonNegative()def __init__(self, math):self.math = mathscore_1 = Score(10)
score_2 = Score(20)# 所有对象共享同一个描述符实例
print(score_1.math, score_2.math)
# 输出: 20 20score_1.math = 30
print(score_1.math, score_2.math)
# 输出: 30 30

修改某个实例的值后,所有实例跟着一起改变了。这通常不是你想要的结果。

要破除这种共享状态,比较好的解决方式是将数据绑定到使用描述符的对象实例上,就像开头的例子所做的那样:

class NonNegative:"""检查输入值不能为负"""def __init__(self, name):self.name = namedef __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] = valueclass 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 = namedef __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] = valueclass Score:# NonNegative() 不需要带参数以规定属性名了math = NonNegative()def __init__(self, math):self.math = math

应用场景

验证器

用描述符实现一个规范的验证器。

首先定义一个仅具有基础功能的验证器抽象基类:

from abc import ABC, abstractmethodclass Validator(ABC):"""验证器抽象基类"""def __set_name__(self, owner, name):self.private_name = '_' + namedef __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)@abstractmethoddef 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     = kindself.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

利用元类和描述符实现ORM的功能。

元类参考文章:python 元类

import numbersclass Field:passclass 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_nameself._max_length = max_lengthdef __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] = valueclass IntField(Field):def __init__(self, col_name, min_length, max_length):self._col_name = col_nameself._min_length = min_lengthself._max_length = max_lengthdef __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] = valueclass 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] = vattrs["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"] = _metareturn 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):#     passclass 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

python 描述符相关推荐

  1. python描述符(descriptor)、属性(property)、函数(类)装饰器(decorator )原理实例详解

    2019独角兽企业重金招聘Python工程师标准>>> 1.前言 Python的描述符是接触到Python核心编程中一个比较难以理解的内容,自己在学习的过程中也遇到过很多的疑惑,通过 ...

  2. python有哪些作用-python描述符有什么作用

    python描述符的作用:代理一个类的属性,让程序员在引用一个对象属性时自定义要完成的工作:它是实现大部分Python类特性中最底层的数据结构的实现手段,是使用到装饰器或者元类的大型框架中的一个非常重 ...

  3. Python描述符是什么?

    在Python中,通过使用描述符,程序员可以在引用对象属性时定制要完成的工作,接下来我们一起来聊聊Python描述符相关的知识. 本质上,描述符是一个类,但它定义了另一个类中属性的访问模式.换句话说, ...

  4. python描述符详解_Python描述符 (descriptor) 详解

    1.什么是描述符? python描述符是一个"绑定行为"的对象属性,在描述符协议中,它可以通过方法重写属性的访问.这些方法有 __get__(), __set__(), 和__de ...

  5. python 描述符类_python的黑魔法--描述符

    python的黑魔法 描述符 官方定义:python描述符是一个"绑定行为"的对象属性,在描述符协议中,它可以通过方法重写属性的访问.这些方法有 get(), set(), 和de ...

  6. python 描述符有什么用_介绍python描述符的意义

    你也许经常会听到「描述符」这个概念,但是由于大多数的程序员很少会使用到他,所以可能你并不太清楚了解它的原理,python视频教程栏目将详细介绍 推荐(免费):python视频教程 但是如果你想自己的事 ...

  7. 技术图文:Python描述符 (descriptor) 详解

    背景 今天在B站上学习"零基础入门学习Python"这门课程的第46讲"魔法方法:描述符",这也是我们组织的 Python基础刻意练习活动 的学习任务,其中有这 ...

  8. python 描述符参考文档_python 描述符详解

    Python中包含了许多内建的语言特性,它们使得代码简洁且易于理解.这些特性包括列表/集合/字典推导式,属性(property).以及装饰器(decorator).对于大部分特性来说,这些" ...

  9. python描述符与实例属性_Python 中的属性访问与描述符

    在Python中,对于一个对象的属性访问,我们一般采用的是点(.)属性运算符进行操作.例如,有一个类实例对象foo,它有一个name属性,那便可以使用foo.name对此属性进行访问.一般而言,点(. ...

  10. python描述符详解

    什么是描述符 数据描述符data descriptor和非数据描述符non-data descriptors 如何检测一个对象是不是描述符 描述符有什么用和好处 例子 总结 本文主要介绍描述符的定义, ...

最新文章

  1. IOT数据库选型——NOSQL,MemSQL,cassandra,Riak或者OpenTSDB,InfluxDB
  2. Matlab-重构和重新排列数组
  3. Spring和Amazon Web Services
  4. Windows Server 2003服务器安装前设置
  5. mysql大数据量分页的一些做法
  6. ipone怎么没有科学计算机,ipone7与ipone8其实根本没什么区别呀
  7. 两种思想实现基于jquery的延时导航菜单,可做延时触发器!
  8. Deep Learning Papers
  9. 自己封装了的AlertController
  10. Flex in a Week video training
  11. Python 小白学习
  12. mybatis直接执行sql_拼多多二面:Mybatis是如何执行一条SQL命令的?
  13. CSS中背景图片的坐标之使用说明及css中把所有背景图都放在一张图片上减少图片服务器的请求次数问题(转)...
  14. 【数据分析】数据分析方法(一):5W2H 分析方法
  15. 无线鼠标没反应怎么办
  16. 解决“连接U8数据库服务器失败”的方法尝试
  17. JS验证电话和传真号码格式
  18. 水仙花数(Java实现)
  19. opensuse 下 sled 11sp2 下安装 转换 deb 到rpm 通过alien fr net
  20. java 工作两年的简历_工作经验只有两年的Java开发,简历中需要写学校经历吗?...

热门文章

  1. 自考大专和函授大专有什么区别
  2. 上市公司高管薪酬比例(2005-2018年)
  3. java数组索引越界异常如何解决_java之ArrayIndexOutOfBoundsException数组越界与IndexOutOfBoundsException索引越界之间关系...
  4. mysql考试ocm_OCM考试中Dataguar的配置
  5. python项目实战大合集
  6. 在IE浏览器中使用Windows窗体控件(三)
  7. linux 拷贝覆盖文件,Linux取消cp命令覆盖文件提示的方法
  8. 数据分析 --- python基础day07
  9. 17.letterCombinations
  10. 川贝母修饰卵清蛋白(Fritillaria thunbergii-OVA/Ovalbumin )