Python描述器(descriptor)

应该承认,Python的OOP不是想象中的那么简单,其中的诸多概念也许很多都知道,像MRO、super、property等等。很多情况问什么是property,都知道怎么用,但是问property是什么、怎么实现的很多情况就抓瞎了。property是通过descriptor实现的。

关于descriptor(描述器),在日常代码中也不经常见到,但是了解descriptor有利于理解Python是怎么工作的,也有利用写出更加优雅的程序。网上的的资料很多都零散,我就写写阅读文档和他人的博客之后的一篇笔记。

descriptor example

descriptor是property、staticmethod和classmethod等内置方法的实现原理。简单说,descriptor就是可以重用的property。

property可以将一个类的方法当成属性来访问,通常通过property可以来做一些数据校验之类的的东西。例如:

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

def __init__(self, name, rating, budget):
self._budget = None
self.name = name
self.rating = rating
self.budget = budget

@property
def budget(self):
return self._budget

@budget.setter
def budget(self, value):
if value < 0:
raise ValueError("Negative value not allowed: %s" % value)
self._budget = value

m = Movie("Logan", 8.7, 30)
print(m.budget)
try:
m.budget = -100
except ValueError as err:
print(err)

好,现在我们需要对rating属性也做一个非负数校验,现在也需要将property封装函数再写一遍么?或者干脆直接将这些参数加双下划线在加getter/setter函数?这时候就该descriptor上场了:

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
31
32
33
34
35
36
37
38
39
40
41
class NonNegative(object):

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

def __get__(self, instance, klass):
return self.value

def __set__(self, instance, value):
if value < 0:
raise ValueError("Negative value not allowed: %s" % value)
self.value = value


class Movie(object):

budget = NonNegative(0)
rating = NonNegative(0)

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


logan = Movie("Logan", 87, 30)
lalaland = Movie("La La Land", 85, 40)

print(logan.budget, logan.rating)
# 87 30
print(lalaland.budget, lalaland.rating)
# 85 40
try:
logan.budget = -100
logan.rating = -98
except ValueError as err:
print(err)

logan.budget = 38
print(logan.budget, lalaland.budget)
# 38 38 // error occurs !!!

一切看起来都可以,除了最后一个,我们修改了logan的budget,但是我们没有修改lalaland的!而descriptor要正确工作只能在类级别,在对象级别是无法正常工作的。现在我们需要的是对于每一个对象都应该有自己的数据拷贝,这一点很多讲descriptor都给忽略了,NonNegative的正确实现是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from weakref import WeakKeyDictionary


class NonNegative(object):

def __init__(self, value):
self.default = value
self.data = WeakKeyDictionary()

def __get__(self, instance, klass):
return self.data.get(instance, self.default)

def __set__(self, instance, value):
if value < 0:
raise ValueError("Negative value not allowed: %s" % value)
self.data[instance] = value

使用WeakKeyDictionary确保了对象被垃圾回收相应的数据会被删除。

除此之外,descriptor还可以实现类的只读属性等等。

what is descriptor

descriptor就是一个实现了__get__ (), __set()__ 和__delete__()其中一个函数的对象。关于Python对象属性获取,Python用dict来存储关于类和对象的一些信息,如:

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

def __init__(self, value):
self.x = value

def func(self):
print("this is a func")


demo = Demo(12)
demo.y = 14
print(demo.__dict__)
"""
{'x': 12, 'y': 14}
"""
print(Demo.__dict__)
"""
{'__dict__': <attribute '__dict__' of 'Demo' objects>, '__init__': <function Demo.__init__ at 0x7fe732d1fa60>, '__weakref__': <attribute '__weakref__' of 'Demo' objects>, '__module__': '__main__', 'func': <function Demo.func at 0x7fe732d1fbf8>, '__doc__': None, 'const': 14}
"""

可以发现关于类的属性包括运行时定义的属性,存储在的是对象的__dict__ 里,而函数及类变量作为类的属性则存储在类的__dict__里,被全部变量共享,这也合理。

在一般情况下,object.attribute访问一个对象属性时,搜索顺序如下:

  1. 对象本身, object.__dict__
  2. 对象类型, type(object).__dict__
  3. 对象所有基类的__dict__
  4. 假如这些都获取不到,raise AttributeError

要定义一个描述器很简单,一个类里只要实现以下其中一个函数既可以:

1
2
3
4
5
6
7
8
def __get__(self, _object, _type=None) -> None:
pass

def __set__(self, _object, value) -> None:
pass

def __delete__(self, _object) -> None:
pass

其中实现了__get__()和__set__()的成为data descriptor(数据描述器),而只实现了__get__()的为non-data descriptor。二者区别是,当一个对象里有重名的数据描述器,属性和非数据描述器时,访问优先级为:

数据描述器 > 普通属性 > 非数据描述器。需要注意的是,当一个属性是descriptor时,属性的值不会在对象的字典出现。

参考资料

posted @ 2017-07-31 13:17  天涯海角路  阅读(427)  评论(0编辑  收藏  举报