描述符
在Python中,描述符作为一个用语言描述起来会有些抽象的概念。其定义有如下说法:
一般来说,描述符是一个具有绑定行为的对象属性,其属性的访问被描述符协议方法覆写。这些方法是__get__()、 __set__()和__delete__(),一个对象中只要包含了这三个方法(译者注:包含至少一个),就称它为描述符。
而我认为,如果可以通俗地去解释“为什么需要描述符”,会让初学者更易接纳。
如果你知道Python中的property——也可能不曾听过,但你可以看我的@property——就会晓得我们可以对实例属性操作时做一些限制,比方说陌生人的姓名必须得字符串类型(这里当然会有些不严谨,因为Python中如"512"
也是字符串类型),如果不是,就抛异常TypeError。代码可以如下:
class Stranger(object):
def __init__(self, name):
self._name = name
@property
def name(self):
return self._name
@name.setter
def name(self, value):
if not isinstance(value, str):
raise TypeError("我期待一个字符串")
else:
self._name = value
if __name__ == "__main__":
stger = Stranger("Ying")
stger.name = 512
运行结果如下:
乍一看,上述代码似乎满足了我们的需求。其实不然,存在两个缺点。
其一,当我们初始化一个对象,就给name传int类型的参数,程序会默然允许:
if __name__ == "__main__":
stger = Stranger(512)
print(type(stger.name))
# 输出:
# <class 'int'>
其二,当我们需要限制的属性较多时:
class Stranger(object):
def __init__(self, name, age, hobbies, addr, school):
self._name = name
self._age = age
self._hobbies = hobbies
...
为了一一限制,便不得不对应的去定义@name.setter,@age.setter ...... 代码怎么不优雅了?
Python的代码需要优雅,这就是引进描述符的原因。你也可以认为描述符是property的升级版。
描述符协议
描述符协议包含:
__get__
用于访问属性的值。当请求的属性不存在时,抛出AttributeError
__set__
设置操作__delete__
删除操作
一个对象中只要包含了这三个方法中的一个,就称它为描述符。
示例
import numbers
class InterField(object):
def __get__(self, instance, owner):
return self.value
def __set__(self, instance, value):
if not isinstance(value, numbers.Integral):
raise ValueError("期待一个整数")
else:
self.value = value
class CharField(object):
def __get__(self, instance, owner):
return self.value
def __set__(self, instance, value):
if not isinstance(value, str):
raise ValueError("期待一个字符串")
else:
self.value = value
class Stranger(object):
name = CharField()
age = InterField()
hobbies = CharField()
addr = CharField()
school = CharField()
def __init__(self, name, age, hobbies, addr, school):
self.name = name
self.age = age
self.hobbies = hobbies
self.addr = addr
self.school = school
if __name__ == "__main__":
stger = Stranger("Ying", 22, "music", "chengdu", "XHU")
print(stger.name, stger.age, stger.hobbies)
# 输出:
# Ying 22 music
此时
stger = Stranger("Ying", "22", "music", "chengdu", "XHU")
或者
stger = Stranger("Ying", 22, "music", "chengdu", "XHU")
stger.name = 512
资料描述符与非资料描述符
认为,如果一个描述符只定义了__get__
方法,则为非资料描述符;如果同时定义了__get__
和__set__
,为资料描述符:
资料描述符:当类属性与实例属性同名时,优先访问类属性
class CharField(object): def __init__(self, value): self.value = value def __get__(self, instance, owner): return self.value # 此时,我甚至不需要对__set__添加任何逻辑 def __set__(self, instance, value): pass class Stranger(object): # 这是一个资料描述符 name = CharField("Guan") def __init__(self): self.name = "Ying" if __name__ == "__main__": stger = Stranger() print(stger.name) # 输出: # Guan
非资料描述符:当类属性与实例属性同名时,优先访问实例属性
class CharField(object): def __init__(self, value): self.value = value def __get__(self, instance, owner): return self.value class Stranger(object): # 这是一个非资料描述符 name = CharField("Guan") def __init__(self): self.name = "Ying" if __name__ == "__main__": stger = Stranger() print(stger.name) # 输出: # Ying
描述符的陷阱
第一点:描述符必须在类的层次上(类属性)
...
class Stranger(object):
def __init__(self):
self.name = CharField("Guan")
if __name__ == "__main__":
stger = Stranger()
print(stger.name)
# 输出:
# <__main__.CharField object at 0x7ff3178f1cc0>
这是因为:只有类层次上的描述符才会默认调用\_get_。
第二点:确保实例属性属于实例本身
事实上类属性是该类实例对象共有的,可参看我的类属性与实例属性。所以很可能出现“一荣俱荣,一损俱损”的现象:
class CharField(object):
def __get__(self, instance, owner):
return self.value
def __set__(self, instance, value):
self.value = value
class Stranger(object):
name = CharField()
if __name__ == "__main__":
g = Stranger()
g.name = "Guan"
print(g.name) # 输出: Guan
y = Stranger()
y.name = "Ying"
print(g.name) # 输出: Ying
print(y.name) # 输出: Ying
解决方案:
在描述符类中维护一个字典,将每个实例对象作为字典的key,而类属性对应的值作为字典的value:
class CharField(object):
def __init__(self):
# 一些实例使用的WeakKeyDictionary()类型字典
# 暂时我并不能明白为什么
self.data = dict()
def __get__(self, instance, owner):
return self.data.get(instance)
# 当y.name时,instance == y
# 当g.name时,instance == g
def __set__(self, instance, value):
self.data[instance] = value
这样做的缺点是,也许会碰到不可以被hash的实例,那么就不能作为字典的key。常用的解决方案是:为描述符增加标签,并通过这个标签,把本来应该访问类属性这一过程,“偷偷地”转化成访问实例属性的过程:
class CharField(object):
def __init__(self, label):
self.label = label
def __get__(self, instance, owner):
return instance.__dict__.get(self.label)
def __set__(self, instance, value):
instance.__dict__[self.label] = value
class MyList(list):
name = CharField("NAME")
这种做的缺点是,你可能会在没有察觉的情况下修改了name值:
mylist = MyList()
mylist.name = "Guan" # 这里是name
print(mylist.name) # 输出: Guan
mylist.NAME = "Ying" # 这里是NAME
print(mylist.name) # 输出:Ying
建议标签名最好与描述符对象的名字相同。
最后可以利用元类来简化这个过程:
class LabelType(type):
def __new__(cls, name, bases, attrs):
for k, v in attrs.items():
if isinstance(v, CharField):
v.label = k # 为描述符增加label
return super().__new__(cls, name, bases, attrs)
class CharField(object):
def __get__(self, instance, owner):
return instance.__dict__.get(self.label)
def __set__(self, instance, value):
instance.__dict__[self.label] = value
class MyList(list, metaclass=LabelType):
name = CharField()
如果你对元类不熟悉,不妨参看我的元类。
还不快抢沙发