Python里我们经常能见到@开头的句法,没错@符号就是装饰器的语法糖,也就是人们常说的装饰器(decorator)。装饰器是Python非常重要的一部分,能够产出更易于维护的代码。
装饰器本质上是一个Python函数或类,它可以让其他函数或类在不需要做任何代码修改的前提下增加额外功能,装饰器的返回值也是一个函数/类对象。它应用场景也比较广泛,比如:插入日志、性能测试、事务处理、缓存、权限校验等。有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码到装饰器中并继续重用。概括的讲,装饰器是用于在源码中"标记"函数,以某种方式增强函数的行为。
装饰器有两个特性:
装饰器是可调用对象,其参数是另一个函数(被装饰的函数)。装饰器可能会处理被装饰的函数,然后把它返回,或者替换成另一个函数或可调用的对象。
假如有个名为decorate的装饰器:
@decorate
def target():
print("running target()")
则上述代码与下述代码等效。
def target():
print("running target()")
target = decorate(target)
有了上面的基础知识,我说个例子更深入讲解装饰器。比如,你在某家公司上班,然后老板跟你说:小A啊,公司代码库的很多函数没有记录函数执行日志功能,希望你解决这个问题。代码库某个函数如下:
def hello():
print("hello, world")
然后你立马在函数后面写下日志代码,然后给老板看:
def hello():
print("hello, world")
logging.info("function 'hello' is running.")
老板一看,语重心长的说,“代码库的函数可是成千上万,一个一个改太慢了;而且啊,代码库中有很多核心代码,不允许做修改哦”。
突然你又想到了一个办法,对老板说:用函数封装怎么样?
def use_log(func):
func()
logging.info("function 'hello' is running")
def hello():
print("hello, world")
但老板立马否定了你的想法:逻辑上没问题,但本来我要使用hello函数,却不得不调用use_log,不仅改变了原有的代码结构,还不得不每次都把函数传给use_log做参数,小A呀,你再想一想,有没有更好的办法。
经过反复的思考,终于想到了解决办法:装饰器,它的作用不正是不改变源码的情况下增加函数功能么。
def use_log(func):
def decorate():
logging.info("%s is runing"%func.__name__)
return func()
return decorate
def hello():
print("hello, world")
hello = use_log(hello)
#与下面代码等价
@use_log
def hello():
print("hello, world")
老板看了你写的这个代码之后说,不错,已经可以实现函数记录日志功能了,不过好像你写的函数都没有参数,如果函数需要参数怎么办呀?
这难不倒你,*args
与*kwargs
就是答案:
def use_log(func):
def decorate(*args,**kwargs):
logging.info("%s is runing"%func.__name__)
return func(*args,**kwargs)
return decorate
@use_log
def say_hello(name):
print("hello, %s"%name)
>>> say_hello('world')
hello, world
终于,老板满意的笑了,说:小A啊,我果然没看错你,今年好好干,明年我再给你娶个嫂子。
若想掌握装饰器,就必须理解闭包。
闭包:指延伸了作用域的函数,其中包含函数定义体中引用、但是不在定义体中定义的非全局变量。
闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,这样调用函数时,虽然定义作用域不可用了,但仍能使用那些绑定(自由变量)。
自由变量:未在本地作用域绑定的变量。
下面给出一个例子,函数make_avg用于计算递增的系列值的均值:
>>> def make_avg():
#####闭包的开始#####
data = []
def averager(value):
#averager的闭包延伸到本函数的作用域之外,包含自由变量data的绑定
data.append(value) #自由变量data的绑定
return sum(data)/len(data)
#####闭包的结束#####
return averager
>>> avg = make_avg()
>>> avg(5)
5.0
>>> avg(1)
3.0
>>>
nonlocal
关键字:作用是把变量标记为自由变量。
下面的代码还是make_avg
函数,不同的是将data
列表换成了count
、total
整数类型。当count
是数字或其他不可变类型时,count+=1
就是count=count+1
;在averager
定义体中为count
赋值,就会把count
变成局部变量。当然,total
变量也一样。如下所示,因为count
、total
被当作(averager
中)局部变量,使用时又发现其没有被赋值,所以就抛出UnboundLocalError
异常.
>>> def make_avg():
count = 0
total = 0
def averager(value):
count += 1
total += value
return total/count
return averager
>>> avg = make_avg()
>>> avg(5)
Traceback (most recent call last):
...
UnboundLocalError: local variable 'count' referenced before assignment
>>>
解决方法很简单,使用nonlocal
关键字将count
、total
声明为自由变量即可。nonlocal count,total
语句将变量count
,total
声明成自由变量,如果为nonlocal
声明的变量赋值,闭包中保存的绑定就会更新。
>>> def make_avg():
count = 0
total = 0
def averager(value):
nonlocal count,total
count += 1
total += value
return total/count
return averager
>>> avg = make_avg()
>>> avg(5)
5.0
>>> avg(1)
3.0
>>>
需求如下,大部分函数都会执行计算过程,我们写一个装饰器,用来记录函数执行时间。
import time
def clock(func):
def clocked(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
use_time = time.time() - start
name = func.__name__#func.__name__是函数名称
arg_str = ', '.join(repr(arg) for arg in args)
kwarg_str = ', '.join(["%s=%s"%(k,v) for k,v in kwargs.items()])
_arg_str = arg_str+","+kwarg_str if kwarg_str else arg_str
print("[%0.8fs] %s(%s) -> %r"%(use_time,name,_arg_str,result))
return result
return clocked
@clock
def factorial(n):
return 1 if n<2 else n*factorial(n-1)
>>> factorial(6)
[0.00000000s] factorial(1) -> 1
[0.00301003s] factorial(2) -> 2
[0.00301003s] factorial(3) -> 6
[0.00401402s] factorial(4) -> 24
[0.00501752s] factorial(5) -> 120
[0.00601840s] factorial(6) -> 720
720
>>>
我在前面讲过,装饰器有两个特性:
即,装饰器在加载模块时立即执行,也可以说装饰器在被装饰的函数定义后立即执行。
下面例子定义了一个注册函数的装饰器:registry
列表用于保存着被register
装饰器装饰的函数引用,register
装饰器功能只是运行输出被装饰函数的名称,再把被装饰函数引用添加到registry
中。
#register.py
registry = []
def register(func):
print("running register(%s)."%func)
registry.append(func)
return func
@register
def f1():
print("running f1.")
def f2():
print("running f2.")
if __name__ == '__main__':
print("registry -> ",registry)
f1()
f2()
运行register.py文件(python register.py
),输出结果如下:
running register().
registry -> []
running f1.
running f2.
根据装饰器在被装饰函数的定义后立即执行这一特性,推出先运行装饰器函数装饰f1
,所以registry
列表保存着f1
函数的引用。
Python把被装饰的函数作为第一个参数传给装饰器。那怎么让装饰器接受其他参数(如@functools.lru_cache(128)
)呢?
答案是:创建一个装饰器工厂函数,把参数传给它,返回一个装饰器。
注册函数装饰器,被装饰的函数自动注册到registry
列表中;装饰器新增参数active
, True
表示注册到列表,False
表示不注册;默认active=True
,即注册到列表。
registry = []
def register(active=True):
def decorate(func):
print("running register(%s)."%func)
if active:
registry.append(func)
return func
return decorate
>>> @register()
... def f1():
... print("running f1.")
...
running register(<function f1 at 0x000002A8E9824730>).
>>> @register(active=False) #active为False,表示不注册被装饰器函数f2
... def f2():
... print("running f2.")
...
running register(<function f2 at 0x000002A8E98247B8>).
>>> registry#如预期,只有f1函数被注册
[<function f1 at 0x000002A8E9824730>]
>>>
staticmethod
将方法转换为静态方法。
classmethod
将方法转换为类方法。
property
返回property(特性)属性。
abc.abstractmethod
将类方法声明为抽象方法。
functools.lru_cache
LRU(Least Recently Used)缓存,只有要重用以前计算的值时才应使用LRU高速缓存。
functools.singledispatch
将普通函数变成泛函数
functools.wraps
协助构建行为良好的装饰器
标准库中的装饰器很多,这里只列举了一些。下面我会挑几个常见的装饰器进行讲解。
静态方法不会接收隐式的第一个参数。要声明静态方法,请使用此惯用法:
class A:
@staticmethod
def f(arg1, arg2, ...): ...
@staticmethod
形式是一个函数装饰。
它可以在类(如A.f()
)或类实例(A().f()
)上被调用。
其实,静态方法就是普通的函数,只是碰巧在类的定义体中,而不是在模块层定义。
类方法接收类作为隐式的第一个参数,就像实例方法接收实例一样。要声明一个类方法,请使用此惯用法:
class A:
@classmethod
def f(cls, arg1, arg2, ...): ...
classmethod
改变了调用方法的方式,类方法的第一个参数是类本身,而不是实例。
classmethod
最常见的用途就是定义备选构造方法。
classmethod
非常有用,而staticmethod
很少用(用处不大)。
>>> class A:
... @classmethod
... def clsmethod(*args):
... return args #返回所有位置参数
... @staticmethod
... def stcmethod(*args):
... return args
...
>>> A.clsmethod()
(<class '__main__.A'>,)
>>> A.clsmethod('cls') #clsmethod第一个参数始终是A类
(<class '__main__.A'>, 'cls')
>>> A.stcmethod()
()
>>> A.stcmethod('stc') #A.stcmethod()行为与普通函数类似
('stc',)
>>>
classmethod
的备选构造方法:
d = {
'postion':[39.5436049640,116.5935223959],
'address':"北京",
'chairman':{
'sex':"男",
'name':"xu",
'hobby':[
{
'sport':['run','swim'],
'culture':['read','chess']
},
]
}
}
>>> d['chairman']['hobby'][0]['sport']
['run','swim']
d['chairman']['hobby'][0]['sport']
这种句法太过冗长。在JavaScript中,可以使用d.chairman.hobby[0].sport
获取值。现在我们使用动态属性方法JSON类数据。
from collections import abc
class JSON:
def __init__(self,mapping):
self.__data = dict(mapping)
def __getattr__(self, name):
if hasattr(self.__data, name):
return getattr(self.__data, name)
else:
return JSON.build(self.__data[name])
@classmethod
def build(cls, obj):
if isinstance(obj, abc.Mapping):#obj映射,构建一个JSON对象
return cls(obj)
elif isinstance(obj, abc.MutableSequence):#列表
return [cls.build(item) for item in obj]
else:#不是映射也不是列表,原封不动返回元素
return obj
>>> js = JSON(d)
>>> js.chairman.hobby[0].sport
['run', 'swim']
class property
(fget=None, fset=None, fdel=None, doc=None)
fget是获取属性值的函数。 fset是用于设置属性值的函数。fdel是用于删除属性值的函数。doc是创建属性的文档字符串。property
类型在python2.2中引入,python2.4才引入@装饰器句法。
典型用法是定义托管属性x
:
class A:
def __init__(self):
self._x = None
def getx(self):
return self._x
def setx(self, value):
self._x = value
def delx(self):
del self._x
x = property(getx, setx, delx, "I'm the 'x' property.")
如果a
为A
的实例,a.x
将调用getter
,a.x=value
将调用setter
,del a.x
则调用deleter
。
property
对象具有getter
,setter
,deleter
可用作装饰器的方法。这些装饰器创建属性的副本,并将相应的访问函数设置为装饰函数。此代码与上一个示例(典型用法)完全等效,例子如下:
class A:
def __init__(self):
self._x = None
@property
def x(self):
"""I'm the 'x' property."""
return self._x
@x.setter
def x(self, value):
self._x = value
@x.deleter
def x(self):
del self._x
特性(property)都是类属性,但特性管理的其实是实例属性的存取。实例属性会覆盖类属性,而特性会覆盖实例属性。
实例属性覆盖类的数据属性:
>>> class C:
data = "It's class attr."
>>> c = C()
>>> c.data
"It's class attr."
>>> c.data = 'object attr'
>>> vars(c)
{'data': 'object attr'}
>>> c.data
'object attr'
>>>
实例属性不会覆盖类特性(property):
>>> class C:
data = "It's class attr."
@property
def prop(self):
return "class property"
>>> c = C()
>>> c.prop#读取prop会执行特性的读值方法
'class property'
>>> c.prop = 'object property'#尝试设置prop实例属性,结果失败
Traceback (most recent call last):
File "" , line 1, in <module>
c.prop = 'object property'
AttributeError: can't set attribute
新添加的类特性会覆盖现有的实例属性
>>> class C:
data = "It's class attr."
@property
def prop(self):
return "class property"
>>> c = C()
>>> c.data
"It's class attr."
>>> c.data = 'object attr'
>>> c.data
'object attr'
>>> C.data = property(lambda self:"property data") #使用新特性覆盖C.data
>>> c.data #实例data熟悉被C.data特性覆盖
'property data'
>>> del C.data #删除特性
>>> c.data #不再覆盖实例data属性
'object attr'
订单商品类实例:利用property(特性)实现值的验证
class Goods:
def __init__(self, name, price, weight, description=None):
self.name = name
self.price = price
self.weight = weight
self.description = description
def total(self):
return self.weight*self.price
@property
def price(self):#实现特性的方法,其名称都与公开属性的名称一样--price
return self.__price#真正的属性存储在私有属性 __price 中
@price.setter
def price(self, value):
if value > 0:
self.__price = value
else:
raise ValueError('value must be > 0')
@property
def weight(self):#实现特性的方法,其名称都与公开属性的名称一样--weight
return self.__weight
@weight.setter
def weight(self, value):
if value > 0:
self.__weight = value
else:
raise ValueError('value must be > 0')
g = Goods('rabbit', 10, 0.2, '大白兔奶糖')
>>> g.price = -1
Traceback (most recent call last):
...
ValueError: value must be > 0
>>>
@functools.lru_cache
(maxsize = 128,typed = False )
functools.lru_cache实现了备忘功能,它把耗时的函数结果缓存在字典里,所以函数的位置参数或关键字参数必须是可hash的
None
,则禁用LRU功能。缓存可以无限制地增长。当maxsize是2的幂时,LRU功能表现最佳。f(3)
、f(3.0)
将被视为产生截然不同的结果而被缓存。如下,使用高速缓存实现计算有效Fibonacci数的示例:
>>> from functools import lru_cache
>>> @lru_cache()
... def fibonacci(n):
... if n<2:
... return 1
... else:
... return fibonacci(n-1)+fibonacci(n-2)
...
>>> fibonacci(10)
89
>>> fibonacci(100)#如果不用lru缓存,则此列需要计算tooooloooooongtime
573147844013817084101
>>> [fibonacci(i) for i in range(10)]
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
>>>