0、引言
Descriptors(描述器)是Python语言中一个深奥但很重要的一个黑魔法,它被广泛应用于Python语言的内核,熟练掌握描述器将会为Python程序员的工具箱添加一个额外的技巧。本文我将讲述描述符的定义以及一些常见的场景,并且在文末会补充一下__getattr__,
__getattribute__
,__getitem__
这三个同样涉及到属性访问的魔术方法。
一、描述器类的定义
只要一个对象属性定义了下面三个方法中的任意一个,那么这个类就可以被称为描述器类。
官网中标明了这三个方法需要传入哪些参数,还有这些方法的返回结果是什么,如下所示:
descr.__get__(self, instance, owner=None) --> value
descr.__set__(self, instance, value) --> None
descr.__delete__(self, instance) --> None
__get__():调用一个属性时,触发
__set__():为一个属性赋值时,触发
__delete__():采用del删除属性时,触发
参数解释:
self: 描述器实例
instance: 调用描述器的类的对象实例 (有时候用obj来表示instance)
owner:调用描述器的类自身 (有时候用cls来表示owner)
为了通俗理解上述参数含义,这里新建一个描述器类lazyproperty来对一个Circle类的面积计算函数area进行描述,代码如下:
#描述器类为lazyproperty,用来描述Circle类的area
class lazyproperty(object):
def __init__(self,func):
self.func=func
def __get__(self,instance,owner):
print('self in lazyproperty:{}'.format(self))
print('self:{}\ninstance:{}\nowner:{}'.format(self,instance,owner))
value=self.func(instance)
return value
class Circle(object):
def __init__(self,r):
self.r=r
@lazyproperty #等效于 area=lazyproperty(area)
def area(self):
print('computing area:')
return 3.14*self.r*2
cir=Circle(2.0)
cir.area
输出:
self in lazyproperty:<__main__.lazyproperty object at 0x0000000003D95BE0>
self:<__main__.lazyproperty object at 0x0000000003D95BE0>
instance:<__main__.Circle object at 0x0000000003FB5390>
owner:<class '__main__.Circle'>
computing area:
12.56
这里说明:self 就是描述器类lazyproperty的对象实例,instance是调用描述器的类Circle的对象实例,而owner就是Circled类本身(未实例化)。
二、描述符基础
下面这个例子中我们创建了一个RevealAcess
类,并且实现了__get__
方法,现在这个类可以被称为一个描述符类。
class RevealAccess(object):
def __get__(self, obj, objtype):
print('self in RevealAccess: {}'.format(self))
print('self: {}\nobj: {}\nobjtype: {}'.format(self, obj, objtype))
class MyClass(object):
x = RevealAccess()
def test(self):
print('self in MyClass: {}'.format(self))
(1)EX1实例属性
接下来我们来看一下__get__
方法的各个参数的含义,在下面这个例子中,先实例化Myclass,再访问其属性x。self
即RevealAccess类的实例x,obj
即MyClass类的实例m,objtype
顾名思义就是MyClass类自身。从输出语句可以看出,m.x
访问描述符x
会调用__get__
方法。
>>> m = MyClass()
>>> m.test()
self in MyClass: <__main__.MyClass object at 0x7f19d4e42160>
>>> m.x
self in RevealAccess: <__main__.RevealAccess object at 0x7f19d4e420f0>
self: <__main__.RevealAccess object at 0x7f19d4e420f0>
obj: <__main__.MyClass object at 0x7f19d4e42160>
objtype: <class '__main__.MyClass'>
(2)EX2类属性
如果通过类直接访问属性x
,那么obj
直接为None,这还是比较好理解,因为不存在MyClass的实例。
>>> MyClass.x
self in RevealAccess: <__main__.RevealAccess object at 0x7f53651070f0>
self: <__main__.RevealAccess object at 0x7f53651070f0>
obj: None
objtype: <class '__main__.MyClass'>
三、描述符的原理
(1)描述符触发
上面这个例子中,我们分别从实例属性和类属性的角度列举了描述符的用法,下面我们来仔细分析一下内部的原理:
- 如果是对
实例属性
进行访问,实际上调用了基类object的__getattribute__方法,在这个方法中将obj.d转译成了type(obj).__dict__['d'].__get__(obj, type(obj))
。 - 如果是对
类属性
进行访问,相当于调用了元类type的__getattribute__方法,它将cls.d转译成cls.__dict__['d'].__get__(None, cls)
,这里__get__()的obj为的None,因为不存在实例。
简单讲一下__getattribute__
魔术方法,这个方法在我们访问一个对象的属性的时候会被无条件调用,详细的细节比如和__getattr
, __getitem__
的区别我会在文章的末尾做一个额外的补充,我们暂时并不深究。
(2)描述符优先级
首先,描述符分为两种:
- 如果一个对象同时定义了__get__()和__set__()方法,则这个描述符被称为数据描述器(
data descriptor
); - 如果一个对象只定义了__get__()方法,则这个描述符被称为非数据描述器(
non-data descriptor
); - 二者的区别是:当属性名和描述器名相同时,在访问这个同名属性时,如果是数据描述器就会先访问描述器,如果是非数据描述器就会先访问属性。
我们知道当访问实例 a
属性 x
的时候,python 会先查看 a.__dict__['x']
,然后会访问 type(a).__dict__['x']
,然后依次访问 type(a)
的基类。当我们调用 obj.x
的时候,如果 x
是描述器,会根据 obj 是对象还是类有不同的调用顺序:
如果是对象,自动访问是在 obj.__getattribute__()
函数中完成的,这个函数会把 a.x
转化成 type(a).__dict__['x']. __get__(a, type(a))
。这个调用的优先级如下:
- 首先调用数据描述器(如果定义了的话)
- 其次调用实例变量
- 然后是非数据描述器(如果定义了的话)
- 最后是
__getattr__
内部函数(当以上调用都没有返回的时候)
举例如下:
# 既有__get__又有__set__,是一个资料描述器
class M(object):
def __init__(self):
self.x = 1
def __get__(self, instance, owner):
print('get m here') # 打印一些信息,看这个方法何时被调用
return self.x
def __set__(self, instance, value):
print('set m here') # 打印一些信息,看这个方法何时被调用
self.x = value + 1 # 这里设置一个+1来更清楚了解调用机制
# 只有__get__是一个非资料描述器
class N(object):
def __init__(self):
self.x = 1
def __get__(self, instance, owner):
print('get n here') # 打印一些信息,看这个方法何时被调用
return self.x
# 调用描述器的类
class AA(object):
m = M() # m就是一个描述器类
n = N() # n是一个非资料描述器类
def __init__(self, m, n):
self.m = m # 属性m和描述器m名字相同,调用时发生一些冲突
self.n = n # 非资料描述器的情况,与m对比
aa = AA(2,5)
print(aa.__dict__) # 只有n没有m, 因为资料描述器同名时,不会访问到属性,会直接访问描述器,所以属性里就查不到m这个属性了
#结果为:{'n': 5}
print(AA.__dict__) # m和n都有
#结果为:{'__module__': '__main__', 'm': <__main__.M object at 0x0000000002E009B0>, 'n': <__main__.N object at 0x0000000002E08CF8>, '__init__': <function AA.__init__ at 0x0000000004CBCBF8>, '__dict__': <attribute '__dict__' of 'AA' objects>, '__weakref__': <attribute '__weakref__' of 'AA' objects>, '__doc__': None}
print(aa.n) # 5, 非资料描述器同名时调用的是属性,为传入的5
print(AA.n) # 1, 如果是类来访问,就调用的是描述器,返回self.x的值
print(aa.m) # 3, 其实在aa=AA(2,5)创建实例时,进行了属性赋值,其中相当于进行了aa.m=2
# 但是aa调用m时却不是常规地调用属性m,而是资料描述器m
# 所以定义实例aa时,其实触发了m的__set__方法,将2传给value,self.x变成3
# aa.m调用时也访问的是描述器,返回self.x即3的结果
# 其实看打印信息也能看出什么时候调用了__get__和__set__
aa.m = 6 # 另外对属性赋值也是调用了m的__set__方法
print(aa.m) # 7,调用__get__方法
print('-'*20)
# 在代码中显式调用__get__方法
print(AA.__dict__['n'].__get__(None, AA)) # 1
print(AA.__dict__['n'].__get__(aa, AA)) # 1
注:要想制作一个只读的资料描述器,需要同时定义 __set__
和 __get__
,并在 __set__
中引发一个 AttributeError 异常。定义一个引发异常的 __set__
方法就足够让一个描述器成为资料描述器。
四、Property(最著名的描述器之一)
每次使用描述符的时候都定义一个描述符类,这样看起来非常繁琐。Python提供了一种简洁的方式用来向属性添加数据描述符。首先要明确,property有两种调用形式,一种是用装饰器,一种是用类似函数的形式,下面会用引言中的例子分别说明两种形式的调用机制。
下面贴出property的等价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__)
从上面的定义中我们可以看出,定义时分为两个部分,一个是__get__
等方法的定义,另一部分是getter
等方法的定义,同时注意到这个类要传入fget
等三个函数作为属性。getter
等方法的定义是为了让它可以完美地使用装饰器形式,
(1)调用形式1:类似函数形式
property(fget=None, fset=None, fdel=None, doc=None) -> property attribute
fget、fset和fdel分别是类的getter、setter和deleter方法。先不使用装饰器的形式的调用机制:
# 类似函数的形式
class A:
def __init__(self, name, score):
self.name = name # 普通属性
self.score = score
def getscore(self):
return self._score
def setscore(self, value):
print('setting score here')
if isinstance(value, int):
self._score = value
else:
print('please input an int')
score = property(getscore, setscore)
a = A('Bob',90)
a.name # 'Bob'
a.score # 90
a.score = 'bob' # please input an int
分析上述调用score的过程:
- 初始化时即开始访问score,发现有两个选项,一个是属性,另一个是
property(getscore, setscore)
对象,因为后者中定义了__get__
与__set__
方法,因此是一个资料描述器,具有比属性更高的优先级,所以这里就访问了描述器 - 因为初始化时是对属性进行设置,所以自动调用了描述器的
__set__
方法 -
__set__
中对fset
属性进行检查,这里即传入的setscore
,不是None
,所以调用了fset
即setscore
方法,这就实现了设置属性时使用自定义函数进行检查的目的 -
__get__
也是一样,查询score时,调用__get__
方法,触发了getscore
方法
(2)调用形式2:装饰器形式
Python也提供了@property
装饰器,对于简单的应用场景可以使用它来创建属性。一个属性对象拥有getter,setter和deleter装饰器方法,可以使用它们通过对应的被装饰函数的accessor函数创建属性的拷贝。
# 装饰器形式,即引言中的形式
class A:
def __init__(self, name, score):
self.name = name # 普通属性
self.score = score
@property
def score(self):
print('getting score here')
return self._score
@score.setter
def score(self, value):
print('setting score here')
if isinstance(value, int):
self._score = value
else:
print('please input an int')
a = A('Bob',90)
# a.name # 'Bob'
# a.score # 90
# a.score = 'bob' # please input an int
如果想让属性只读,只需要去掉setter方法。
下面进行分析:
- 在第一种使用方法中,是将函数作为传入property中,所以可以想到是否可以用装饰器来封装
- get部分很简单,访问
score
时,加上装饰器变成访问property(score)
这个描述器,这个score
也作为fget
参数传入__get__
中指定调用时的操作 - 而set部分就不行了,于是有了
setter
等方法的定义 - 使用了
property
和setter
装饰器的两个方法的命名都还是score,一般同名的方法后面的会覆盖前面的,所以调用时调用的是后面的setter
装饰器处理过的score
,是以如果两个装饰器定义的位置调换,将无法进行属性赋值操作。 - 而调用
setter
装饰器的score
时,面临一个问题,装饰器score.setter
是什么呢?是score
的setter
方法,而score
是什么呢,不是下面定义的这个score
,因为那个score
只相当于参数传入。自动向其他位置寻找有没有现成的score
,发现了一个,是property
修饰过的score
,这是个描述器,根据property
的定义,里面确实有一个setter
方法,返回的是property
类传入fset
后的结果,还是一个描述器,这个描述器传入了fget
和fset
,这就是最新的score
了,以后实例只要调用或修改score
,使用的都是这个描述器 - 如果还有
del
则装饰器中的score
找到的是setter
处理过的score
,最新的score
就会是三个函数都传入的score
- 对最新的
score
的调用及赋值删除都跟前面一样了
property
的原理就讲到这里,从它的定义我们可以知道它其实就是将我们设置的检查等函数传入get set
等方法中,让我们可以自由对属性进行操作。它是一个框架,让我们可以方便传入其他操作,当很多对象都要进行相同操作的话,重复就是难免的。如果想要避免重复,只有自己写一个类似property
的框架,这个框架不是传入我们希望的操作了,而是就把这些操作放在框架里面,这个框架因为只能实现一种操作而不具有普适性,但是却能大大减少当前问题代码重复问题。
下面使用描述器定义了Checkint类之后,会发现A类简洁了非常多
class Checkint:
def __init__(self, name):
self.name = name
def __get__(self, instance, owner):
if instance is None:
return self
else:
return instance.__dict__[self.name]
def __set__(self, instance, value):
if isinstance(value, int):
instance.__dict__[self.name] = value
else:
print('please input an integer')
# 类似函数的形式
class A:
score = Checkint('score')
age = Checkint('age')
def __init__(self, name, score, age):
self.name = name # 普通属性
self.score = score
self.age = age
a = A('Bob', 90, 30)
a.name # 'Bob'
a.score # 90
# a.score = 'bob' # please input an int
# a.age='a' # please input an integer
五、在运行时创建描述符
我们可以在运行时添加property属性:
class Person(object):
def addProperty(self, attribute):
# create local setter and getter with a particular attribute name
getter = lambda self: self._getProperty(attribute)
setter = lambda self, value: self._setProperty(attribute, value)
# construct property attribute and add it to the class
setattr(self.__class__, attribute, property(fget=getter, \
fset=setter, \
doc="Auto-generated method"))
def _setProperty(self, attribute, value):
print("Setting: {} = {}".format(attribute, value))
setattr(self, '_' + attribute, value.title())
def _getProperty(self, attribute):
print("Getting: {}".format(attribute))
return getattr(self, '_' + attribute)
>>> user = Person()
>>> user.addProperty('name')
>>> user.addProperty('phone')
>>> user.name = 'john smith'
Setting: name = john smith
>>> user.phone = '12345'
Setting: phone = 12345
>>> user.name
Getting: name
'John Smith'
>>> user.__dict__
{'_phone': '12345', '_name': 'John Smith'}
六、用描述器类来模拟静态方法@staticmethod
和类方法@classmethod
我们可以使用描述符来模拟Python中的@staticmethod
和@classmethod
的实现。我们首先来浏览一下下面这张表:
Transformation | Called from an Object | Called from a Class |
function | f(obj, *args) | f(*args) |
staticmethod | f(*args) | f(*args) |
classmethod | f(type(obj), *args) | f(klass, *args) |
(1)静态方法@staticmethod
对于静态方法f
。c.f
和C.f
是等价的,都是直接查询object.__getattribute__(c, ‘f’)
或者object.__getattribute__(C, 'f’)
。静态方法一个明显的特征就是没有self
变量。
静态方法有什么用呢?假设有一个处理专门数据的容器类,它提供了一些方法来求平均数,中位数等统计数据方式,这些方法都是要依赖于相应的数据的。但是类中可能还有一些方法,并不依赖这些数据,这个时候我们可以将这些方法声明为静态方法,同时这也可以提高代码的可读性。
使用非数据描述符来模拟一下静态方法的实现:
class StaticMethod(object):
def __init__(self, f):
self.f = f
def __get__(self, obj, objtype=None):
return self.f
我们来应用一下:
class MyClass(object):
@StaticMethod
def get_x(x):
return x
print(MyClass.get_x(100)) # output: 100
(2)类方法@classmethod
@classmethod的作用见我的博客:《python中的@classmethod的作用》
Python的@classmethod
和@staticmethod
的用法有些类似,但是还是有些不同,当某些方法只需要得到类的引用
而不关心类中的相应的数据的时候就需要使用classmethod了。
使用非数据描述符来模拟一下类方法的实现:
class ClassMethod(object):
def __init__(self, f):
self.f = f
def __get__(self, obj, klass=None):
if klass is None:
klass = type(obj)
def newfunc(*args):
return self.f(klass, *args)
return newfunc
附录:其他的魔术方法
首次接触Python魔术方法的时候,我也被__get__
, __getattribute__
, __getattr__
, __getitem__
之间的区别困扰到了,它们都是和属性访问相关的魔术方法,其中重写__getattr__
,__getitem__
来构造一个自己的集合类非常的常用,下面我们就通过一些例子来看一下它们的应用。
(1)__getattr__
Python默认访问类/实例的某个属性都是通过__getattribute__
来调用的,__getattribute__
会被无条件调用,没有找到的话就会调用__getattr__
。如果我们要定制某个类,通常情况下我们不应该重写__getattribute__
,而是应该重写__getattr__
,很少看见重写__getattribute__
的情况。
从下面的输出可以看出,当一个属性通过__getattribute__
无法找到的时候会调用__getattr__
。
In [1]: class Test(object):
...: def __getattribute__(self, item):
...: print('call __getattribute__')
...: return super(Test, self).__getattribute__(item)
...: def __getattr__(self, item):
...: return 'call __getattr__'
...:
In [2]: Test().a
call __getattribute__
Out[2]: 'call __getattr__'
应用
对于默认的字典,Python只支持以obj['foo']
形式来访问,不支持obj.foo
的形式,我们可以通过重写__getattr__
让字典也支持obj['foo']
的访问形式,这是一个非常经典常用的用法:
class Storage(dict):
"""
A Storage object is like a dictionary except `obj.foo` can be used
in addition to `obj['foo']`.
"""
def __getattr__(self, key):
try:
return self[key]
except KeyError as k:
raise AttributeError(k)
def __setattr__(self, key, value):
self[key] = value
def __delattr__(self, key):
try:
del self[key]
except KeyError as k:
raise AttributeError(k)
def __repr__(self):
return '<Storage ' + dict.__repr__(self) + '>'
我们来使用一下我们自定义的加强版字典:
>>> s = Storage(a=1)
>>> s['a']
1
>>> s.a
1
>>> s.a = 2
>>> s['a']
2
>>> del s.a
>>> s.a
...
AttributeError: 'a'
(2)__getitem__
getitem用于通过下标[]
的形式来获取对象中的元素,下面我们通过重写__getitem__
来实现一个自己的list。
class MyList(object):
def __init__(self, *args):
self.numbers = args
def __getitem__(self, item):
return self.numbers[item]
my_list = MyList(1, 2, 3, 4, 6, 5, 3)
print my_list[2]
这个实现非常的简陋,不支持slice和step等功能,请读者自行改进,这里我就不重复了。
应用
下面是参考requests库中对于__getitem__
的一个使用,我们定制了一个忽略属性大小写的字典类。
程序有些复杂,我稍微解释一下:由于这里比较简单,没有使用描述符的需求,所以使用了@property
装饰器来代替,lower_keys
的功能是将实例字典
中的键全部转换成小写并且存储在字典self._lower_keys
中。重写了__getitem__
方法,以后我们访问某个属性首先会将键转换为小写的方式,然后并不会直接访问实例字典,而是会访问字典self._lower_keys
去查找。赋值/删除操作的时候由于实例字典会进行变更,为了保持self._lower_keys
和实例字典同步,首先清除self._lower_keys
的内容,以后我们重新查找键的时候再调用__getitem__
的时候会重新新建一个self._lower_keys
。
class CaseInsensitiveDict(dict):
@property
def lower_keys(self):
if not hasattr(self, '_lower_keys') or not self._lower_keys:
self._lower_keys = dict((k.lower(), k) for k in self.keys())
return self._lower_keys
def _clear_lower_keys(self):
if hasattr(self, '_lower_keys'):
self._lower_keys.clear()
def __contains__(self, key):
return key.lower() in self.lower_keys
def __getitem__(self, key):
if key in self:
return dict.__getitem__(self, self.lower_keys[key.lower()])
def __setitem__(self, key, value):
dict.__setitem__(self, key, value)
self._clear_lower_keys()
def __delitem__(self, key):
dict.__delitem__(self, key)
self._lower_keys.clear()
def get(self, key, default=None):
if key in self:
return self[key]
else:
return default
我们来调用一下这个类:
>>> d = CaseInsensitiveDict()
>>> d['ziwenxie'] = 'ziwenxie'
>>> d['ZiWenXie'] = 'ZiWenXie'
>>> print(d)
{'ZiWenXie': 'ziwenxie', 'ziwenxie': 'ziwenxie'}
>>> print(d['ziwenxie'])
ziwenxie
# d['ZiWenXie'] => d['ziwenxie']
>>> print(d['ZiWenXie'])
ziwenxie