Python从入门到精通之装饰器

闭包

def outer():
    x = 10
    def inner(): # 内部函数
        print(x) # 引用外部变量
    return inner # inner是闭包

f = outer()
f() # f在outer()外执行,却能取到其内部变量x

如果在一个内部函数中,对外部作用域(但不是全局作用域)的变量进行引用,那么内部函数就被认为是闭包(closure)。
闭包 = 函数快 + 定义函数时的环境

装饰器概念

装饰器本质上是一个函数,该函数用来处理其他函数,它可以让其他函数在不需要修改代码的前提下增加额外的功能,装饰器的返回值也是一个函数对象。它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等应用场景。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能。

先来看一个简单例子:

def foo():
    print('I am fool')

现在有一个新的需求,希望可以记录下函数的执行前后的时间,于是在代码中添加日志代码

import time, logging

def fool():
    print("Begin:", time.ctime())
    print('I am fool')
    print("End:", time.ctime())

foo()

bar()bar2()也有类似的需求,怎么做?再在bar函数里调用时间函数?这样就造成大量雷同的代码,为了减少重复写代码,我们可以这样做,重新定义一个函数:专门设定时间

import time
def show_time(func):
    print("Begin:", time.ctime())
    func()
    print("End", time.ctime())

def bar():
    print("I am bar")

show_time(bar)

逻辑上不难理解,而且运行正常。 但是这样的话,我们每次都要将一个函数作为参数传递给show_time函数。而且这种方式已经破坏了原有的代码逻辑结构,之前执行业务逻辑时,执行运行bar(),但是现在不得不改成show_time(bar)。那么有没有更好的方式的呢?当然有,答案就是装饰器。

简单装饰器

如果令bar 等于show_time(bar) ,就不要改动以前的代码了。所以,我们需要show_time(bar)返回一个函数对象,而这个函数对象内则是核心业务函数:func()与装饰函数:两个时间函数,修改如下:

import time
def show_time(func):
    def wrapper():
        print("Begin:", time.ctime())
        func()
        print("End", time.ctime())
    return wrapper

def bar():
    print("I am bar")

bar = show_time(bar)
bar()

函数show_time就是装饰器,它把真正的业务方法func包裹在函数里面,看起来像bar被上下时间函数装饰了。在这个例子中,函数进入和退出时 ,被称为一个横切面(Aspect),这种编程方式被称为面向切面的编程(Aspect-Oriented Programming)。
@符号是装饰器的语法糖,在定义函数的时候使用,避免再一次赋值操作。

import time
def show_time(func):
    def wrapper():
        print("Begin:", time.ctime())
        func()
        print("End", time.ctime())
    return wrapper

@showtime   # bar = show_time(bar)
def bar():
    print('I am bar')

@show_time  # bar2 = show_time(bar2)
def bar2():
    print('I am bar2')

bar()
bar2()

如上所示,这样我们就可以省去bar = show_time(bar)这一句了,直接调用bar()即可得到想要的结果。如果我们有其他的类似函数,我们可以继续调用装饰器来修饰函数,而不用重复修改函数或者增加新的封装。这样,我们就提高了程序的可重复利用性,并增加了程序的可读性。

带参数的被装饰函数

import time

def show_time(func):
    def wrapper(a, b):
        print("Begin:", time.ctime())
        func(a, b)
        print("End:", time.ctime())
    return wrapper

@show_time 
def add(a, b):
    print(a+b)

add(2, 3)

即:被装饰的功能函数带有参数时,只需在wrapper()内对应加上参数即可

带参数的装饰器

装饰器还有更大的灵活性,例如带参数的装饰器:在上面的装饰器调用中,比如@show_time,该装饰器唯一的参数就是执行业务的函数。装饰器的语法允许我们在调用时,提供其它参数,比如@decorator(a)。这样,就为装饰器的编写和使用提供了更大的灵活性。

import time 

def cal_time(flag):
    def show_time(func):
        def wrapper(a, b):
            start = time.ctime()
            print("Begin:", start)
            func(a, b)
            end = time.ctime()
            print("End:", end)
            if flag:
                print("Expend time:", time.mktime(time.strptime(end))-time.mktime(time.strptime(start)))
        return wrapper 
    return show_time

@cal_time(True)
def add(a, b):
    c = a + b
    time.sleep(1)
    print(c)

add(1, 2)

上面的cal_time是允许带参数的装饰器。它实际上是对原有装饰器的一个函数封装,并返回一个装饰器。我们可以将它理解为一个含有参数的闭包。当我 们使用@cal_time("true")调用的时候,Python能够发现这一层的封装,并把参数传递到装饰器的环境中。

类装饰器

再来看看类装饰器,相比函数装饰器,类装饰器具有灵活度大、高内聚、封装性等优点。使用类装饰器还可以依靠类内部的__call__方法,当使用 @ 形式将装饰器附加到函数上时,就会调用此方法。

class Foo():
    def __init__(self, func):
        self.func = func

    def __call__(self):
        print('class decorator running')
        self.func()
        print('class decorator ending')

@Foo
def bar():
    print('bar')

bar()
++++++++++++++++++++++++++++++
class decorator running
bar
class decorator ending

functools.wraps

使用装饰器极大地复用了代码,但是他有一个缺点就是原函数的元信息不见了,比如函数的docstring__name__、参数列表,先看例子:

def foo():
    print('foo')

print(foo.__name__)

def logged(func):
    def wrapper(*args, **kwars):
        print(func.__name__ + 'was called')
        return func(*args, **kwargs)
    return wrapper

@logged
def bar(x):
    return x + x * x

print(bar.__name__)

++++++++++++++++++++++++++++++
foo
wrapper

函数

@logged
def bar(x):
    return x + x * x

等价于

def bar(x):
    return x + x * x
bar = logged(bar)

不难发现,函数barwrapper取代了,于是其docstring__name__就变成了wrapper函数的信息了。
这个问题可能引起一定的麻烦,好在我们有functools.wrapswraps本身也是一个装饰器,它能把原函数的元信息拷贝到装饰器函数中,这使得装饰器函数也有和原函数一样的元信息了。

from functools import wraps
def foo():
    print('foo')

print(foo.__name__)

def logged(func):
    @wraps(func)  # 只需加上这一句
    def wrapper(*args, **kwars):
        print(func.__name__ + 'was called')
        return func(*args, **kwargs)
    return wrapper

@logged
def bar(x):
    return x + x * x

print(bar.__name__)

++++++++++++++++++++++++++++++
foo
bar

内置装饰器

  • @staticmathod
  • @classmethod
  • @property

装饰器的顺序

@a
@b
@c
def f ():

等效于

f = a(b(c(f)))

猜你喜欢

转载自blog.csdn.net/u010525694/article/details/79879272