Python装饰器

装饰器

  • 装饰器简介
  • 装饰器基本知识
  • 闭包
  • 参数化装饰器
  • 标准库中的装饰器

1 简介

Python里我们经常能见到@开头的句法,没错@符号就是装饰器的语法糖,也就是人们常说的装饰器(decorator)。装饰器是Python非常重要的一部分,能够产出更易于维护的代码。

装饰器本质上是一个Python函数或类,它可以让其他函数或类在不需要做任何代码修改的前提下增加额外功能,装饰器的返回值也是一个函数/类对象。它应用场景也比较广泛,比如:插入日志、性能测试、事务处理、缓存、权限校验等。有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码到装饰器中并继续重用。概括的讲,装饰器是用于在源码中"标记"函数,以某种方式增强函数的行为。

装饰器有两个特性:

  • 能把被装饰的函数替换成其他函数。
  • 装饰器在加载模块时立即执行(在被装饰函数的定义后立即执行)。

2 装饰器基本知识

装饰器是可调用对象,其参数是另一个函数(被装饰的函数)。装饰器可能会处理被装饰的函数,然后把它返回,或者替换成另一个函数或可调用的对象。

假如有个名为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啊,我果然没看错你,今年好好干,明年我再给你娶个嫂子。

3 闭包

若想掌握装饰器,就必须理解闭包。

闭包:指延伸了作用域的函数,其中包含函数定义体中引用、但是不在定义体中定义的非全局变量。

闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,这样调用函数时,虽然定义作用域不可用了,但仍能使用那些绑定(自由变量)。

自由变量:未在本地作用域绑定的变量。

下面给出一个例子,函数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列表换成了counttotal整数类型。当count是数字或其他不可变类型时,count+=1就是count=count+1;在averager定义体中为count赋值,就会把count变成局部变量。当然,total变量也一样。如下所示,因为counttotal被当作(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关键字将counttotal声明为自由变量即可。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
>>> 

4 实现一个简单的装饰器

需求如下,大部分函数都会执行计算过程,我们写一个装饰器,用来记录函数执行时间。

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
>>> 

5 装饰器何时运行

我在前面讲过,装饰器有两个特性:

  • 能把被装饰的函数替换成其他函数。
  • 装饰器在加载模块时立即执行(在被装饰函数的定义后立即执行)。

即,装饰器在加载模块时立即执行,也可以说装饰器在被装饰的函数定义后立即执行。

下面例子定义了一个注册函数的装饰器: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函数的引用。

6 参数化装饰器

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>]
>>>

7 标准库中的装饰器

  • staticmethod

    将方法转换为静态方法。

  • classmethod

    将方法转换为类方法。

  • property

    返回property(特性)属性。

  • abc.abstractmethod

    将类方法声明为抽象方法。

  • functools.lru_cache

    LRU(Least Recently Used)缓存,只有要重用以前计算的值时才应使用LRU高速缓存。

  • functools.singledispatch

    将普通函数变成泛函数

  • functools.wraps

    协助构建行为良好的装饰器

标准库中的装饰器很多,这里只列举了一些。下面我会挑几个常见的装饰器进行讲解。

(1)staticmethod

静态方法不会接收隐式的第一个参数。要声明静态方法,请使用此惯用法:

class A:
    @staticmethod
    def f(arg1, arg2, ...): ...

@staticmethod形式是一个函数装饰。

它可以在类(如A.f())或类实例(A().f())上被调用。

其实,静态方法就是普通的函数,只是碰巧在类的定义体中,而不是在模块层定义。

(2)classmethod

类方法接收类作为隐式的第一个参数,就像实例方法接收实例一样。要声明一个类方法,请使用此惯用法:

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']

(3)property(特性)

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.")

如果aA的实例,a.x将调用gettera.x=value将调用setterdel a.x则调用deleter

property对象具有gettersetterdeleter可用作装饰器的方法。这些装饰器创建属性的副本,并将相应的访问函数设置为装饰函数。此代码与上一个示例(典型用法)完全等效,例子如下:

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
>>> 

(4)functools.lru_cache

@functools.lru_cachemaxsize = 128typed = False

functools.lru_cache实现了备忘功能,它把耗时的函数结果缓存在字典里,所以函数的位置参数或关键字参数必须是可hash的

  • 如果将maxsize设置为None,则禁用LRU功能。缓存可以无限制地增长。当maxsize是2的幂时,LRU功能表现最佳。
  • 如果typed设置为true,则将分别缓存不同类型的函数参数。例如,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]
>>>

参考文章

  • 《Fluent Python》——函数装饰器和闭包

你可能感兴趣的:(Python)