descriptors(描述符)是python语言中一个深奥但很重要的一个黑魔法,它被广泛应用于python语言的内核,熟练掌握描述符将会为python程序员的工具箱添加一个额外的技巧。本文我将讲述描述符的定义以及一些常见的场景,并且在文末会补充一下<code>__getattr</code>,<code>__getattribute__</code>, <code>__getitem__</code>这三个同样涉及到属性访问的魔术方法。
只要一个<code>object attribute</code>(对象属性)定义了上面三个方法中的任意一个,那么这个类就可以被称为描述符类。
下面这个例子中我们创建了一个<code>revealacess</code>类,并且实现了<code>__get__</code>方法,现在这个类可以被称为一个描述符类。
ex1实例属性
接下来我们来看一下<code>__get__</code>方法的各个参数的含义,在下面这个例子中,<code>self</code>即revealaccess类的实例x,<code>obj</code>即myclass类的实例m,<code>objtype</code>顾名思义就是myclass类自身。从输出语句可以看出,<code>m.x</code>访问描述符<code>x</code>会调用<code>__get__</code>方法。
ex2类属性
如果通过类直接访问属性<code>x</code>,那么<code>obj</code>接直接为none,这还是比较好理解,因为不存在myclass的实例。
上面这个例子中,我们分别从实例属性和类属性的角度列举了描述符的用法,下面我们来仔细分析一下内部的原理:
如果是对<code>实例属性</code>进行访问,实际上调用了基类object的__getattribute__方法,在这个方法中将obj.d转译成了<code>type(obj).__dict__['d'].__get__(obj, type(obj))</code>。
如果是对<code>类属性</code>进行访问,相当于调用了元类type的__getattribute__方法,它将cls.d转译成<code>cls.__dict__['d'].__get__(none, cls)</code>,这里__get__()的obj为的none,因为不存在实例。
简单讲一下<code>__getattribute__</code>魔术方法,这个方法在我们访问一个对象的属性的时候会被无条件调用,详细的细节比如和<code>__getattr</code>, <code>__getitem__</code>的区别我会在文章的末尾做一个额外的补充,我们暂时并不深究。
首先,描述符分为两种:
如果一个对象同时定义了__get__()和__set__()方法,则这个描述符被称为<code>data descriptor</code>。
如果一个对象只定义了__get__()方法,则这个描述符被称为<code>non-data descriptor</code>。
我们对属性进行访问的时候存在下面四种情况:
data descriptor
instance dict
non-data descriptor
__getattr__()
它们的优先级大小是:
这是什么意思呢?就是说如果实例对象obj中出现了同名的<code>data descriptor->d</code> 和 <code>instance attribute->d</code>,<code>obj.d</code>对属性<code>d</code>进行访问的时候,由于data descriptor具有更高的优先级,python便会调用<code>type(obj).__dict__['d'].__get__(obj, type(obj))</code>而不是调用obj.__dict__[‘d’]。但是如果描述符是个non-data descriptor,python则会调用<code>obj.__dict__['d']</code>。
每次使用描述符的时候都定义一个描述符类,这样看起来非常繁琐。python提供了一种简洁的方式用来向属性添加数据描述符。
fget、fset和fdel分别是类的getter、setter和deleter方法。我们通过下面的一个示例来说明如何使用property:
如果acct是account的一个实例,acct.acct_num将会调用getter,acct.acct_num = value将调用setter,del acct_num.acct_num将调用deleter。
python也提供了<code>@property</code>装饰器,对于简单的应用场景可以使用它来创建属性。一个属性对象拥有getter,setter和deleter装饰器方法,可以使用它们通过对应的被装饰函数的accessor函数创建属性的拷贝。
如果想让属性只读,只需要去掉setter方法。
我们可以在运行时添加property属性:
我们可以使用描述符来模拟python中的<code>@staticmethod</code>和<code>@classmethod</code>的实现。我们首先来浏览一下下面这张表:
transformation
called from an object
called from a class
function
f(obj, *args)
f(*args)
staticmethod
classmethod
f(type(obj), *args)
f(klass, *args)
对于静态方法<code>f</code>。<code>c.f</code>和<code>c.f</code>是等价的,都是直接查询<code>object.__getattribute__(c, ‘f’)</code>或者<code>object.__getattribute__(c, ’f‘)</code>。静态方法一个明显的特征就是没有<code>self</code>变量。
静态方法有什么用呢?假设有一个处理专门数据的容器类,它提供了一些方法来求平均数,中位数等统计数据方式,这些方法都是要依赖于相应的数据的。但是类中可能还有一些方法,并不依赖这些数据,这个时候我们可以将这些方法声明为静态方法,同时这也可以提高代码的可读性。
使用非数据描述符来模拟一下静态方法的实现:
我们来应用一下:
python的<code>@classmethod</code>和<code>@staticmethod</code>的用法有些类似,但是还是有些不同,当某些方法只需要得到<code>类的引用</code>而不关心类中的相应的数据的时候就需要使用classmethod了。
使用非数据描述符来模拟一下类方法的实现:
首次接触python魔术方法的时候,我也被<code>__get__</code>, <code>__getattribute__</code>, <code>__getattr__</code>, <code>__getitem__</code>之间的区别困扰到了,它们都是和属性访问相关的魔术方法,其中重写<code>__getattr__</code>,<code>__getitem__</code>来构造一个自己的集合类非常的常用,下面我们就通过一些例子来看一下它们的应用。
python默认访问类/实例的某个属性都是通过<code>__getattribute__</code>来调用的,<code>__getattribute__</code>会被无条件调用,没有找到的话就会调用<code>__getattr__</code>。如果我们要定制某个类,通常情况下我们不应该重写<code>__getattribute__</code>,而是应该重写<code>__getattr__</code>,很少看见重写<code>__getattribute__</code>的情况。
从下面的输出可以看出,当一个属性通过<code>__getattribute__</code>无法找到的时候会调用<code>__getattr__</code>。
对于默认的字典,python只支持以<code>obj['foo']</code>形式来访问,不支持<code>obj.foo</code>的形式,我们可以通过重写<code>__getattr__</code>让字典也支持<code>obj['foo']</code>的访问形式,这是一个非常经典常用的用法:
我们来使用一下我们自定义的加强版字典:
getitem用于通过下标<code>[]</code>的形式来获取对象中的元素,下面我们通过重写<code>__getitem__</code>来实现一个自己的list。
这个实现非常的简陋,不支持slice和step等功能,请读者自行改进,这里我就不重复了。
程序有些复杂,我稍微解释一下:由于这里比较简单,没有使用描述符的需求,所以使用了<code>@property</code>装饰器来代替,<code>lower_keys</code>的功能是将<code>实例字典</code>中的键全部转换成小写并且存储在字典<code>self._lower_keys</code>中。重写了<code>__getitem__</code>方法,以后我们访问某个属性首先会将键转换为小写的方式,然后并不会直接访问实例字典,而是会访问字典<code>self._lower_keys</code>去查找。赋值/删除操作的时候由于实例字典会进行变更,为了保持<code>self._lower_keys</code>和实例字典同步,首先清除<code>self._lower_keys</code>的内容,以后我们重新查找键的时候再调用<code>__getitem__</code>的时候会重新新建一个<code>self._lower_keys</code>。
我们来调用一下这个类:
作者:ziwenxie
来源:51cto