0
点赞
收藏
分享

微信扫一扫

『Python学习笔记』如何理解Python装饰器Decorator


如何理解Python装饰器Decorator?

文章目录

  • ​​一. 复习回顾​​
  • ​​1.1. 4点回顾​​
  • ​​1.2. 闭包函数​​
  • ​​二. 装饰器介绍​​
  • ​​2.1. 简单例子​​
  • ​​2.2. 带有参数的装饰器​​
  • ​​2.3 带有自定义参数的装饰器​​
  • ​​2.4. 原函数还是原函数吗?​​
  • ​​2.5. 类装饰器​​
  • ​​2.6. 装饰器的嵌套​​
  • ​​三. 装饰器用法实例​​
  • ​​四. 参考文献​​

一. 复习回顾

1.1. 4点回顾

  • 第一点:在 Python 中,函数是一等公民,函数也是对象。我们可以把函数赋予变量,比如下面这段代码:

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

send_message = func
send_message('hello world')

# 输出
Got a message: hello world

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

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


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

root_call(get_message, 'hello world')

# 输出
Got a message: hello world

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

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

func('hello world')

# 输出
Got a message: hello world

  • 第四点函数的返回值也可以是函数对象(闭包),比如下面这个例子:

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

send_message = func_closure()
send_message('hello world')

# 输出
Got a message: hello world

1.2. 闭包函数

  • 闭包函数: 闭包是Python编程一个非常重要的概念。如果一个函数中定义了一个内函数,且内函数体内引用到了体外的变量,这时外函数通过return返回内函数的引用时,会把定义时涉及到的外部引用变量和内函数打包成一个整体(闭包)返回。
  • 闭包的定义可能会有点儿晦涩难懂,我们通过下面的代码示例详细解释一下:

def outer(a):
def inner(b):
print(a+b)
return inner(2)

outer(2)

  • 上面代码中的inner就是内函数,内函数中定义了一个局部变量b,除此之外还引用到了其函数外部的a变量
  • a 变量本身不属于inner函数的变量作用域,但如果inner函数使用 a 变量时无法在自己的局部作用域中找到该变量时,会继续向上一层作用域中寻找,而inner函数的上一层作用域就是outer函数,a变量就是outer函数的参数,所以此时inner函数就找到了a变量。
  • 当调用outer()函数时,其实outer内部又调用了inner函数。
  • 但如果在outer函数里不调用inner函数,而是返回inner函数的引用,会有什么效果呢?代码如下:

def outer(a):
def inner(b):
print(a + b)
return inner

f = outer(2)
print(type(f))
print(f)
print(f.__name__)

# 输出结果为
<class 'function'>
<function outer.<locals>.inner at 0x7f9fd8093280>
inner

  • 如果想要调用inner函数,就需要如下写法:

def outer(a):
def inner(b):
print(a+b)
return inner

f = outer(2)
f(1)
# out(2)(1)
返回3

  • 光从变量的作用域中还无法体现闭包的精髓。
  • 正常情况下,一个函数运行结束的时候,临时变量会被销毁,所以上面代码中执行了f = outer(2),此时其实outer函数已经执行结束了,所以outer函数中的局部变量a理应被销毁
  • 但由于我们在outer函数中返回了inner函数的引用,而inner函数又使用了outer函数中的局部变量,所以执行完f = outer(2)后,还不能将被inner函数引用到的变量a销毁,而是会将outer函数的局部变量同inner函数绑定在一起,并将inner函数返回,以供后续调用
  • 所以outer函数中最后的return inner做了两件事情,一件是返回了inner函数的引用,另一件就是暂时保留了被inner函数引用到的外部变量
  • 综上所述,闭包就是即使外函数已经结束了,内函数仍然能够使用外函数的临时变量。

二. 装饰器介绍

  • 装饰器是从Decorator直译而来,它可以在不改变一个函数代码和调用方式的情况下给函数添加新的功能
  • 装饰器本质上是一个闭包函数,它接受被装饰的函数(func)作为参数,并返回一个包装过的函数。这样我们可以在不改变被装饰函数的代码的情况下给被装饰函数或程序添加新的功能。
  • Python的装饰器广泛应用于缓存、权限校验(如Flask中的@login_required和@permission_required装饰器)、性能测试(比如统计一段程序的运行时间)和插入日志等应用场景。有了装饰器,我们就可以抽离出大量与函数功能本身无关的代码,增加一个函数的重用性。

2.1. 简单例子

  • 接下来学习今天的新知识——装饰器。按照习惯,我们可以先来看一个装饰器的简单例子:
  • 这段代码中,变量 greet 指向了内部函数 wrapper(),而内部函数 wrapper() 中又会调用原函数 greet(),因此,最后调用 greet() 时,就会先打印’wrapper of decorator’,然后输出’hello world’。
  • 这里的函数 my_decorator() 就是一个装饰器,它把真正需要执行的函数 greet() 包裹在其中,并且改变了它的行为,但是原函数 greet() 不变。

def mydetorator(func):
def wrapper():
print("wrapper of detorator")
func()
return wrapper

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

