文章归档

置顶文章

Web安全

Web安全基础

PHP相关

Writeups

靶机系列

HackTheBox

VulnHub

代码审计

PHP代码审计

流量分析

机器学习

基础学习

Python

Python编程

Java

Java编程

算法

Leetcode

随笔

经验

技术

 2019-09-15   1.5k

Python函数式编程-装饰器

搬运自https://foofish.net/python-decorator.html

原文有若干处错误!

引入

装饰器本质上是一个 Python 函数或类,它可以让其他函数或类在不需要做任何代码修改的前提下增加额外功能,装饰器的返回值也是一个函数/类对象。

它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景,装饰器是解决这类问题的绝佳设计。有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码到装饰器中并继续重用。

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

接下来,我们一步一步的来看装饰器是怎么形成和起作用的。

先来看一个简单函数:

1
2
def foo():
print('I am foo')

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

1
2
3
def foo():
print('I am foo')
logging.info('foo is running')

假设现在其他函数也有类似的需求,如果再写一个logging在其他函数里面,这样就会造成大量重复代码。首先想到的解决办法肯定是重新定义一个函数专门来处理日志。

1
2
3
4
5
6
7
8
def use_logging(func):
logging.warn("%s is running" % func.__name__)
func()

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

use_logging(foo)

这样做逻辑上是没问题的,功能是实现了,但是我们调用的时候不再是调用真正的业务逻辑foo函数,而是换成了use_logging函数。那么有没有更好的方式的呢?当然有,答案就是装饰器。

简单装饰器

1
2
3
4
5
6
7
8
9
10
11
12
import logging
def use_logging(func):
def wrapper():
logging.warning("%s is running" % func.__name__)
return func() # 把foo当作参数传进来时,执行func()就相当于执行了foo()
return wrapper

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

foo = use_logging(foo) # 因为装饰器 use_logging(foo) 返回的时函数对象 wrapper,这条语句相当于 foo = wrapper
foo() # 执行foo()就相当于执行 wrapper()

use_logging 就是一个装饰器,它一个普通的函数,它把执行真正业务逻辑的函数 func 包裹在其中,看起来像 foo 被 use_logging 装饰了一样,use_logging 返回的也是一个函数,这个函数的名字叫 wrapper。

语法糖

@符号就是装饰器的语法糖,它放在函数开始定义的地方,这样就可以省略最后一步再次赋值的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
import logging
def use_logging(func):
def wrapper():
logging.warning("%s is running" % func.__name__)
return func()
return wrapper

@use_logging
def foo():
print('I am foo')

#foo = use_logging(foo)
foo()

如上所示,有了 @ ,我们就可以省去foo = use_logging(foo)这一句了,直接调用 foo() 即可得到想要的结果。

带参数的foo()

如果业务逻辑函数 foo 需要参数,例如:

1
2
def foo(name):
print('I am %s', name)

我们可以在定义 wrapper 函数的时候指定参数:

1
2
3
def wrapper(name):
logging.warning("%s is running" % func.__name__)
return func(name)

这样 foo 函数定义的参数就可以定义在 wrapper 函数中。如果func函数需要若干个参数的话,可以用*args代替:

1
2
3
def wrapper(*args):
logging.warning("%s is running" % func.__name__)
return func(*args)

如果 foo 函数还定义了一些关键字参数呢?比如:

1
2
def foo(name, age=None, height=None):
print('I am %s, age %s, height %s' % (name, age, height))

这时,你就可以把 wrapper 函数指定关键字函数:

1
2
3
4
def wrapper(*args, **kw):
# args是一个数组,kwargs一个字典
logging.warn("%s is running" % func.__name__)
return func(*args, **kw)

完整的函数定义和调用如下:

1
2
3
4
5
6
7
8
9
10
11
12
import logging
def use_logging(func):
def wrapper(*args, **kw):
logging.warn("%s is running" % func.__name__)
return func(*args, **kw)
return wrapper

@use_logging
def foo():
print('I am foo')

foo()

带参数的装饰器

装饰器的语法允许我们在调用时,提供其它参数,比如@decorator(a)。这样,就为装饰器的编写和使用提供了更大的灵活性。比如:我们可以在装饰器中指定日志的等级,因为不同业务函数可能需要的日志级别是不一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import logging

def use_logging(level):
def decorator(func):
def wrapper(*args, **kw):
if level == 'warn':
logging.warning("%s is running" % func.__name__)
elif level == 'error':
logging.error("%s is running" % func.__name__)
return func(*args, **kw)
return wrapper
return decorator

@use_logging(level='error')
def foo(name, age):
print('I am %s, %s age' % (name, age))

foo('foo', 19)

上面的use_logging是允许带参数的装饰器。它实际上是对原有装饰器的一个函数封装,并返回一个装饰器。我们可以将它理解为一个含有参数的闭包。

具体而言,和两层嵌套的decorator相比,3层嵌套的效果是这样的:

1
2
>>> foo = use_logging('error')(foo)

首先执行use_logging('error')返回的是decorator,再调用返回的函数,参数是foo,即decorator(foo),返回的最终是wrapper函数。

@use_logging(level="warn")等价于@decorator

functools.wraps

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def log(text):
def decorator(func):
def wrapper(*args, **kw):
print('%s %s():' % (text, func.__name__))
return func(*args, **kw)
return wrapper
return decorator

@log('execute')
def now():
print('2015-3-25')

now()
print(now.__name__)

输出结果如下:

1
2
3
4
execute now():
2015-3-25
'wrapper'

因为返回的那个wrapper()函数名字就是'wrapper',所以,需要把原始函数的__name__等属性复制到wrapper()函数中,否则,有些依赖函数签名的代码执行就会出错。

可以直接使用Python内置的functools.wraps函数解决上述问题。一个完整的decorator如下:

1
2
3
4
5
6
7
8
9
import functools

def log(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print('call %s():' % func.__name__)
return func(*args, **kw)
return wrapper

针对带参数的装饰器:

1
2
3
4
5
6
7
8
9
10
11
import functools

def log(text):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print('%s %s():' % (text, func.__name__))
return func(*args, **kw)
return wrapper
return decorator

Copyright © ca01h 2019-2020 | 本站总访问量