python 的闭包到各种装饰器

一、闭包

闭包概念:在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。
上面是wiki上的定义,解释出来就是:函数中可以嵌套函数,内部的函数就是一个闭包
闭包有两种作用:

  1. 在一个大函数中使用,为了重复使用某个功能。
def outerFunc():
    outerVar = 10
    def innerFunc_mul(times):
        nonlocal outerVar
        outerVar *= times
    # 扩大两倍
    innerFunc_mul(2)
    # 再扩大三倍
    innerFunc_mul(3)
    print(outerVar)
outerFunc()
  1. 闭包作为返回值,返回外部函数的环境下的某个状态
def outerFunc():
    outerVar = 10
    def innerFunc_mul(times, printFlag=False):
        nonlocal outerVar
        if printFlag:
            print(outerVar)
            return
        outerVar *= times
    return innerFunc_mul # 注意不要加括号

mul = outerFunc()
mul(2)
mul(3)
mul(1, True)

需要注意的是,如果返回闭包,是没有括号的,因为没有括号才是未实例化的函数类。如果加上括号就是实例化的对象了
两个输出都是一样的:
《python 的闭包到各种装饰器》

二、闭包的应用–减少重复

举一个常见的例子,我们做效率分析的时候经常会输出一些函数的执行时间,所以经常会这样:

import time
def func1():
    beginTime = time.time()
    time.sleep(2)
    print("func1 exec time:%s" % (time.time() - beginTime))

def func2():
    beginTime = time.time()
    time.sleep(3)
    print("func2 exec time:%s" % (time.time() - beginTime))

func1()
func2()

输出结果:
《python 的闭包到各种装饰器》
但是这种情况有一些不好的地方:
(1)如果再多加十个函数,其中的一些片段还是重复的,太冗余了。
(2)如果我们这次不想统计执行了多少秒,而是毫秒,岂不是要把所有的函数都改一下,太麻烦了。
观察规律我们发现,其实每个函数的实体都是加在一个固定代码中的,那我们把函数的实体作为变量,固定的地方提取出来不久行了吗,这样就得到了下面的程序。

def changeToCanPrintExecTime(func):
    beginTime = time.time()
    func()
    print("%s exec time:%s" % (func.__name__, time.time() - beginTime))
def func3():
    time.sleep(2)
def func4():
    time.sleep(3)
changeToCanPrintExecTime(func3)
changeToCanPrintExecTime(func4)

这种情况下,输出和我们想要的一样。
但是,看着总是不得劲,因为我们想要执行的是func3和func4,现在执行的都是changeToCanPrintExecTime函数,怪怪的。如果我们还想执行func3和func4怎么办,那就把changeToCanPrintExecTime(func3)赋值给func3就可以了,但这样也就要求changeToCanPrintExecTime需要有返回值,返回的就是changeToCanPrintExecTime内部执行逻辑,这就用到了我们上面说的闭包,程序如下:

def changeToCanPrintExecTime(func):
    def inner():
        beginTime = time.time()
        func()
        print("%s exec time:%s" % (func.__name__, time.time() - beginTime))
    return inner
def func3():
    time.sleep(2)
def func4():
    time.sleep(3)
func3 = changeToCanPrintExecTime(func3)
func4 = changeToCanPrintExecTime(func4)
func3()
func4()

执行结果符合我们的预期:
《python 的闭包到各种装饰器》
这样一来我们还是执行func3,func4,而且如果有十个函数都需要做这样的处理,做一下这样的转换就可以了,后面也比较好维护。

三、装饰器

上面的情况中,我们还是需要利用changeToCanPrintExecTime函数来对func3做重新的赋值,python中定义了一个语法糖,用@符表示。见下面的改版程序

def changeToCanPrintExecTime(func):
    def inner():
        beginTime = time.time()
        func()
        print("%s exec time:%s" % (func.__name__, time.time() - beginTime))
    return inner
@changeToCanPrintExecTime
def func3():
    time.sleep(2)
@changeToCanPrintExecTime
def func4():
    time.sleep(3)
func3()
func4()

这种写法和第二部分的最后的写法在这种情况下是等效的。
这就是一个简单的装饰器,他把第一个参数省略掉了其实装饰器其实就是利用闭包的特点,来对某个函数做一些修饰,这是python的语法糖之一。

