引入描述器
以stackoverflow上关于描述器(descriptor )的疑问开篇。
以上代码实现了温度的摄氏温度和华氏温度之间的自动转换。其中Temperature类含有实例变量fahrenheit和类变量celsius,celsius由描述器Celsius进行代理。由这段代码引出的三点疑问:
- 疑问一:什么是描述器?
- 疑问二:
__get__
,__set__
,__delete__
三种方法的参数 - 疑问三:描述器有哪些应用场景
- 疑问四:property和描述器的区别是什么?
疑问一:什么是描述器?
描述器是一个 实现了 __get__
、 __set__
和__delete__
中1个或多个方法的类对象。当一个类变量指向这样的一个装饰器的时候, 访问这个类变量会调用__get__
方法, 对这个类变量赋值会调用__set__
方法,这种类变量就叫做描述器。
描述器 事实上是一种代理机制:当一个类变量被定义为描述器,对这个类变量的操作,将由此描述器来代理。
疑问二:描述器三种方法的参数
由以上输出结果可以得出结论:
参数解释
__get__(self, instance, owner)
instance 表示当前实例 owner 表示类本身, 使用类访问的时候, instance为None__set__(self, instance, value)
instance 表示当前实例, value 右值, 只有实例才会调用 __set__
__delete__(self, instance)
instance 表示当前实例
三种方法的本质
- 访问:
instance.descriptor
实际是调用了descriptor.__get__(self, instance, owner)
方法,并且需要返回一个value - 赋值:
instance.descriptor = value
实际是调用了descriptor.__set__(self, instance, value)
方法,返回值为None。 - 删除:
del instance.descriptor
实际是调用了descriptor.__delete__(self, obj_instance)
方法,返回值为None
疑问三:描述器有哪些应用场景
我们想创建一种新形式的实例属性,除了修改、访问之外还有一些额外的功能,例如 类型检查、数值校验等,就需要用到描述器 《Python Cookbook》
即描述器主要用来接管对实例变量的操作。
实现classmethod装饰器
将方法fn的第一个参数固定成实例的类。可参考python官方文档的另一种写法:descriptor
实现staticmethod装饰器
实现property装饰器
使用自定义的Property来描述farenheit和celsius类变量:
使用结果:
使用装饰器的方式来装饰Temperature的两个属性farenheit和celsius:
使用结果同直接用描述器描述类变量
实现属性的类型检查
首先实现一个类型检查的描述器Typed
然后实现一个Person类,Person类的属性name和age都由Typed来描述
类型检查过程:
但是上述类型检查的方法存在一些问题,Person类可能有很多属性,那么每一个属性都需要使用Typed描述器描述一次。我们可以写一个带参数的类装饰器来解决这个问题:
然后使用typeassert类装饰器重新定义Person类:
可以看到typeassert类装饰器的参数是传入的属性名称和类型的键值对。
如果我们想让typeassert类装饰器自动的识别类的初始化参数类型,并且增加相应的类变量的时候,我们就可以借助inspect库和python的类型注解实现了:
疑问四:property和描述器的区别
我们可以利用Python的内部机制获取和设置属性值。总共有三种方法:
- Getters和Setter。我们可以使用方法来封装每个实例变量,获取和设置该实例变量的值。为了确保实例变量不被外部访问,可以把这些实例变量定义为私有的。所以,访问对象的属性需要通过显式函数:anObject.setPrice(someValue); anObject.getValue()。
- property。我们可以使用内置的property函数将getter,setter(和deleter)函数与属性名绑定。因此,对属性的引用看起来就像直接访问那么简单,但是本质上是调用对象的相应函数。例如,anObject.price = someValue; anObject.value。
- 描述器。我们可以将getter,setter(和deleter)函数绑定到一个单独的类中。然后,我们将该类的对象分配给属性名称。这时候对每个属性的引用也像直接访问一样,但是本质上是调用这个描述器对象相应的方法,例如,anObject.price = someValue; anObject.value。
Getter和Setter这种设计模式不够Pythonic,虽然在C++和JAVA中很常见,但是Python追求的是简介,追求的是能够直接访问。
附1、data-descriptor and no-data descriptor
翻译为中文其实就是资料描述器和非资料描述器
- data-descriptor:同时实现了
__get__
和__set__
方法的描述器 - no-data descriptor:只实现了
__get__
方法的描述器
两者的区别在于:
- no-data descriptor的优先级低于
instance.__dict__
- data descriptor的优先级高于
instance.__dict__
附2、描述器机制分析资料:
- 官方文档-descriptor
- understanding-get-and-set-and-python-descriptors
- anyisalin - Python - 描述器
- Python描述器引导(翻译)
- Properties and Descriptors