结合源码分析Python的 Descriptors的原理和使用

一 什么是Descriptors

描述器,是指一个包含 绑定行为的 对象,对其属性的访问被描述器协议中所定义的方法覆盖。所定义的方法有__get()____set()____del()__,如果某个类实现了这三个方法中的一个,那么该实例就被称作描述器。

定义形式如下:

1
2
3
4
5
descr.__get__(self, obj, type=None) -> value

descr.__set__(self, obj, value) -> None

descr.__delete__(self, obj) -> None

说明:

如果某一个对象定义了__set__()__delete__(),则会称为数据描述器;如果仅定义了__get__()方法,则被称为非数据描述器。


二 获取实例属性的方法

获取实例属性一般与__getattribute__()__getattr__()__get__()方法有关,接下来依次学习。

1.__getattribute__()内置方法

1
2
3
4
# object类中的__getattribute__方法
def __getattribute__(self, *args, **kwargs):
""" Return getattr(self, name). """
pass

说明:
通过注释,我们可以了解到__getattribute__()方法其实等价与getattr()基于字符串的反射机制的函数。

2.举个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Test(object):
sss = 's'
def __getattribute__(self, item):
raise AttributeError('syz') # 主动抛出AttributeError,如果定义了__getattr__,则会被__getattr__捕捉
# return 'syz'
def __getattr__(self, item):
return item
def __get__(self, instance, owner):
return 'love'

class Operation(object):

t = Test()

m = Operation()
print(m.t,'|',type(m.t)) # 结果:love | <class 'str'>

test = Test()
print(test.sss) # 结果:syz
print(getattr(test,'sss') == test.sss) # 结果:True

分析:

1.调用m.t获取属性<==>m.__getattribute__(t)方法<=>m.__dict__['t'].__get__(m,type(m))

1
2
3
4
5
6
7
8

# 官网中是例子,这段代码我找到出现在哪里...
def __getattribute__(self, key):
"Emulate type_getattro() in Objects/typeobjectEmulate type_getattro() in Objects/typeobject.c"
v = object.__getattribute__(self, key)
if hasattr(v, '__get__'):
return v.__get__(None, self)
return v

2.如果定义了__getattribute__()方法,却在内部主动抛出了AttributeError,如果同时定义了__getattr__()方法,则会被其捕获调用。

3.无论访问的是该对象存在还是不存在的属性或方法,首先都会去寻找是否定义了__getattribute__方法,如果定义,则尽管属性存在,但还是返回__getattribute__()返回的值。

4.__getattribute__()方法<==>getattr()方法

5.描述器:当一个类(Test)的实例作为另一个类(Operation)的属性,调用类(Operation)的属性(t),其实调用类(Test)实例的__get__()方法,可能说的有点绕,结合代码看清晰点。

6.描述器会由__getattribute__()调用,调用了__getattribute__(),实际底层调用__dict__['t'].__get(m,type(m))__。实例调用和类调用是有区别的。

7.描述器必须通过类的属性赋值,而不能通过__init__()实例化产生。

8.总结调用时机和次序:

(1)如果访问一个属性,该属性是描述器,则调用描述器的__get__()方法

(2)如果访问非描述器的属性,首先会调用__getattribute__()方法,无论属性是否存在,只要定义了__getattribute__(),则会返回__getattribute__()的值。

(3)如果调用没有定义__getattribute__()或者在__getattribute__()中抛出了AttributeError异常,则会调用__getattr__()方法


三 Property—描述器的应用之一

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
25
26
27
28
29
30
31
32
33
34
35
36
37
#Python的一个等价实现
class property(object):
"Emulate PyProperty_Type() in Objects/descrobject.c"

