都说Python是优雅的语言,在我看来Python的语法和实现给人一种自然而然的感觉,比如容器的继承机制,迭代器和生成器,上下文管理,以及各种双下划线开头的特殊方法。了解Python的设计之后,就会觉得本该是这个样子,毫无违和感。唯一例外的是描述符(Descriptor),这玩意对Python而言可是举足轻重,但是从一般用户的角度去看就有点生硬,不知道为什么会存在这样的东西,特别是关于访问优先级,看上去就是纯粹的人为规则,一点都不自然,需要硬记。所以尝试重新理解了一下描述符并记录下来。
首先明确了,描述符的作用是用来代理另外一个类的属性,也就是提供了更细粒度的访问控制。当然,必须把描述符定义成类成员而非实例成员,这样的设计可能和该特性的具体实现有关,在没搞清楚之前就当是个死规定。我们知道通常而言类的成员可被全部实例所共享,每个实例自己还可以有一个同名的成员覆盖类成员。但是在存取上无法实现细节的控制,比如最简单的一个类:
class Person:
def __init__(self, name):
if not isinstance(name, str):
raise TypeError("`name` should be a string")
self.name = name
xiao_ming = Person("Xiao Ming")
xiao_ming.name = 123
为了避免name被赋值为非字符串,只能在构造函数内检测,如果有多个类似的成员变量,那么就要在构造函数里写上一堆东西,何况还无法阻止实例化以后再改变name的值。有了描述符就好办多了,若有个类属性是数据描述符对象,此时会优先调用描述符中定义的方法:
class Name(object):
dic = {}
def __init__(self, value_type):
self.value_type = value_type
def __get__(self, instance, owner):
print("get name")
return self.dic.get(instance, None)
def __set__(self, instance, value):
if not isinstance(value, self.value_type):
print("failed to set name")
else:
print("set name")
self.dic[instance] = value
class Person(object):
name = Name(str)
def __init__(self, name):
self.name = name
xiao_ming = Person("Xiao Ming")
print(xiao_ming.name)
xiao_ming.name = 123
#====== Output =======
#
# set name
# get name
# Xiao Ming
# failed to set name
这样看来,我们把访问一个成员的细节抽离了出来,况且这个Name类还可以重用,比如取名为StringClass,更加一目了然。描述符类以及调用它的类都应该写成继承object的新式类。若描述符类仅仅定义了__get__而没有__set__和__delete__则是个非数据描述符,否则是数据描述符。接下来是访问优先级,网上有大量的说法是这样的:
1. 类属性
2. 数据描述符
3. 实例属性
4. 非数据描述符
5. 找不到的属性触发__getattr__()
个人认为把类属性排在第一位是有误导性的。从类的角度看,对类属性直接赋值,确实可以替换掉描述符,毕竟描述符本质上也是个类属性,那么通过类名来给某个属性赋值,理所当然应该覆盖之前的值(描述符对象),不然就违背了Python的语义。但是访问实例的成员,并不会首先去访问普通的类属性,当然,除非它是个数据描述符。后来看到另一段描述,显得靠谱多了:
如果user是某个类的实例,那么 user.age 以及等价的 getattr(user,’age’),首先调用__getattribute__。
如果类定义了__getattr__方法,那么在__getattribute__抛出 AttributeError 的时候就会调用到__getattr__,
而对于描述符__get__的调用,则是发生在__getattribute__内部。
user = User(), 那么user.age 顺序如下:
(1)如果“age”是出现在User或其基类的__dict__中, 且age是data descriptor, 那么调用其__get__方法, 否则
(2)如果“age”出现在user的__dict__中, 那么直接返回 obj.__dict__[‘age’], 否则
(3)如果“age”出现在User或其基类的__dict__中
(3.1)如果age是non-data descriptor,那么调用其__get__方法, 否则
(3.2)返回 __dict__[‘age’]
(4)如果User有__getattr__方法,调用__getattr__方法,否则
(5)抛出AttributeError
数据描述符必须优先于普通实例的成员,这个好理解,不然还提什么访问控制呢,但是为什么仅有__get__方法的非数据描述符就排后面去了呢。简单的想想,既然只有getter没有setter,那么遇到最简单的 instance.attribute = value 这样的语句要怎么办呢,既然非数据描述符无法处理,那么只能把它当成一个普通的实例成员来看待了,为保持一致,执行 print(instance.attribute) 也没必要走非数据描述符的__get__方法了,这么想似乎说得通了。那么非数据描述符还有什么作用呢,我认为可以用于延迟计算,根据传入的实例的成员来做一些新的计算,或者隐藏某些私有成员,若有必要可以将计算好的值重新赋予实例成员,这样下次访问就无须重复计算,因为实例成员的优先级更高。当然如果不用非数据描述符,我们还可以通过定义方法来达到同样目的,只不过 instance.attribute 用起来肯定比 instance.attribute() 要更舒服吧。可能有人会说,这不就是Python中的 property 吗,还真没错,property 底层正是用描述符实现的。
https://github.com/mzohaibqc/python-attribute-lookup 这个地方有两张很棒的流程图,一个是实例成员的查找顺序,一个是类成员的查找顺序,一目了然:
有了描述符,便可以实现 property,不过都是借用装饰器语法糖而已:
class Property(object):
"""Uused as a class decorator which returns a descriptor object."""
def __init__(self, func):
self.get_func = func
self.set_func = None
self.delete_func = None
def __get__(self, instance, owner):
if instance is None:
return self
return self.get_func(instance)
def setter(self, func):
self.set_func = func
return self
def __set__(self, instance, value):
return self.set_func(instance, value)
def deleter(self, func):
self.delete_func = func
return self
def __delete__(self, instance):
return self.delete_func(instance)
class Person(object):
def __init__(self, name):
self.__name = name
@Property # name = Property(name)
def name(self):
return self.__name
@name.setter
def name(self, value):
if not isinstance(value, str):
raise TypeError("`Name` must be a string")
self.__name = value
@name.deleter
def name(self):
print("Cannot delete `name`")
xiao_ming = Person("Xiao Ming")
print(xiao_ming.name)
xiao_ming.name = 'Da Ming'
print(xiao_ming.name)
del xiao_ming.name
同样可以实现 classmethod 和 staticmethod:
class ClassMethod:
def __init__(self, func):
self.get_func = func
self.cls = None
def __get__(self, instance, owner):
self.cls = owner
return self
def __call__(self, *args, **kwargs):
return self.get_func(self.cls, *args, **kwargs)
class StaticMethod:
def __init__(self, func):
self.get_func = func
def __get__(self, instance, owner):
return self
def __call__(self, *args, **kwargs):
return self.get_func(*args, **kwargs)
class Person:
def __init__(self, name):
self.name = name
def eat(self, sth):
print("{} is eating {}".format(self.name, sth))
@ClassMethod
def friendship(cls, sth):
print("{} and {} are friends".format(cls.__name__, sth))
@StaticMethod
def live(sth):
print("Everyone lives on {}".format(sth))
xiao_ming = Person("Xiao Ming")
xiao_ming.eat("rice")
xiao_ming.friendship("dog")
xiao_ming.live("earth")