Python函数装饰器

Python 函数装饰器

装饰器(Decorators)是 Python 的一个重要部分。简单地说:他们是修改其他函数的功能的函数。他们有助于让我们的代码更简短,也更Pythonic(Python范儿)。

一切皆为对象

对于python中的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def hi(name="you"):
return "hi " + name

print(hi())
# output: 'hi you'

# 我们甚至可以将一个函数赋值给一个变量,比如
greet = hi
# 我们这里没有在使用小括号,因为我们并不是在调用hi函数
# 而是在将它放在greet变量里头。我们尝试运行下这个

print(greet())
# output: 'hi you'

# 如果我们删掉旧的hi函数,看看会发生什么!
del hi
print(hi())
#outputs: NameError

print(greet())
#outputs: 'hi you'

在函数中定义函数

函数中的函数属于私有函数,外部无法直接调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

def hi(name="yasoob"):
print("now you are inside the hi() function")

def greet():
return "now you are in the greet() function"

def welcome():
return "now you are in the welcome() function"

print(greet())
print(welcome())
print("now you are back in the hi() function")

hi()
#output:now you are inside the hi() function
# now you are in the greet() function
# now you are in the welcome() function
# now you are back in the hi() function

# 上面展示了无论何时你调用hi(), greet()和welcome()将会同时被调用。
# 然后greet()和welcome()函数在hi()函数之外是不能访问的,比如:

greet()
#outputs: NameError: name 'greet' is not defined

从函数中返回函数

当将函数名本身作为返回值时 a=f,则是将函数地址返回,并不是调用函数,而如果将函数赋值给变量a=f(),则是将f()的返回值赋值给a的过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

def hi(name="yasoob"):
def greet():
return "now you are in the greet() function"

def welcome():
return "now you are in the welcome() function"

if name == "yasoob":
return greet
else:
return welcome

a = hi()
print(a)
#outputs: <function greet at 0x7f2143c01500>

#上面清晰地展示了`a`现在指向到hi()函数中的greet()函数
#现在试试这个

print(a())
#outputs: now you are in the greet() function
b=hi()()
print(b)
#outputs: now you are in the greet() function

在上述代码中,使用 判断语句返回了 greetwelcome ,而不是greet()welcome() ,那么上面的a()b的结果就是一样的。

将函数作为参数传给另一个函数

1
2
3
4
5
6
7
8
9
10
11

def hi():
return "hi yasoob!"

def doSomethingBeforeHi(func):
print("I am doing some boring work before executing hi()")
print(func())

doSomethingBeforeHi(hi)
#outputs:I am doing some boring work before executing hi()
# hi yasoob!

当函数为变量名时,指向地址 可将函数本身作为参数传向函数调用。

那么装饰器的用法就出现了,可以将函数本身插入某段代码之中。(类似jvav中spring框架的依赖注解)

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def a_new_decorator(a_func):#定义装饰器
def wrapTheFunction():# 定义一个包装函数
print("add something before")

a_func()

print("add somthing after")

return wrapTheFunction #返回包装函数

def func():
print("我是执行函数")

a=a_new_decorator(func) #将返回的函数赋值给a
a()#调用函数
#outputs: add something before
# 我是执行函数
# add somthing after

我们用装饰器封装了一个函数,那么我们可以用@来进行注解注入

1
2
3
4
5
6
7
8
@a_new_decorator 
def func1():
print("我是执行函数1")

func1()
#outputs: add something before
# 我是执行函数1
# add somthing after

@a_new_decorator相当于a=a_new_decorator(func1)那么注解@就相当于将当前函数注入装饰器中,那么调用当前这个函数,就是将当前函数装饰后返回结果。

但是此时要注意

1
2
print(func1.__name__)
# Output: wrapTheFunction

对于正常函数来说,应该返回函数本身的名称即func1

这里的函数被warpTheFunction替代了。它重写了我们函数的名字和注释文档(docstring)。幸运的是Python提供给我们一个简单的函数来解决这个问题,那就是functools.wraps。我们修改上一个例子来使用functools.wraps:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from functools import wraps

def a_new_decorator(a_func):#定义装饰器

@wraps(a_func)#解决函数名装饰时被修改的问题
def wrapTheFunction():# 定义一个包装函数
print("add something before")

a_func()

print("add somthing after")

return wrapTheFunction #返回包装函数

@a_new_decorator #a=a_new_decorator(func) 那么注解@就相当于将当前函数注入装饰器中,那么调用这个函数,就是带装饰器打包
def func1():
print("我是执行函数1")

func1()
print(func1.__name__)
# Output: func1

