Python中的描述符

什么是描述符

python是一种动态的编程语言,因此不能像java那样在编译的时候确定对象的类型,如果我们希望能够像java中的那样规定某个实例属性的类型时该怎么做呢?这就需要用到描述符了。

描述符是实现了特定协议的类, 这个协议就是描述符协议。

描述符协议包括以下三种方法:

  1. get: 用于访问属性
  2. set: 用于设置属性
  3. delete: 用于删除

比如,我们有一个类:

1
2
3
4
5
6
class Demo(object):

name = ""

d = Demo()
d.name = 7

如果我们希望name固定为str类型,虽然我们可以通过类似get\set方法的来统一读写name属性,但用户仍然可以通过obj.name等方式来设置。我们可以通过添加一个set方法来控制该属性的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class BeString(object):

def __init__(self,name):
self.name = name

def __set__(self,instance,value):
if isinstance(value,str):
instance.__dict__[self.name] = value
else:
raise Exception("Must be str.")

class Demo(object):

name = BeString("name")

d = Demo()
d.name = 7

----------
Traceback (most recent call last):
File "demo_descriptor.py", line 25, in <module>
d.name = 7
File "demo_descriptor.py", line 18, in __set__
raise Exception("Must be str.")
Exception: Must be str.

覆盖型描述符合非覆盖型描述符

如上述示例,包含set特殊方法的类称为覆盖型描述符,只包含get特殊方法的类称为非覆盖型描述符

使用覆盖型描述符会覆盖实例属性的赋值操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class BeString(object):

def __init__(self,name):
self.attr_name = name

def __set__(self,instance,value):
if isinstance(value,str):
instance.__dict__[self.attr_name] = value
else:
raise Exception("Must be str.")

class Demo(object):

name = BeString("xname")

d = Demo()
d.name = "张三"

print(vars(d))
------------
{'xname': '张三'}

为了区别显示,我这里把描述符的名称设置为了xname,从d的dict来看,set方法覆盖了实例属性的赋值操作d.name

恢复正常的显示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class BeString(object):

def __init__(self,name):
self.attr_name = name

def __set__(self,instance,value):
if isinstance(value,str):
instance.__dict__[self.attr_name] = value
else:
raise Exception("Must be str.")

def __get__(self,instance,type):
return instance.__dict__[self.attr_name]

class Demo(object):

name = BeString("xname")

d = Demo()
d.name = "张三"

print(vars(d))
print(d.name)
---------
{'xname': '张三'}
张三

如果删掉set方法就成了非覆盖型描述符,如下例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class BeString(object):

def __init__(self,name):
self.attr_name = name

def __get__(self,instance,type):
return "随便起个名"

class Demo(object):

name = BeString("xname")
family_name = BeString("fname")

d = Demo()
d.name = "张三"

print(vars(d))
print(d.name)
print(d.family_name)
---------
{'name': '张三'}
张三
随便起个名

因为实例属性存在,__get__方法并没有被调用。而family_name不存在示例属性,则调用了描述符的__get__方法显示了”随便起个名”的文字。

在类中覆盖描述符

在类中给类属性赋值都会覆盖掉描述符,这是一种猴子补丁技术,不管是不是覆盖型描述符。
如下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class BeString(object):

def __init__(self,name):
self.attr_name = name

def __set__(self,instance,value):
if isinstance(value,str):
instance.__dict__[self.attr_name] = value
else:
raise Exception("Must be str.")

def __get__(self,instance,type):
return "随便起个名"

class Demo(object):

name = BeString("xname")
family_name = BeString("fname")

d = Demo()
Demo.name = 7
Demo.family_name = "TEST"

print(vars(d))
print(d.name)
print(d.family_name)
--------
{}
7
TEST

可以看到,不论是__set__还是__get__都没有生效,哪怕在后续的操作中继续使用实例方法也不会有效。

1
2
3
4
5
6
7
8
9
10
d = Demo()
Demo.name = 7
Demo.family_name = "TEST"
d.name = 9
d.family_name = 11

------
{'name': 9, 'family_name': 11}
9
11

另外,

1
2
3
4
5
6
7
8
9
10
d = Demo()
Demo.name = 7

print(vars(d))
print(d.name)
print(Demo.family_name)
-----
{}
7
随便起个名

从这里能看出来,在读取类的属性上,是可以调用类上面的描述符处理,而写却不行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class BeString(object):

def __init__(self,name):
self.attr_name = name

def __set__(self,instance,value):
if isinstance(value,str):
instance.__dict__[self.attr_name] = value
else:
raise Exception("Must be str.")

def __get__(self,instance,type):
return "随便起个名"

class Demo(object):

name = BeString("name")

d = Demo()
d.name = "小王"

print(vars(d))
print(d.name)

-----
{'name': '小王'}
随便起个名

属性查找的顺序

当使用实例对象访问属性的时候,查找会以下面的顺序进行:

类属性——>覆盖型描述符——>实例属性——>非覆盖型描述符——> __getattr__

__getattr__和__getattribute__

getattr会在getattribute找不到属性的时候被调用,getattribute是属性查找的入口,其内部实现了我们上述的查找顺序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Demo(object):

def __getattribute__(self,item):
print("开始查找")
# return super(Demo,self).__getattribute__(item)
raise AttributeError("")

def __getattr__(self,item):
print("没有找到,返回默认值")
return "000"

name = BeString("name")

d = Demo()

d.mono

如果我们自己实现getattribute,则需要手动抛出AttributeError才能调用到getattr.

描述符在很多著名的框架中都有引用,像Django中的ORM,SQLalchemy等,都是描述符应用的范例。