greet = mydetorator(greet)
greet()

  • 事实上,上述代码在 Python 中有更简单、更优雅的表示:
  • 这里的@,我们称之为语法糖,@my_decorator就相当于前面的greet=my_decorator(greet)语句,只不过更加简洁。因此,如果你的程序中有其它函数需要做类似的装饰,你只需在它们的上方加上@decorator就可以了,这样就大大提高了函数的重复利用和程序的可读性。
  • 补充:Python中同样也有很多语法糖,比如:列表推导式,with上下文管理器,装饰器等。其中装饰器是一个十分重要的特性,并且在之前面向对象章节中类的讲解,以及Flask框架的讲解中,也都或多或少使用过装饰器。

def mydetorator(func):
def wrapper():
print("wrapper of detorator")
func()
return wrapper

@mydetorator
def greet():
print("hello world")

# greet = mydetorator(greet)
greet()

2.2. 带有参数的装饰器

  • 你或许会想到,如果原函数 greet() 中,有参数需要传递给装饰器怎么办?一个简单的办法,是可以在对应的装饰器函数 wrapper() 上,加上相应的参数,比如:

def mydetorator(func):
def wrapper(message):
print("wrapper of detorator")
func(message)
return wrapper

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

# greet=my_decorator(greet) 等价@mydetorator
greet("hello world")

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

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

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

def mydetorator(func):
def wrapper(*args, **kwargs):
print("wrapper of detorator")
func(*args, **kwargs)
return wrapper

@mydetorator
def celebrate(name, message):
print(name, message)

# celebrate = mydetorator(celebrate)等价于@mydetorator
celebrate("zhangkaifang", "hello")

2.3 带有自定义参数的装饰器

  • 其实,装饰器还有更大程度的灵活性。刚刚说了,装饰器可以接受原函数任意类型和数量的参数,除此之外,它还可以接受自己定义的参数。
  • 举个例子,比如我想要定义一个参数,来表示装饰器内部函数被执行的次数,那么就可以写成下面这种形式:

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

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

def mydetorator(func):
def wrapper(*args, **kwargs):
print("wrapper of detorator")
func(*args, **kwargs)
return wrapper

@mydetorator
def celebrate(name, message):
print(name, message)

# celebrate = mydetorator(celebrate)等价于@mydetorator
celebrate("zhangkaifang", "hello")

celebrate.__name__
help(celebrate)

# 输出如下信息:你会发现,greet() 函数被装饰以后,它的元信息变了。元信息告诉我们“它不再是以前的那个
# greet() 函数,而是被 wrapper() 函数取代了”。
wrapper of detorator
zhangkaifang hello
Help on function wrapper in module __main__:

wrapper(*args, **kwargs)

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

import functools

def mydetorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("wrapper of detorator")
func(*args, **kwargs)
return wrapper

@mydetorator
def celebrate(name, message):
print(name, message)

# celebrate = mydetorator(celebrate)等价于@mydetorator
celebrate("zhangkaifang", "hello")

celebrate.__name__
help(celebrate)

# 输出如下信息:
wrapper of detorator
zhangkaifang hello
Help on function celebrate in module __main__:

celebrate(name, message)

2.5. 类装饰器

  • 前面我们主要讲了函数作为装饰器的用法,实际上,类也可以作为装饰器。类装饰器主要依赖于函数 ​__call__()​​ ,每当你调用一个类的示例时,函数 ​__call__()
  • 来看下面这段代码:这里,我们定义了类 Count,初始化时传入原函数 func(),而 ​__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 fo call is: {}".format(self.num_calls))
return self.func(*args, **kwargs)

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

# example = Call(example)等价于@Count
example() # 调用__call__
# 输出
num fo call is: 1
hello, world

example() # 调用__call__
# 输出
num fo call is: 2
hello, world

2.6. 装饰器的嵌套

  • Python 也支持多个装饰器,比如写成下面这样的形式:

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

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

detorator1(detorator2(detorator3(func3)))

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

import functools

def mydetorator1(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("execute detorator1")
func(*args, **kwargs)
return wrapper

def mydetorator2(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("execute detorator2")
func(*args, **kwargs)
return wrapper

@mydetorator2
@mydetorator1
def greet(message):
print(message)

greet("hello world!")

# 输出
execute detorator2
execute detorator1
hello world!

三. 装饰器用法实例

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

import time

def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
func(*args, **kwargs)
print("{} cost {} seconds".format(func.__name__, time.time()-start))
return wrapper

@timer
def test():
time.sleep(1)

# test = timer(test)
test()

  • 输入合理性检查: 在大型公司的机器学习框架中,我们调用机器集群进行模型训练前,往往会用装饰器对其输入(往往是很长的 JSON 文件)进行合理性检查。这样就可以大大避免,输入不正确对机器造成的巨大开销。它的写法往往是下面的格式:其实在工作中,很多情况下都会出现输入不合理的现象。因为我们调用的训练模型往往很复杂,输入的文件有成千上万行,很多时候确实也很难发现。

import functools

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

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

  • 总结:所谓的装饰器,其实就是通过装饰器函数,来修改原函数的一些功能,使得原函数不需要修改

四. 参考文献

  • 如何理解Python装饰器?​​https://www.zhihu.com/question/26930016/answer/2331088908​​


举报

相关推荐

0 条评论