头图

提到Python的重难点,装饰器肯定得占一席之地,就感觉像看九阴真经里面的怪文字,看不懂,完全一脸懵......

如何理解Python装饰器?

说回正题,如何理解Python的装饰器?

装饰器(decorator)可以说是Python的一个神器,它可以在不改变一个函数代码和调用方式的情况下给函数添加新的功能。

本质上是一个嵌套函数,它接受被装饰的函数(func)作为参数,并返回一个包装过的函数。

这样我们可以在不改变被装饰函数的代码的情况下给被装饰函数或程序添加新的功能。

装饰器的原则组成:
< 函数+实参高阶函数+返回值高阶函数+嵌套函数+语法糖 = 装饰器 >

Python的装饰器广泛应用于:

  • 缓存
  • 权限校验(如django中的@login_required和@permission_required装饰器)
  • 性能测试(比如统计一段程序的运行时间)
  • 插入日志

等应用场景(两个常见的入门用法日志打印器、时间计时器),有了装饰器,我们就可以抽离出大量与函数功能本身无关的代码,增加一个函数的重用性。

很多初学者对装饰器难以理解,可能是因为以下三点内容没有理解到位:

  • 关于函数“变量”(或“变量”函数)的理解
  • 关于高阶函数的理解
  • 关于嵌套函数的理解

因为装饰器本质上还是函数,如果能将以上问题逐一攻破,同时遵循装饰器的基本原则,相信会对装饰器有个很好的理解的。

语法糖写法

如果你接触装饰器有一段时间了的话,对 @ 符号一定不陌生了。

@ 符号是装饰器的语法糖;

它放在一个函数开始定义的地方,就像一顶帽子戴在这个函数的头上。

语法糖的书写格式: @装饰器名字,通过语法糖的方式也可以完成对已有函数的装饰。

例如:

# 定义装饰器
def decorator(func):
    def inner():
        # 在内部函数里面对已有函数进行装饰
        print('已添加登录认证')
        func()
    return inner

@decorator  # comment = decorator(comment) 装饰器语法糖对该代码进行了封装 左边comment=inner
def comment():
    print('发表评论')
# 调用方式不变
comment()

当前模块加载完成以后,装饰器会立即执行,对已有函数进行装饰。

装饰有参函数

import time

def timer(func)
    def deco():  
        start = time.time()
        func()
        stop = time.time()
        print(stop-start)
    return deco

@timer
def test(parameter): #8
    time.sleep(2)
    print("test is running!")   
test()

对于一个实际问题,往往是有参数的,如果要在#8处,给被修饰函数加上参数,显然这段程序会报错的。

错误原因是test()在调用的时候缺少了一个位置参数的,而我们知道test = func = deco,因test()=func()=deco(),那么当test(parameter)有参数时,就必须给func()和deco()也加上参数。

为了使程序更加有扩展性,因此在装饰器中的deco()和func(),加了可变参数agrs和 *kwargs。

import time

def timer(func)
    def deco(*args, **kwargs):  
        start = time.time()
        func(*args, **kwargs)
        stop = time.time()
        print(stop-start)
    return deco

@timer
def test(parameter): #8
    time.sleep(2)
    print("test is running!")   
test()

带有参数的装饰器

带有参数的装饰器就是使用装饰器装饰函数的时候可以传入指定参数;

语法格式:@装饰器(参数,…)

使用带有参数的装饰器,其实是在装饰器外面又包裹了一个函数,使用该函数接收参数,返回是装饰器。

因为 @ 符号需要配合装饰器实例使用。

def return_decorator(flag):
    # 装饰器只能接收一个参数并且是函数类型
    def decorator(func):
        def inner(a, b):
            if flag == '+':
                print('正在努力执行加法计算')
            elif flag == '-':
                print('正在努力执行减法计算')
            func(a, b)
        return inner
    # 当调用函数的时候可以返回一个装饰器decorato
    return decorator

@return_decorator('+')  # decorator = return_decorator('+'), @decorator => add_num = decorator(add_num)
def add_num(a, b):
    result = a + b
    print(result)
    
@return_decorator('-')
def sub_num(a, b):
    result = a - b
    print(result)

add_num(1, 2)
sub_num(1, 2)

由于Python装饰器的工作原理主要依赖于嵌套函数和闭包,所以我们必须先对嵌套函数和闭包有深入的了解。

嵌套函数和闭包几乎是Python工作面试必考题;

说自己掌握了 Python,那装饰器是必须要会的,反之则不能说你学会了 Python。

还有诸如迭代器、生成器、列表解析式、生成器表达式等等这些都应该懂吧。

嵌套函数

如果在一个函数的内部还定义了另一个函数(注意:是定义,不是引用!)这个函数就叫嵌套函数。

外部的我们叫它外函数,内部的我们叫他内函数。

先来看一个最简单的嵌套函数的例子;

在outer函数里又定义了一个inner函数,并调用了它:

def outer():
    x = 1
    def inner():
        y = x + 1
        print(y)
    inner()

outer() #输出结果 2

可以看到内函数在自己作用域内查找局部变量失败后,会进一步向上一层作用域里查找。

如果我们在外函数里不直接调用内函数,而是通过return inner返回一个内函数的引用,这时会发生什么?

你将会得到一个内函数对象,而不是运行结果。

def outer():
    x = 1
    def inner():
        y = x + 1
        print(y)
    return inner

outer() # 输出<function outer.<locals>.inner at 0x039248E8>
f1 = outer()
f1() # 输出2

上述案例比较简单,因为outer和inner函数都是没有参数的。

我们现在对上述代码加入参数,你可以看到外函数的参数或变量可以很容易传递到内函数。

def outer(x):
    a = x

    def inner(y):
        b = y
        print(a+b)

    return inner

f1 = outer(1) # 返回inner函数对象
f1(10) # 相当于inner(10)。输出11

如果上例中外函数的变量x换成被装饰函数对象(func),内函数的变量y换成被装饰函数的参数,我们就可以得到一个通用的装饰器。

我们在没对func本身做任何修改的情况下,添加了其它功能, 从而实现了对函数的装饰。

如下示例:

def decorator(func):
    def inner(*args, **kwargs):
        add_other_actions()
        return func(*args, **kwargs)
    return inner

decorator返回的仅仅是inner函数吗? NO!

它返回的其实是个闭包,整个装饰器的工作都依赖于Python的闭包原理。

Python闭包

如果一个外函数中定义了一个内函数,且内函数体内引用到了体外的变量,这时外函数通过return返回内函数的引用时,会把定义时涉及到的外部引用变量和内函数打包成一个整体(闭包)返回。

在之前的示例代码,outer方法返回的只是内函数对象吗? 

不,outer函数返回的实际上是一个由inner函数和外部引用变量(a)组成的闭包!

def outer(x):
    a = x

    def inner(y):
        b = y
        print(a+b)

    return inner


f1 = outer(1) # 返回inner函数对象+局部变量1(闭包)
f1(10) # 相当于inner(10)。输出11

一般一个函数运行结束的时候,临时变量会被销毁,但闭包是一个特别的情况。

当外函数发现自己的临时变量,会在将来的内函数中用到,自己在结束的时候,返回内函数的同时,会把外函数的临时变量同内函数绑定在一起。

这样即使外函数已经结束了,内函数仍然能够使用外函数的临时变量,这就是闭包的强大之处。

万能装饰器

装饰器的外函数会接收一个函数作为参数,这个函数在内函数内部执行,这个函数可以有参数也可以没有参数,可以有返回值也可以没有返回值。

所以装饰器也分为四类:

  • 无参无返回值
  • 无参有返回值
  • 有参无返回值
  • 有参有返回值

是否有参数和返回值完全取决于被装饰的函数;

但是,我们写装饰器的目的就是用一个装饰器来装饰不同的函数,所以要考虑装饰器的通用性。

我们通过可变参数来实现一种可以用来装饰任何函数的装饰器——万能装饰器。

def decorator_all(func):
    def wrapper(*args, **kwargs):
        print('add some coding')
        return func(*args, **kwargs)
    return wrapper

使用这种装饰器,可以用来装饰任意函数,不管

函数是否有参数,是否有返回值。

1)装饰带有参数的函数

def decorator(func):
    def inner(num1, num2):
        print('正在努力执行加法计算')
        func(num1, num2)
    return inner

@decorator
def add_num(num1, num2):
    result = num1 + num2
    print(f'结果为:{result}')

add_num(1, 2)

2)装饰带有参数、返回值的函数

def decorator(func):
    def inner(num1, num2):
        print('正在努力执行加法计算')
        num = func(num1, num2)
        return num
    return inner

@decorator
def add_num(num1, num2):
    result = num1 + num2
    return result

result = add_num(1, 2)
print(f'结果为:{result}')

输出:

正在努力执行加法计算
结果为:3

3)装饰带有不定长参数、返回值的函数

def decorator(func):
    def inner(*args, **kwargs):
        print('正在努力执行加法计算...')
        print("args={}, kwargs={}".format(args, kwargs))
        # *args:把元组里面的每一个元素,按照位置参数的方式进行传参
        # **kwargs:把字典里面的每一个键值对,按照关键字的方式进行传参
        num = func(*args, **kwargs)
        return num
    return inner

@decorator
def add_num(*args, **kwargs):
    result = 0
    for value in args:
        result += value
    for value in kwargs.values():
        result += value
    return result

result = add_num(1, 2, a=3, b=7)
print(f'结果为:{result}')

输出结果:

正在努力执行加法计算
args=(1, 2), kwargs={'a': 3, 'b': 7}
结果为:13

注意:使用装饰器装饰已有函数的时候,内部函

数的类型和要装饰的已有函数的类型保持一致。

多个装饰器同时装饰一个函数

def decorator_one(func):
    def wrapper(*args, **kwargs):
        print('decorator one start')
        result = func(*args, **kwargs)
        print('decorator one end')
        return result
    return wrapper
 
 
def decorator_two(func):
    def wrapper(*args, **kwargs):
        print('decorator two start')
        result = func(*args, **kwargs)
        print('decorator two end')
        return result
    return wrapper
 
 
@decorator_two
@decorator_one
def hello_python():
    print('Hello Python!')
 
 
hello_python()

运行结果:

decorator two start
decorator one start
Hello Python!
decorator one end
decorator two end

可以看到,当多个装饰器装饰同一个函数时,会是一个嵌套的装饰结果;

也就是说,先执行完离函数近的一个装饰器,然后再用离函数远的装饰器来装饰执行结果。

再看一个示例:比如下面使用两个装饰器来装饰 greeting 函数:

@log
@retry(10)
def greeting():
print "Hello, World!"

这段代码等价于:

def greeting():
print "Hello, World!"
temp = retry(10)(greeting)
greeting = log(temp)

可以看到,叠加的装饰器生效的顺序是从内往外的,这一点在使用的时候需要特别注意。

用类来定义一个装饰器

在Python中,也可以通过类的方式来实现装饰器。

在实现类装饰器的时候:

  • 使用__init__()方法来接收被装饰函数
  • 使用__call__()方法来添加装饰器要实现的功能
  • 并在__call__()方法中执行和返回被装饰函数
class Log(object):
def__init__(self, f):
self.f = f
def__call__(self, *args, **kwargs):
print "before"
self.f()
print "after"

使用类装饰器来装饰函数:

@Log
def greeting():
print "Hello, World!"
greeting()

输出结果和使用函数装饰器一样:

before
Hello, World!
after

相比函数装饰器,类装饰器具有面向对象编程所支援的一系列特点,比如高内聚、封装性和灵活度大等优点。

实际上,Python 中任何 callable 的对象都可以用来定义装饰器。

末尾总结

① 概括的来讲,装饰器的作用就是为已经存在的对象添加额外的功能。

② 同时在面向对象(OOP)的设计模式中,decorator被称为装饰模式。

③ OOP的装饰模式需要通过继承和组合来实现,而Python除了能支持OOP的decorator外,直接从语法层次支持decorator。

④ Python的decorator可以用函数实现,也可以用类实现。

内容较为详细,觉得有帮助的话可以前往gzh【Python编程学习圈】了解更多技术干货,还有大量系统化学习资料以及教程可以免费领取,对大家的学习会很有帮助。


程序员小六
7 声望3 粉丝