def __init__(self, fget=None, fset=None, fdel=None, doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
if doc is None and fget is not None:
doc = fget.__doc__
self.__doc__ = doc

def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.fget is None:
raise AttributeError("unreadable attribute")
return self.fget(obj)

def __set__(self, obj, value):
if self.fset is None:
raise AttributeError("can't set attribute")
self.fset(obj, value)

def __delete__(self, obj):
if self.fdel is None:
raise AttributeError("can't delete attribute")
self.fdel(obj)

def getter(self, fget):
return type(self)(fget, self.fset, self.fdel, self.__doc__)

def setter(self, fset):
return type(self)(self.fget, fset, self.fdel, self.__doc__)

def deleter(self, fdel):
return type(self)(self.fget, self.fset, fdel, self.__doc__)

例子

利用上面Python实现的等价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
25
26
class Emm:
def __init__(self):
self._name = 'syz'
# Property 装饰器,用于实例化,name函数作为__init__的第一个参数fget,返回Property描述器对象,具备fget属性
@Property
def name(self):
return self._name

# name.setter也是个装饰器,调用的是描述器对象的setter方法, name函数作为参数传给setter方法,用于返回新的Property描述器对象,具备fset属性
@name.setter
def name(self, value):
self._name = value

# 同理setter,返回新的Property描述器对象,具备fdel属性
@name.deleter
def name(self):
del self._name


emm = Emm()
print(emm.__dict__) # 结果: {'_name': 'syz'}
print(emm.name) # 结果: syz
emm.name = 'zjw'
print(emm.name) # 结果: zjw
del emm.name
print(emm.__dict__) # 结果: {}

注:

上述Emm类中,实际上创建了三个Property描述器,分别添加fget属性,fset属性,fdel属性,这三个属性分别对应三个同名的方法,在对某个属性操作的时候,三个描述器分别会调用__get()____set()____del()__

总结:

描述器在property中是对同一个属性进行不同行为绑定的一种形式,如上个例子中,对_name属性绑定get,set,del三种行为。


四 Django中FileField —-描述器的应用之一

很多框架的底层肯定也会涉及到众多的内置方法,正巧打算用FastDFS重写Django默认的文件存储系统,在重写过程中需要知道默认的存储系统是如何调用的,以及如何和Field字段产生关系。现在就来学习以下底层的源码。

这里我不打算贴出重写的Storage,一方面还没有完善好,我准备单独写一篇笔记来记录FastDfs重写的代码。

主要分析下FileField中的描述器

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

# The class to wrap instance attributes in. Accessing the file object off
# the instance will always return an instance of attr_class.
attr_class = FieldFile # 属性类,FileField实例最终会返回attr_class的实例,包含了文件操作的属性

# The descriptor to use for accessing the attribute off of the class.
descriptor_class = FileDescriptor # 用于获取该类属性的描述器

def pre_save(self, model_instance, add):
#在保存模型的前,先写入文件到指定的位置
file = super().pre_save(model_instance, add)
if file and not file._committed:
# Commit the file to storage prior to saving the model
file.save(file.name, file.file, save=False)
return file

def contribute_to_class(self, cls, name, **kwargs):
super().contribute_to_class(cls, name, **kwargs)
# self.name为attr_class实例,即将__dict__中的该字段对象属性与attr_class实例绑定,通过描述器来绑定对象的一种行为
setattr(cls, self.name, self.descriptor_class(self)) # 将 描述器对象赋值给self.name,调用self.name,其实调用的是self.__dict__[self.field.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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class FileDescriptor:
"""
The descriptor for the file attribute on the model instance. Return a
FieldFile when accessed so you can write code like::
"""
def __init__(self, field):
# field参数其实是FileField字段对象
self.field = field

def __get__(self, instance, cls=None): # instance 为 字段实例,cls为字段类
if instance is None:
return self
if self.field.name in instance.__dict__:
# self.field.name 为 字段名
file = instance.__dict__[self.field.name] # 获取实例__dict__中的字段对象
else:
instance.refresh_from_db(fields=[self.field.name])
file = getattr(instance, self.field.name)

if isinstance(file, str) or file is None:
attr = self.field.attr_class(instance, self.field, file)
instance.__dict__[self.field.name] = attr

elif isinstance(file, File) and not isinstance(file, FieldFile):
file_copy = self.field.attr_class(instance, self.field, file.name) # 返回FieldFile实例,包含一些文件的操作
file_copy.file = file
file_copy._committed = False
instance.__dict__[self.field.name] = file_copy # 替换了原来__dict__中的FileFiled对象为FieldFile对象

elif isinstance(file, FieldFile) and not hasattr(file, 'field'):
file.instance = instance
file.field = self.field
file.storage = self.field.storage

# Make sure that the instance is correct.
elif isinstance(file, FieldFile) and instance is not file.instance:
file.instance = instance

return instance.__dict__[self.field.name] # 返回FieldFile对象,用于直接操作文件对象

def __set__(self, instance, value):
instance.__dict__[self.field.name] = value # set描述器

说明:

1
2
3
# 使用shell测试的时候获取head_image字段时,返回的实际是ImageFieldFile文件对象,也就是attr_class实例
In [44]: queryset[0].head_image
Out[44]: <ImageFieldFile: group1/M00/00/00/wKgAaV9KO-mAHdRbAAAzLoMjwYM8236232>

顺便提一下,访问某个模型的字段,实际上访问的是field.name,因为model源码中_setattr(self, field.name, rel_obj),将真实的obj和field.name绑定起来。这一切其实在实例化Model的时候就做好了,绑定对应的字段名到真实的对象上。


总结

在Django框架中ImageFiled/FileField中使用到描述器,作用是替换原来的FieldFile实例为FileField实例,通过setattr()和描述器的__get()__内置方法,修改instance.__dict__[self.field.name] = attr为attr_class实例,即FieldFile文件对象。做了一个偷换对象的事情,正如注释中所说That was fun, wasn’t it?

总的来说,描述器的主要作用还是绑定某个属性的行为,利用协议的方法来覆盖掉对原始的属性的访问!