蓝本规范:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from functools import wraps
def decorator_name(f):
@wraps(f)
def decorated(*args, **kwargs):
if not can_run:
return "Function will not run"
return f(*args, **kwargs)
return decorated

@decorator_name
def func():
return("Function is running")

can_run = True
print(func())
# Output: Function is running

can_run = False
print(func())
# Output: Function will not run

注意:@wraps接受一个函数来进行装饰,并加入了复制函数名称、注释文档、参数列表等等的功能。这可以让我们在装饰器里面访问在装饰之前的函数的属性。

关于*args,**kwargs的小知识

答:可变参数

  • 如果我们不确定要往函数中传入多少个参数,或者我们想往函数中以列表和元组的形式传参数时,那就使要用*args输出结果以元组的形式展示
  • 如果我们不知道要往函数中传入多少个关键词参数,或者想传入字典的值作为关键词参数时,那就要使用**kwargs,它将打包关键字参数成dict给函数体调用, 输出结果以列表形式展示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
L=(1,2,[1,2,3])
def test(*args):
print(type(args),args)
test(L)
test(1,2)
test(1)
#outputs:
# <class 'tuple'> ((1, 2, [1, 2, 3]),)
# <class 'tuple'> (1, 2)
# <class 'tuple'> (1,)

def test1(**kwargs):
print(type(kwargs),kwargs)

info={'id':1,'age':15,'name':"老王"}

test1()
test1(id=1,age=15,name="老王")
test1(id=info['id'],name=info['name'],age=info['age'])
#outputs:
# <class 'dict'> {}
# <class 'dict'> {'id': 1, 'age': 15, 'name': '老王'}
# <class 'dict'> {'id': 1, 'name': '老王', 'age': 15}

使用场景

授权(Authorization)

装饰器能有助于检查某个人是否被授权去使用一个web应用的端点(endpoint)。它们被大量使用于Flask和Django web框架中。这里是一个例子来使用基于装饰器的授权:

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

def requires_auth(f):
@wraps(f)
def decorated(*args, **kwargs):
auth = request.authorization
if not auth or not check_auth(auth.username, auth.password):
authenticate()
return f(*args, **kwargs)
return decorated

日志(Logging)

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

from functools import wraps

def logit(func):
@wraps(func)
def with_logging(*args, **kwargs):
print(func.__name__ + " was called")
return func(*args, **kwargs)#返回被装饰函数的返回值 即x+x
return with_logging

@logit
def addition_func(x):
"""Do some math."""
return x + x


result = addition_func(4)
# Output: addition_func was called
result
# Output: 8

带参数的装饰器

在函数中嵌入装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

from functools import wraps

def logit(logfile='out.log'):
def logging_decorator(func):
@wraps(func)
def wrapped_function(*args, **kwargs):
log_string = func.__name__ + " was called"
print(log_string)
# 打开logfile,并写入内容
with open(logfile, 'a') as opened_file:
# 现在将日志打到指定的logfile
opened_file.write(log_string + '\n')
return func(*args, **kwargs)
return wrapped_function
return logging_decorator

@logit()
def myfunc1():
pass

myfunc1()
# Output: myfunc1 was called
# 现在一个叫做 out.log 的文件出现了,里面的内容就是上面的字符串

@logit(logfile='func2.log')#通过外包装函数可以
def myfunc2():
pass

myfunc2()
# Output: myfunc2 was called
# 现在一个叫做 func2.log 的文件出现了,里面的内容就是上面的字符串

装饰器类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
from functools import wraps

class logit(object):
def __init__(self, logfile='out.log'):
self.logfile = logfile

def __call__(self, func):
@wraps(func)
def wrapped_function(*args, **kwargs):
log_string = func.__name__ + " was called"
print(log_string)
# 打开logfile并写入
with open(self.logfile, 'a') as opened_file:
# 现在将日志打到指定的文件
opened_file.write(log_string + '\n')
# 现在,发送一个通知
self.notify()
return func(*args, **kwargs)
return wrapped_function

def notify(self):
# logit只打日志,不做别的
pass


@logit()
def myfunc1():
pass


class email_logit(logit):
'''
一个logit的实现版本,可以在函数调用时发送email给管理员
'''
def __init__(self, email='admin@myproject.com', *args, **kwargs):
self.email = email
super(email_logit, self).__init__(*args, **kwargs)

def notify(self):
# 发送一封email到self.email
# 这里就不做实现了
pass

从现在起,@email_logit 将会和 @logit 产生同样的效果,但是在打日志的基础上,还会多发送一封邮件给管理员。

被面试官问到的悲惨经历,就突然想不起来了~~