问: 什么是描述器?
答: 描述器是一个Python对象。
问: 这个Python对象和普通的Python对象有什么区别?
答: 只要具有__get__(), __set__(), __delete__()方法中任意一个方法的对象就叫做描述器。
问: 描述器在功能上有什么特殊之处吗?
答: 一个对象在访问描述器时,如果该属性是一个描述器,则默认属性回调规则会被__set__, __get__,__delete__方法所覆盖。
问: 描述器有什么用?
答: Python内部自带的staticmethod, classmethod, property,super等都是描述器,在很多Python库中也都有描述器的身影(例如SQLAlchemy),使用描述器能让你有更高的概率写出优美的代码、更简洁的API,并会加深对Python理解。
通过上面假象式的QA,希望你对描述器是什么能有初步的了解,接下来正式介绍描述器。
1. 描述器的定义
具有下面三个方法中任意一个方法的Python对象就叫做描述器。
__set__(**)
__get__(**)
__delete__(**)图 1. 描述器协议相关方法
描述器又可分为数据描述器(data descriptor)和非数据描述器(non-data descriptor),只有__get__方法的对象被称为非数据描述器(non-data desriptor),其他的都称为数据描述器(data descriptor)。以上便是描述器的定义,虽然定义简单但用起不一定那么好把握,接下来通过代码片段来分析描述器的特点。
2. 使用描述器
描述器怎么定义?请下面代码(code#1):
# code#1
class PositiveNumber(object):
def __init__(self, name, value):
self.name = name
self._value = value
def __get__(self, obj, dtype):
return self._value
def __set__(self, obj, value):
if value < 0:
raise ValueError('Cannot be negative.')
self._value = value
if __name__ == '__main__':
number = PositiveNumber('number', 10)
上面代码中的number便是一个描述器。直接将描述器作为一个独立对象,并不能展现出描述器的魔力,只有在描述器作为另一个对象的属性的时候,描述器的魔力才能真正展现出来。例如下面代码(code#2):
# code#2
class PositiveNumber(object):
def __init__(self, name, value):
self.name = name
self._value = value
def __get__(self, obj, dtype):
print("call `__get__` method")
print("obj : {}".format(obj))
print("dtype: {}".format(dtype))
return self._value
def __set__(self, obj, value):
if value < 0:
raise ValueError('Cannot be negative.')
print("call `__set__` method")
print("obj : {}".format(obj))
print("set price to {:d}".format(value))
self._value = value
def __delete__(self, obj):
print("call `__delete__` method")
print("obj : {}".format(obj))
print("delete object :{}".format(obj))
class Apple(object):
price = PositiveNumber('price', 0)
def __init__(self):
self.price = 10
上面代码中我将描述器price作为Apple的一个类对象,然后在__init__对其进行赋值操作。在这里停一下,想一下创建一个Apple对象时,会发生什么?
从上面结果可以看出,当我们创建Apple对象的时候,内部自动调用了price描述器的__set__方法。
当我们通过apple对象访问price时,又会发生什么?
从上面结果可以看出,当我们通过apple对象访问price时,内部自动调用了__get__方法。
当我们通过apple对象对price进行赋值操作时,又会发生什么?
从上面结果可知,当通过apple向price赋一个负值的时候,price的__set__方法被调用了,并抛出了ValueError(因为定义中指定了price不能小于零)。
当我们通过del删除apple.price时,又会发生什么?
从上面结果可知,通过del删除apple.price时,内部并没有真正删除price,而是直接调用了price的__delete__方法。
通过上面几行代码,我想你对描述器的工作原理已经有了大致的理解,总结如下:当通过其他对象A访问描述器时,不会直接返回描述器本身,而是调用描述器的__get__方法,并把对象A和A的类(即type(A))作为__get__的输入参数,最后返回__get__方法的返回值。如果没有定义__get__方法,则返回描述器本身。需要注意的是,当对象存在一个与描述器同名的属性时(即同时存在两个名字相同的属性,一个为描述器,另一个为其他普通属性),对象访问该属性的回调顺序会根据描述器的不同(数据描述器和非数据描述器)而不同,如下图所示:Python 对象属性访问回调顺序,图来源[1],侵删
当描述器为数据描述器时,数据描述器会先于对象的普通属性被访问(图蓝色框部分);如果描述器为非数据描述器时,对象的普通属性会先于非数据描述器被访问(图红色框部分)。不信?来飚一波代码?算了,空间有限,放图太占空间,代码给留在这里,有心可以在线运行一下,有相应注释。当通过其他对象A对描述器赋值时,不会把值直接赋给描述器本身,而是调用描述器的__set__方法,并把对象A和赋的值作为__set__的输入参数。如果描述器没有__set__方法(即为非资料描述器),强行对其赋值,此时会抛出AttributeError。如果你想让你的描述器为只读属性,可以先实现__set__方法,然后在其内部进行错误处理,并抛出对应的Error,这样会显得更友好一点。
和属性访问相同的,在给对象属性赋值的时候,也要遵从一定赋值顺序,不过相比于对象属性访问要简单的多,如下图所示。对象属性幅值顺序
简单说,如果赋值属性是数据描述器,则调用数据描述器的__set__进行赋值操作,如果不是,则直接在当前对象的__dict__创建对应的属性,并赋值。当通过del删除描述器时,不会删除描述器本身,而是调用描述器的__delete__方法,并把当前对象作为__delete__的输入参数。
3. 描述器的使用
文章开头提到过Python内部的property,classmethod, staticmethod, super都是描述器,这一节就使用前面讲到描述器简单实现一下property,classmethod和staticmethod。
3.1 实现property
代码(code#3),在线访问点击这里。
class mproperty(object):
def __init__(self, fget, fset=None, fdel=None):
self._fget = fget
self._fset = fset
self._fdel = fdel
def __get__(self, obj, klass):
return self._fget(obj)
def __set__(self, obj, val):
if not hasattr(self._fset, '__call__'):
raise AttributeError("Readonly attribute!")
self._fset(obj, val)
def __delete__(self, obj):
if not hasattr(self._fdel, '__call__'):
raise AttributeError("Can't delete the attribute!")
self._fdel(obj)
def setter(self, fset):
self._fset = fset
return self
def deleter(self, fdel):
self._fdel = fdel
return self
class Apple(object):
def __init__(self, price=0):
self._price = price
@mproperty
def price(self):
return self._price
@price.setter
def price(self, value):
if value < 0:
raise ValueError("Price must be greater than 0")
self._price = value
@price.deleter
def price(self):
print("delete price")
测试:
访问测试访问测试
赋值测试赋值测试
删除测试删除测试
3.2 实现staticmethod
代码(code#4)
class mstaticmethod(object):
def __init__(self, func):
self._func = func
def __get__(self, obj, klass=None):
return self._func
@property
def __func__(self):
return self._func
class Apple(object):
def __init__(self, price=0):
self._price = price
@mstaticmethod
def kg2jin(weight):
# 将千克转换为斤
return weight * 2
测试:staticmethod 测试
3.3 实现classmethod
代码(code#5)
class mclassmethod(object):
def __init__(self, func):
self._func = func
def __get__(self, obj, klass=None):
if klass is None:
klass = type(obj)
def wapper(*args):
return self._func(klass, *args)
return wapper
@property
def __func__(self):
return self._func
class Apple(object):
lowest_price = 10
def __init__(self, price=15):
self._price = price
@mclassmethod
def calc_lowest_total_price(cls, weight):
return cls.lowest_price * weight
测试:classmethod测试
3.4 实现super
想一想,算了,这个时间是大家的表演时间,我就不参合了。
此处我想说,,,,。其实我没什么想说的,该回去睡觉了→_→。
参考与荐读: