答疑·限时优惠

如果,你想让我看见你的疑问并且百分之百的回答。可以加入我的知识星球。
AI悦创·进化岛
AI悦创·进化岛

img

你好,我是悦创。

这节课我们一起来学装饰器。

装饰器一直以来都是 Python 中很有用、很经典的一个 feature(特色),在工程中的应用也十分广泛,比如日志、缓存等等的任务都会用到。然而,在平常工作生活中,我发现不少人,尤其是初学者,常常因为其相对复杂的表示,对装饰器望而生畏,认为它“too fancy to learn”,实际并不如此。

今天这节课,我会以前面所讲的函数、闭包为切入点,引出装饰器的概念、表达和基本用法,最后,再通过实际工程中的例子,让你再次加深理解。

接下来,让我们进入正文一起学习吧!

1. 函数 -> 装饰器

函数核心回顾引入装饰器之前,我们首先一起来复习一下,必须掌握的函数的几个核心概念。

1.1 第一点

我们要知道,在 Python 中,函数是一等公民(first-class citizen),函数也是对象。我们可以把函数赋予变量,比如下面这段代码:

def func(message):
    print('Got a message: {}'.format(message))

send_message = func
send_message('hello aiyuechuang')

# 输出
Got a message: hello aiyuechuang

这个例子中,我们把函数 func() 赋予了变量 send_message,这样之后你调用 send_message,就相当于是调用函数 func()。(小白同学呢,可以理解成给这个自定义函数取了一个小名,那日常中我们叫一个人的小名,那个人也是会回应你的。)

1.2 第二点

我们可以把函数当作参数,传入另一个函数中,比如下面这段代码:

def get_message(message):
    return 'Got a message: ' + message


def root_call(func, message):
    print(func(message))

root_call(get_message, 'hello aiyuechuang')

# 输出
Got a message: hello aiyuechuang

这个例子中,我们就把函数 get_message()以参数的形式,传入了函数 root_call() 中然后调用它。

Python 函数的另一大特性,是 Python 支持函数的嵌套。所谓的函数嵌套,就是指函数里面又有函数,比如:

def f1():
    print('hello')
    def f2():
        print('world')
    f2()
f1()

# 输出
hello
world

其实,函数的嵌套,主要有下面两个方面的作用。

第一,函数的嵌套能够保证内部函数的隐私。内部函数只能被外部函数所调用和访问,不会暴露在全局作用域,因此,如果你的函数内部有一些隐私数据(比如数据库的用户、密码等),不想暴露在外,那你就可以使用函数的的嵌套,将其封装在内部函数中,只通过外部函数来访问。比如:

def connect_DB():
    def get_DB_configuration():
        ...
        return host, username, password
    conn = connector.connect(get_DB_configuration())
    return conn

这里的函数 get_DB_configuration,便是内部函数,它无法在 connect_DB() 函数以外被单独调用。也就是说,下面这样的外部直接调用是错误的:

get_DB_configuration()

# 输出
NameError: name 'get_DB_configuration' is not defined

我们只能通过调用外部函数 connect_DB() 来访问它,这样一来,程序的安全性便有了很大的提高。

第二,合理的使用函数嵌套,能够提高程序的运行效率。我们来看下面这个例子:

def factorial(input):
    # validation check
    if not isinstance(input, int):
        raise Exception('input must be an integer.')
    if input < 0:
        raise Exception('input must be greater or equal to 0' )
    ...

    def inner_factorial(input):
        if input <= 1:
            return 1
        return input * inner_factorial(input-1)
    return inner_factorial(input)


print(factorial(5))

这里,我们使用递归的方式计算一个数的阶乘。因为在计算之前,需要检查输入是否合法,所以我写成了函数嵌套的形式,这样一来,输入是否合法就只用检查一次。而如果我们不使用函数嵌套,那么每调用一次递归便会检查一次,这是没有必要的,也会降低程序的运行效率。

实际工作中,如果你遇到相似的情况,输入检查不是很快,还会耗费一定的资源,那么运用函数的嵌套就十分必要了。

1.3 第三点

我们可以在函数里定义函数,也就是函数的嵌套。这里我同样举了一个例子:

def func(message):
    def get_message(message):
        print('Got a message: {}'.format(message))
    return get_message(message) # 返回一个值

func('hello aiyuechuang')

# 输出
Got a message: hello aiyuechuang

这段代码中,我们在函数 func() 里又定义了新的函数 get_message(),调用后作为 func() 的返回值返回。

1.4 第四点

要知道,函数的返回值也可以是函数对象(闭包),比如下面这个例子:

def func_closure():
    def get_message(message):
        print('Got a message: {}'.format(message))
    return get_message

send_message = func_closure()
send_message('hello aiyuechuang')

# 输出
Got a message: hello aiyuechuang

这里,函数 func_closure() 的返回值是函数对象 get_message()本身,之后,我们将其赋予变量 send_message,再调用 send_message(‘hello aiyuechuang’),最后输出了'Got a message: hello aiyuechuang'

1.5 闭包

这节课的第二个重点,我想再来介绍一下闭包(closure)。闭包其实和刚刚讲的嵌套函数类似,不同的是,这里外部函数返回的是一个函数,而不是一个具体的值。返回的函数通常赋于一个变量,这个变量可以在后面被继续执行调用。

举个例子你就更容易理解了。

比如,我们想计算一个数的 n 次幂,用闭包可以写成下面的代码:

def nth_power(exponent):
    def exponent_of(base):
        return base ** exponent
    return exponent_of # 返回值是exponent_of函数

square = nth_power(2) # 计算一个数的平方
cube = nth_power(3) # 计算一个数的立方 
print(square)
# 输出
<function __main__.nth_power.<locals>.exponent(base)>

print(cube)
# 输出
<function __main__.nth_power.<locals>.exponent(base)>

print(square(2))  # 计算2的平方
print(cube(2)) # 计算2的立方
# 输出
4 # 2^2
8 # 2^3

这里外部函数 nth_power() 返回值,是函数 exponent_of(),而不是一个具体的数值

需要注意的是,在执行完 square = nth_power(2)cube = nth_power(3) 后,外部函数 nth_power()的参数 exponent,仍然会被内部函数 exponent_of() 记住。

这样,之后我们调用 square(2)或者 cube(2) 时,程序就能顺利地输出结果,而不会报错说参数 exponent没有定义了。

看到这里,你也许会思考,为什么要闭包呢?上面的程序,我也可以写成下面的形式啊!

def nth_power_rewrite(base, exponent):
    return base ** exponent

确实可以,不过,要知道,使用闭包的一个原因,是让程序变得更简洁易读。

设想一下,比如你需要计算很多个数的平方,那么你觉得写成下面哪一种形式更好呢?

# 不使用闭包
res1 = nth_power_rewrite(base1, 2)
res2 = nth_power_rewrite(base2, 2)
res3 = nth_power_rewrite(base3, 2)
...

# 使用闭包
square = nth_power(2)
res1 = square(base1)
res2 = square(base2)
res3 = square(base3)
...

显然是第二种,是不是?

首先直观来看,第二种形式,让你每次调用函数都可以少输入一个参数,表达更为简洁。

其次,和上面讲到的嵌套函数优点类似,函数开头需要做一些额外工作,而你又需要多次调用这个函数时,将那些额外工作的代码放在外部函数,就可以减少多次调用导致的不必要的开销,提高程序的运行效率。

另外还有一点,我们后面会讲到,闭包常常和装饰器(decorator)一起使用。

2. 装饰器爱之初体验

现在什么都不用懂,什么都不用想,看一个例子 (看我怎么把它和装饰器扯上关系的)。

斯蒂文是个厨师,有一天开始研究汉堡 (burger) 的做法,第一次他只用鸡肉饼做汉堡。

def meat(food='--鸡肉饼--'):
    print(food)

# meat()
burger = meat # 给这个函数取个小名
burger()

# 输出
--鸡肉饼--

很明显汉堡都是肉,太荤了。加点蔬菜 (vegetable) 如何?

def vegetable(func):
    def wrapper():
        print(' #西红柿#')
        func()
        print(' ~沙拉菜~')
    return wrapper

burger = vegetable(meat)
# print(type(burger))
burger()

# 输出
 #西红柿#
--鸡肉饼--
 ~沙拉菜~

有些同学可能会想,为什么不能像下面这么写呢?

def meat(food='--鸡肉饼--'):
    print(food)

def vegetable(func):
    print(' #西红柿#')
    func()
    print(' ~沙拉菜~')

burger = vegetable(meat)

我们来看看输出就知道了:

 #西红柿#
--鸡肉饼--
 ~沙拉菜~

直接就是一个结果,并不是一个函数。

现在汉堡看起来不错,可是好像看缺少了什么?对,再加点面包就齐活了。

def bread(func):
    def wrapper():
        print('</------\>')
        func()
        print('<\------/>')
    return wrapper

burger = bread(vegetable(meat))
burger()

# 输出
</------\>
 #西红柿#
--鸡肉饼--
 ~沙拉菜~
<\------/>

现在看上去真像个汉堡,面包夹着蔬菜,蔬菜夹着肉。

完整代码:

"""
project = 'Code', file_name = 'yibudaima', author = 'AI悦创'
time = '2020/4/22 19:24', product_name = PyCharm, 公众号:AI悦创
# code is far away from bugs with the god animal protecting
    I love animals. They taste delicious.
"""
def meat(food='--鸡肉饼--'):
    print(food)

def vegetable(func):
    def wrapper():
        print(' #西红柿#')
        func()
        print(' ~沙拉菜~')
    return wrapper


def bread(func):
    def wrapper():
        print('</------\>')
        func()
        print('<\------/>')

    return wrapper


burger = bread(vegetable(meat))
burger()

# 输出结果
</------\>
 #西红柿#
--鸡肉饼--
 ~沙拉菜~
<\------/>

要点:面包和蔬菜「装饰」着鸡肉饼,bread()vegatable() 这两个函数起着「装饰器」的作用,它们没有改变 meat() 函数,只在它的基础上添砖加瓦,最后把鸡肉饼装饰成汉堡。

3. 简单的装饰器

下面是装饰器的正规语法,用 @func 语法 (注意@符),将@bread@vegatable 放在要装饰的函数上面。

@bread
@vegetable
def meat(food='--鸡肉饼--'):
    print(food)

再调用被装饰后的 meat() 函数并赋值给 burger,就做出汉堡了。

burger = meat
burger()

运行结果如下:

</------\>
 #西红柿#
--鸡肉饼--
 ~沙拉菜~
<\------/>

装饰器是有序的,如下例所示,如果互换 bread()vegatable() 这两函数的位置,那么这汉堡最外层是蔬菜,中间是面包,里面是鸡肉饼,不像汉堡了。

@vegetable
@bread
def meat(food='--鸡肉饼--'):
    print(food)
burger = meat
burger()
#西红柿#
</------\>
--鸡肉饼--
<\------/>
 ~沙拉菜~

要点:一个函数可以被多个装饰器装饰,装饰器的顺序很重要。

在这里插入图片描述

对装饰器有点感觉了么?有就往下看把。

简单的复习和简单的介绍之后,我们接下来学习今天的新知识——装饰器。按照习惯,我们可以先来看一个装饰器的简单例子:

def my_decorator(func):
    def wrapper():
        print('wrapper of decorator')
        func()
    return wrapper

def greet():
    print('hello world')

greet = my_decorator(greet)
greet()

# 输出
wrapper of decorator
hello world

这段代码中:

  1. 变量 greet指向了内部函数 wrapper()
  2. 而内部函数 wrapper()中又会调用原函数 greet()
  3. 因此,最后调用 greet() 时,就会先打印'wrapper of decorator',然后输出'hello world'

这里的函数 my_decorator() 就是一个装饰器,它把真正需要执行的函数 greet()包裹在其中,并且改变了它的行为,但是原函数 greet() 不变。(原本的 greet 只有一个功能——输出 hello world 而使用装饰器之后,功能增多。)

事实上,上述代码在 Python 中有更简单、更优雅的表示:

def my_decorator(func):
    def wrapper():
        print('wrapper of decorator')
        func()
    return wrapper

@my_decorator
def greet():
    print('hello world')

greet()

这里的@,我们称之为语法糖,@my_decorator 就相当于前面的 greet=my_decorator(greet) 语句,只不过更加简洁。

因此,如果你的程序中有其它函数需要做类似的装饰,你只需在它们的上方加上 @decorator 就可以了,这样就大大提高了函数的重复利用和程序的可读性。

4. 带有参数的装饰器

你或许会想到,如果原函数 greet() 中,有参数需要传递给装饰器怎么办?

一个简单的办法,是可以在对应的装饰器函数 wrapper() 上,加上相应的参数,比如:

# 示例一
def my_decorator(func):
    def wrapper(message):
        print('wrapper of decorator')
        func(message)
    return wrapper


@my_decorator
def greet(message):
    print(message)


greet('hello world')

# 输出
wrapper of decorator
hello world

# 示例二
def my_decorator(func):
    def wrapper(message):
        print('wrapper of decorator')
        func(message)
    return wrapper

@my_decorator
def greet(message):
    print('hello world' + message)

# greet1 = greet
# greet1('AI悦创')
greet("AI悦创")

# 输出
wrapper of decorator
hello worldAI悦创

不过,新的问题来了。如果我另外还有一个函数,也需要使用 my_decorator() 装饰器,但是这个新的函数有两个参数,又该怎么办呢?比如:

@my_decorator
def celebrate(name, message):
    ...

上面的代码,你有可能会这么写来以次达到传如多个参数的目的:

def my_decorator(func):
    def wrapper(message, message2):
        print('wrapper of decorator')
        func(message, message2)
    return wrapper

@my_decorator
def greet(message, message2):
    print('hello world' + message + message2)

greet1 = greet
greet1('AI悦创', '公众号:AI悦创')

这样明显是比较麻烦的。

事实上,通常情况下,我们会把 *args**kwargs,作为装饰器内部函数 wrapper() 的参数。*args**kwargs,表示接受任意数量和类型的参数,因此装饰器就可以写成下面的形式:

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print('wrapper of decorator')
        func(*args, **kwargs)
    return wrapper
def my_decorator(func):
    def wrapper(*args):
        print('wrapper of decorator')
        func(*args)
    return wrapper


@my_decorator
def greet(*args):
    print(args)
    print(*args)


greet('hello world', 'AI悦创')

4.1 *args 的用法

*args**kwargs 主要用于函数定义。 你可以将不定数量的参数传递给一个函数。

这里的不定的意思是:预先并不知道, 函数使用者会传递多少个参数给你,所以在这个场景下使用这两个关键字。 *args 是用来发送一个非键值对的可变数量的参数列表给一个函数。

这里有个例子帮你理解这个概念:

def test_var_args(*args):
    print(args)
    print(type(args))
    # print("first normal arg:", f_arg)
    # for arg in args:
        # print("another arg through *args:", arg)

test_var_args('yasoob', 'python', 'eggs', 'test')

# 输出
('yasoob', 'python', 'eggs', 'test')
<class 'tuple'>

我们可以发现,拿到的数据的数据类型是元组(tuple)

def test_var_args(f_arg, *args):
    print("first normal arg:", f_arg)
    for arg in args:
        print("another arg through *argv:", arg)

test_var_args('yasoob', 'python', 'eggs', 'test')

# 输出
def test_var_args(f_arg, *args):
    print("first normal arg:", f_arg)
    for arg in args:
        print("another arg through *args:", arg)

test_var_args('yasoob', 'python', 'eggs', 'test')

*args:可以自行换成别的名称,不过我们约定俗成写成:args

我希望这解决了你所有的困惑. 那接下来让我们谈谈 **kwargs

4.2 **kwargs 的用法

**kwargs 允许你将不定长度的键值对, 作为参数传递给一个函数。 如果你想要在一个函数里处理带名字的参数, 你应该使用**kwargs

这里有个让你上手的例子:

def greet_me(**kwargs):
    print(kwargs)
    print(type(kwargs))
    for key, value in kwargs.items():
        print("{0} == {1}".format(key, value))
        # print("{1} == {0}".format(key, value)) # 第二个主要用来让同学们了解 format

greet_me(name="yasoob", gzh='aiyuechuang', the_public='AI悦创')

# 输出
{'name': 'yasoob', 'gzh': 'aiyuechuang', 'the_public': 'AI悦创'}
<class 'dict'>
name == yasoob
gzh == aiyuechuang
the_public == AI悦创

现在你可以看出我们怎样在一个函数里, 处理了一个键值对参数了。

这就是 **kwargs 的基础, 而且你可以看出它有多么管用。 接下来让我们谈谈,你怎样使用 *args**kwargs 来调用一个参数为列表或者字典的函数。

4.3 使用 *args**kwargs 来调用函数

那现在我们将看到怎样使用*args**kwargs 来调用一个函数。 假设,你有这样一个小函数:

def test_args_kwargs(arg1, arg2, arg3):
    print("arg1:", arg1)
    print("arg2:", arg2)
    print("arg3:", arg3)

可以联想如果参数很多的话,这样就代码就不是非常的简洁。你可以使用 *args**kwargs 来给这个小函数传递参数。 下面是怎样做:

# 首先使用 *args
>>> args = ("two", 3, 5)
>>> test_args_kwargs(*args)
arg1: two
arg2: 3
arg3: 5

# 现在使用 **kwargs:
>>> kwargs = {"arg3": 3, "arg2": "two", "arg1": 5}
>>> test_args_kwargs(**kwargs)
arg1: 5
arg2: two
arg3: 3

