装饰器(decorator)和描述器(descriptor)是Python中两个重要的概念,理解它们是深入理解Python的关键,因为这是很多特性的基础,包括:函数、方法、属性、类方法、静态方法和父类的引用等等(Python如何实现property、classmethod和staticmethod)。
本文重点在于探讨decorator和descriptor特性的细节。
装饰器(decorator)
装饰器可以认为是面向切面编程模式(AOP aspect-oriented programming)的一种实现,可以对原有功能做扩展或者彻底的改变,而不改变外部的调用方式。
语法分析
下面来看一个例子,fPrint
函数实现了一个打印的功能,现在需要在不改变fPrint
的前提下,为打印内容增加一个前缀,以方便阅读
def header(func):
def wrapper(msg):
print('log: ', end='')
func(msg)
return wrapper
@header
def fPrint(msg):
print(msg)
# 调用打印
fPrint('msg')
# 打印内容
>>>> log: msg
从结果上来看fPrint('msg')
的结果:先调用了header
然后调用了fPrint
。我们注意到,fPrint
的函数定义前面,多了@header
,装饰器语法实际上是将fPrint
的实现做了替换,所以我们调用的不再是原始的那个fPrint
。装饰器语法实际上等价于:
...
def fPrint(msg):
print(msg)
fPrint = header(fPrint)
# 调用打印
fPrint('msg')
# 打印内容
>>>> log: msg
带有自定义入参的装饰器
通常装饰器函数是以被装饰函数作为入参,我们要手动传入参数该怎么办?记住一点装饰器语法实际是函数地址的替换,从最简单的装饰器(fPrint = header(fPrint)
)入手,推测一下自定义入参等价形式:fPrint = header(param)(fPrint)
,根据等价形式实现装饰器:
def header(ipt):
def decorator(func):
def wrapper(msg):
print('{} log: '.format(ipt), end='')
func(msg)
return wrapper
return decorator
@header('header')
def fPrint(msg):
print(msg)
# 调用打印
fPrint('msg')
# 打印内容
>>>> header log: msg
关于语法的一些理解:当@header
后面省略参数时,则以后面的函数定义(fPrint
)为参数来完成函数替换。有参数时,执行后的结果为一个函数,再以后面的函数定义(fPrint
)为参数完成函数替换,即fPrint = header('header')(fPrint)
。
装饰器函数写起来不是很好理解,尤其是对于含有自定义参数的装饰器,这里建议,先写出要实现的装饰器的等价形式,依此来写装饰器函数
装饰器类的定义
装饰器可以是一个函数,也可以是一个class, fPrint
作为__init__
函数的参数传入,fPrint = header(fPrint)
,这时fPrint
已经不再是函数而是header的一个实例,至于有什么用等下篇再谈。
class header(object):
def __init__(self, func):
self.f = func
@header
def fPrint(msg):
print(msg)
描述器(descriptor)
描述器是任何一个实现了描述器协议(__get__()
, __set__()
, 或者 __delete__()
)的对象,当描述器作为class属性时,对这个属性的访问便会触发相应的方法。通常使用a.b
的语法来获取、赋值、删除一个class属性时,会在A.__dict__
中查找属性b
,如果b
是一个描述器,相应的描述器方法就会被调用。
描述器的实现
以下方法仅适用于,一个描述器作为class属性时,这一点需要特别注意,如果作为实例属性,对应方法是不会被触发的。
在以下的例子中,"属性"指的是class属性,即type(a).__dict__
中的某个元素。
object.__get__(self, instance, owner=None)
当通过类或者实例获取属性时__get__
方法时会被调用,可选的owner
参数是持有者类(以a.b
为例,owner
是A
),当通过实例获取属性时,instance
为对应实例(以a.b
为例,instanca
是a
),通过持有者类获取时(A.b
),instance
为None
,这个方法应该返回一个计算好的属性值或者抛出AttributeError
异常
object.__set__(self, instance, value)
通过持有者类的实例对一个属性设置新的值时会调用__set__
方法,注意的是,添加set或者delete方法会使得描述器的类型变为数据型描述器,查看“描述器的调用”获取更多细节。
object.__delete__(self, instance)
通过持有者的实例来del
一个属性时会调用__delete__
方法
object.__set_name__(self, owner, name)
描述器被创建并添加到持有者类时被调用,这个调用不会自动进行,需要显式调用,以告知描述器已被持有者持有
class A:
pass
descr = custom_descriptor()
A.attr = descr
descr.__set_name__(A, 'attr')
描述器方法的调用
首先,关注一下普通属性的调用优先级:
一个属性可以是class属性,也可以是实例属性,回顾一下属性调用的优先级:
a.x
调用优先级为:a.__dict__['x']
,type(a).__dict__['x']
,所以当一个class属性不存在同名的实例属性时,通过class和实例都可以调用。
描述器是基于属性的调用来发挥作用的,对于一个描述器(它是一个class 属性)在不存在实例属性x
的情况下,a.x
等价于A.x
等价于type(a).__dict__['x']
。
-
以下是触发描述器
__get__
方法的情景:直接调用:
x.__get__(a)
通过实例调用:
a.x
通过类调用:
A.x
以下是触发描述器
__set__
方法的情景:
class A(object):
pass
class Descriptor(object):
def __set__(self, instance, value):
pass
A.b = Descriptor()
a = A()
# 用class调用不会触发__set__
# A.b = 'b'
# 实例调用才会触发__set__
a.b = 'b'
- 以下是触发描述器
__delete__
方法的情景:
class A(object):
pass
class Descriptor(object):
def __set__(self, instance, value):
pass
def __delete__(self, instance):
pass
a = A()
A.b = Descriptor()
del a.b
数据型和非数据型描述器
数据型描述器:
定义了__set__
和__get__
方法,这种描述器可以监听读和写非数据型描述器:
只定义了__get__
方法,只能监听属性的读取操作。
二者的区别在于,以a.b
为例:
对于一个非数据型描述器b
,如果给a
设置了一个实例属性b
,根据属性读取的优先级,a.b
读取的将是a
的实例属性b
,描述器的__get__
方法将不会被触发。而对于数据型描述器,即使存在一个同名的实例属性,a.b
访问的仍然是描述器,数据型描述器不会被实例属性覆盖。
class DataDescriptor(object):
def __set__(self, instance, value):
print('data descriptor set')
def __get__(self, instance, owner=None):
return 'data descriptor get'
class NonDataDescriptor(object):
def __get__(self, instance, owner=None):
return 'non-data descriptor get'
class A(object):
dataDescriptor = DataDescriptor()
nonDataDescriptor = NonDataDescriptor()
pass
a = A()
print('a.dataDescriptor: {}'.format(a.dataDescriptor))
print('a.nonDataDescriptor: {}'.format(a.nonDataDescriptor))
a.dataDescriptor = 11
# 非数据型描述器会被实例属性覆盖
a.nonDataDescriptor = 22
print('a.dataDescriptor: {}'.format(a.dataDescriptor))
print('a.nonDataDescriptor: {}'.format(a.nonDataDescriptor))
>>> a.dataDescriptor: data descriptor get
>>> a.nonDataDescriptor: non-data descriptor get
>>> data descriptor set
>>> a.dataDescriptor: data descriptor get
>>> a.nonDataDescriptor: 22