四、复杂点的装饰器

第三步中说“这种情况下等效”,是因为上面没有局部变量做干扰,而且装饰器没有参数,接下来分别说明。
1.有局部变量时的情况
比如经典的单例模式可以如下程序实现

def singleton(cls, *args, **kw):
   instances = {}
   def _singleton():
       if cls not in instances:
           instances[cls] = cls(*args, **kw)
       return instances[cls]
   return _singleton

这个装饰器的功能就是保证每个类创建过就不再创建直接使用了。但是如果和上面一样做一个赋值操作来对比的话,

@singleton
class Func(object):
    def __init__(self):
        pass
func5 = Func()
func6 = Func()
print('这个类的地址是:%s'%id(func5))
print('这个类的地址是:%s'%id(func6))
func7 = singleton(Func)
print('这个类的地址是:%s'%id(func7))

结果是这样的:
《python 的闭包到各种装饰器》
前两个一样,最后的那个不一样。原因是前两个都是用的一个装饰器,相当于在内存中维护了一个singleton函数,里面的局部变量自然都是共享的所以实现了相同的类id一样。而func7中重新使用了singleton函数做转换,这和内存中保存的函数是不一样的,所以instances 为空自然就重新创建了

对修饰类的理解:上面changeToCanPrintExecTime函数传的值是函数的值,而singleton传一个类的值,可以理解为在函数中对类cls进行了一些处理,在构造函数__new__中去进行了第二部分的那个重新赋值操作。

2.带参数的装饰器
额外值得说的一点是,函数装饰器,都是为各种不同的函数服务的,所以其中的参数是大有不同。为了兼容各种情况,建议在装饰器中加上*args, **kw。
之前说过,装饰器默认的第一个参数就是函数类(func or cls),但是如果装饰器需要参数来判断的话是把参数加到后面就可以吗。比如我们打印执行时间的装饰器如果添加一个参数是可以根据函数来控制使用单位是毫秒还是秒。
我的第一反应是这么写:
《python 的闭包到各种装饰器》
但不幸的是报错了,因为如果这样写就是把’ms’传入到了func中,不支持这样。正确的做法是下面这样:

def changeToCanPrintExecTime(unit='s'):
    def deco(func):
        def inner(*args, **kwargs):
            beginTime = time.time()
            func(*args, **kwargs)
            spendTime = time.time() - beginTime
            if unit == 'ms':
                spendTime = spendTime * 1000
            print("%s exec time:%s" % (func.__name__, spendTime))
        return inner
    return deco

@changeToCanPrintExecTime(unit='ms')
def func3(arg):
    time.sleep(arg)
# 括号也不能省
@changeToCanPrintExecTime()
def func4():
    time.sleep(3)
func3(2)
func4()

上面的func4进行修饰时,括号也不能省略,changeToCanPrintExecTime函数返回的deco才是真正的装饰,不加括号的话只是一个类不是装饰器。

3.类装饰器
首先要知道python类中有一个内置函数名为__call __,类如果定义了__call__函数的话,实例化后是可以和函数一样使用的,但凡是可以把一对括号()应用到某个对象身上都可称之为可调用对象,判断对象是否为可调用对象需要用到callable(),如下面的例子:

class A:
    def __call__(self, *args, **kwargs):
        print(args)
a = A()
print(callable(a))  #返回 True
a(1)  # 返回(1,),实例化的对象也能像函数一样调用了

所以我们只要对__call__函数内进行闭包返回,就和函数的装饰器一样了,原理和上面说的一样。
那就有一个问题了,有了函数装饰器为什么要用类装饰器呢,看下面的例子:

class A:
    def __call__(self, func):
        def wrapper( *args, **kw):
            self.doSomething()
            return func( *args, **kw)
        return wrapper

    def doSomething(self):
         print('doSomething')

class B(A): #B继承A类,重写A类的doSomething方法
    def doSomething(self):
        print("B doSomething")

@A()
def funcA():
    print("funcA")
funcA()
@B()  
def funcB():
    print("funcB")
funcB()

可以通过继承来重写不同的方法,这样更加的灵活。
额外需要注意的是,刚才说过类需要实例化后才能调用__call__ 函数,所以@A和@B都需要有括号,代表实例化后,调用了 __call__函数。

点赞