4.4 标准参数与 *args、**kwargs 在使用时的顺序

那么如果你想在函数里同时使用所有这三种参数, 顺序是这样的:

some_func(fargs, *args, **kwargs)

4.5 什么时候使用它们?

这还真的要看你的需求而定。

最常见的用例是在写函数装饰器的时候。

此外它也可以用来做猴子补丁(monkey patching)。猴子补丁的意思是在程序运行时(runtime)修改某些代码。 打个比方,你有一个类,里面有个叫 get_info 的函数会调用一个 API 并返回相应的数据。如果我们想测试它,可以把 API 调用替换成一些测试数据。例如:

import someclass

def get_info(self, *args):
    return "Test data"

someclass.get_info = get_info

我敢肯定你也可以想象到一些其他的用例。

5. 带有自定义参数的装饰器

其实,装饰器还有更大程度的灵活性。刚刚说了,装饰器可以接受原函数任意类型和数量的参数,除此之外,它还可以接受自己定义的参数。

举个例子,比如我想要定义一个参数,来表示装饰器内部函数被执行的次数,那么就可以写成下面这种形式:

def repeat(num):
    def my_decorator(func):
        def wrapper(*args, **kwargs):
            for i in range(num):
                print('wrapper of decorator')
                func(*args, **kwargs)
        return wrapper
    return my_decorator


@repeat(4)
def greet(message):
    print(message)

greet('hello world')

# 输出:
wrapper of decorator
hello world
wrapper of decorator
hello world
wrapper of decorator
hello world
wrapper of decorator
hello world

6. 原函数还是原函数吗?

现在,我们再来看个有趣的现象。还是之前的例子,我们试着打印出 greet() 函数的一些元信息:

greet.__name__
## 输出
'wrapper'

help(greet)
# 输出
Help on function wrapper in module __main__:

wrapper(*args, **kwargs)

你会发现,greet() 函数被装饰以后,它的元信息变了。元信息告诉我们“它不再是以前的那个 greet() 函数,而是被 wrapper() 函数取代了”。

为了解决这个问题,我们通常使用内置的装饰器@functools.wrap,它会帮助保留原函数的元信息(也就是将原函数的元信息,拷贝到对应的装饰器函数里)。

import functools

def my_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print('wrapper of decorator')
        func(*args, **kwargs)
    return wrapper

@my_decorator
def greet(message):
    print(message)

greet.__name__

# 输出
'greet'

7. 类装饰器

前面我们主要讲了函数作为装饰器的用法,实际上,类也可以作为装饰器。类装饰器主要依赖于函数__call__(),每当你调用一个类的示例时,函数__call__() 就会被执行一次。

我们来看下面这段代码:

class Count:
    def __init__(self, func):
        self.func = func
        self.num_calls = 0

    def __call__(self, *args, **kwargs):
        self.num_calls += 1
        print('num of calls is: {}'.format(self.num_calls))
        return self.func(*args, **kwargs)

@Count
def example():
    print("hello world")

example()

# 输出
num of calls is: 1
hello world

example()

# 输出
num of calls is: 2
hello world

...

这里,我们定义了类 Count,初始化时传入原函数 func(),而__call__() 函数表示让变量 num_calls 自增 1,然后打印,并且调用原函数。因此,在我们第一次调用函数 example() 时,num_calls 的值是 1,而在第二次调用时,它的值变成了 2

8. 装饰器的嵌套

回顾刚刚讲的例子,基本都是一个装饰器的情况,但实际上,Python 也支持多个装饰器,比如写成下面这样的形式:

@decorator1
@decorator2
@decorator3
def func():
    ...

它的执行顺序从里到外,所以上面的语句也等效于下面这行代码:

decorator1(decorator2(decorator3(func)))

这样,'hello world' 这个例子,就可以改写成下面这样:

import functools

def my_decorator1(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print('execute decorator1')
        func(*args, **kwargs)
    return wrapper


def my_decorator2(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print('execute decorator2')
        func(*args, **kwargs)
    return wrapper


@my_decorator1
@my_decorator2
def greet(message):
    print(message)


greet('hello world')

# 输出
execute decorator1
execute decorator2
hello world

9. 装饰器用法实例

到此,装饰器的基本概念及用法我就讲完了,接下来,我将结合实际工作中的几个例子,带你加深对它的理解。

9.1 身份认证

首先是最常见的身份认证的应用。这个很容易理解,举个最常见的例子,你登录微信,需要输入用户名密码,然后点击确认,这样,服务器端便会查询你的用户名是否存在、是否和密码匹配等等。如果认证通过,你就可以顺利登录;如果不通过,就抛出异常并提示你登录失败。

再比如一些网站,你不登录也可以浏览内容,但如果你想要发布文章或留言,在点击发布时,服务器端便会查询你是否登录。如果没有登录,就不允许这项操作等等。

我们来看一个大概的代码示例:

import functools

def authenticate(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        request = args[0]
        if check_user_logged_in(request): # 如果用户处于登录状态
            return func(*args, **kwargs) # 执行函数post_comment() 
        else:
            raise Exception('Authentication failed')
    return wrapper

@authenticate
def post_comment(request, ...)
    ...

这段代码中,我们定义了装饰器 authenticate;而函数 post_comment(),则表示发表用户对某篇文章的评论。每次调用这个函数前,都会先检查用户是否处于登录状态,如果是登录状态,则允许这项操作;如果没有登录,则不允许。

9.2 日志记录

日志记录同样是很常见的一个案例。在实际工作中,如果你怀疑某些函数的耗时过长,导致整个系统的 latency(延迟)增加,所以想在线上测试某些函数的执行时间,那么,装饰器就是一种很常用的手段。

我们通常用下面的方法来表示:

import time
import functools

def log_execution_time(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        res = func(*args, **kwargs)
        end = time.perf_counter()
        print('{} took {} ms'.format(func.__name__, (end - start) * 1000))
        return res
    return wrapper

@log_execution_time
def calculate_similarity(items):
    ...

这里,装饰器 log_execution_time记录某个函数的运行时间,并返回其执行结果。如果你想计算任何函数的执行时间,在这个函数上方加上@log_execution_time 即可。

9.3 输入合理性检查

再来看今天要讲的第三个应用,输入合理性检查。

在大型公司的机器学习框架中,我们调用机器集群进行模型训练前,往往会用装饰器对其输入(往往是很长的 JSON 文件)进行合理性检查。这样就可以大大避免,输入不正确对机器造成的巨大开销。

它的写法往往是下面的格式:

import functools

def validation_check(input):
    @functools.wraps(func)
    def wrapper(*args, **kwargs): 
        ... # 检查输入是否合法

@validation_check
def neural_network_training(param1, param2, ...):
    ...

其实在工作中,很多情况下都会出现输入不合理的现象。因为我们调用的训练模型往往很复杂,输入的文件有成千上万行,很多时候确实也很难发现。

试想一下,如果没有输入的合理性检查,很容易出现“模型训练了好几个小时后,系统却报错说输入的一个参数不对,成果付之一炬”的现象。这样的“惨案”,大大减缓了开发效率,也对机器资源造成了巨大浪费。

9.4 缓存

最后,我们来看缓存方面的应用。关于缓存装饰器的用法,其实十分常见,这里我以 Python 内置的 LRU cache 为例来说明(如果你不了解 LRU cache,可以点击链接自行查阅)。

LRU cache,在 Python 中的表示形式是 @lru_cache@lru_cache 会缓存进程中的函数参数和结果,当缓存满了以后,会删除 least recenly used 的数据。

正确使用缓存装饰器,往往能极大地提高程序运行效率。为什么呢?我举一个常见的例子来说明。

大型公司服务器端的代码中往往存在很多关于设备的检查,比如你使用的设备是安卓还是 iPhone,版本号是多少。这其中的一个原因,就是一些新的 feature,往往只在某些特定的手机系统或版本上才有(比如 Android v200+)。

这样一来,我们通常使用缓存装饰器,来包裹这些检查函数,避免其被反复调用,进而提高程序运行效率,比如写成下面这样:

@lru_cache
def check(param1, param2, ...) # 检查用户设备类型,版本号等等
    ...

10. 总结

这节课,我们一起学习了装饰器的概念及用法。所谓的装饰器,其实就是通过装饰器函数,来修改原函数的一些功能,使得原函数不需要修改。

Decorators is to modify the behavior of the function through a wrapper so we don’t have to actually modify the function.

而实际工作中,装饰器通常运用在身份认证、日志记录、输入合理性检查以及缓存等多个领域中。合理使用装饰器,往往能极大地提高程序的可读性以及运行效率。

11. 思考题

那么,你平时工作中,通常会在哪些情况下使用装饰器呢?欢迎留言和我讨论,也欢迎你把这篇文章分享给你的同事、朋友,一起在交流中进步。

AI悦创·创造不同!
AI悦创 » Python 系列特别篇 - 装饰器

Leave a Reply