Fork me on GitHub

Python系列文章-Python装饰器总结

目录

  • 背景

  • 第一部分 准备知识

  • 第二部分 Python中的装饰器

  • 第三部分 最佳实践

  • 第四部分 高阶使用

  • 第五部分 Python内置装饰器

  • 参考文献及资料

背景

第一部分 准备知识

1.1 Python中一切皆对象

Python语言中一切皆对象,变量都是对象的引用,包括函数。这有点像C语言的指针。我们可以先创建一个对象,并给对象一定的值,这叫做实例化。先看一下下面的案例:

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
import sys

def show_input(input_context="default"):
print(input_context)

if __name__ == '__main__':
# <function show_input at 0x0000000002476F70>
print(show_input)

object_func = show_input
object_func_other = show_input
object_func()
object_func_other()

# 打印变量指向的内存地址:地址相同(38432624)
print("address of {} is {}".format(object_func.__name__, id(object_func)))
print("address of {} is {}".format(object_func_other.__name__, id(object_func_other)))

# 打印对象引用次数:4
print(sys.getrefcount(show_input))

# delete
del object_func
# object_func()
object_func_other()

# 打印对象引用次数:3
print(sys.getrefcount(show_input))

对于上面代码进行一下说明:

  • 变量名show_inputobject_funcobject_func_other都是指向同一个函数对象(共享引用),并且增加括号后均能调用。
  • 可以使用sys.getrefcount方法查看对象被引用的次数,这里除了3个变量引用:show_inputobject_funcobject_func_other,还有方法的临时引用。所以计数是4。当使用del 删除一个变量引用(object_func)后,对象引用计数变为3。这时候调用object_func()就会报错:NameError: name 'object_func' is not defined
  • 一个对象如果引用次数为:0,Python会启动垃圾回收机制,回收对象,释放资源。

1.2 局部函数

Python支持在函数体内定义函数,这种在函数体内被定义的函数称为局部函数。

在默认情况下,局部函数对外部是隐藏的,局部函数只能在其封闭函数内有效。例如:

1
2
3
4
5
6
7
8
9
def show_input(input_context="default"):
def print_input():
print(input_context)
print_input()


if __name__ == '__main__':
show_input("test")
# print_input()

对于上面代码进行一下说明:

  • 函数show_input中我们定义了局部函数print_input,并进行了内部调用。
  • print_input在不能直接调用,否则会报未定义错误:NameError: name 'print_input' is not defined

但是函数可以将局部函数作为一个对象进行返回,以便程序在其他作用域中使用局部函数。

1.3 函数返回函数对象

直接看例子:

1
2
3
4
5
6
7
8
9
def show_input(input_context="default"):
def print_input():
print(input_context)
return print_input

if __name__ == '__main__':
return_func = show_input("test")
return_func()
# 输出:test

对于上面代码进行一下说明:

  • 例子中我们在show_input函数中定义了print_input函数,并且局部函数print_input引用了外部函数的参数input_context,当show_input函数返回print_input函数的时候,相关的变量保存在返回的函数中,这种称谓闭包(Closure)。

1.4 函数作为参数

既然函数是对象,所以也可以和其他 Python 对象一样,作为参数传递到另一个函数中去。

1
2
3
4
5
6
7
8
def show_input(input_func, print_str: str):
input_func(print_str)

def print_input(input_context):
print(input_context)

if __name__ == '__main__':
show_input(print_input, "test")

对于上面代码进行一下说明:

  • 函数print_input作为参数传给show_input函数,并在内部进行了调用。

第二部分 Python中的装饰器

2.1 案例

直接看案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def decorator_test(func):
def decorator_func():
func()
return decorator_func

@decorator_test
def show_input():
print("test")


if __name__ == '__main__':
show_input()
print(show_input.__name__)
# decorator_func

例子中我们定义了函数decorator_test,其内部又定义了局部函数decorator_func,并且将函数作为返回对象。所以@decorator_test装饰器的本质是将被装饰的函数作为对象参数传入装饰函数中,然后调用执行。

但是当我们查看show_input的函数名属性show_input.__name__的时候发现输出是装饰函数名:decorator_func。这容易造成困惑,所以Python提供了functools.wraps方法解决这个问题,如下:

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

def decorator_test(func):
@wraps(func)
def decorator_func():
func()
return decorator_func

@decorator_test
def show_input():
print("test")

if __name__ == '__main__':
show_input()
print(show_input.__name__)
# show_input

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

2.2 标准实践

事实上,装饰器可以在环境调用前后分别添加额外的代码逻辑:

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_test(func):
@wraps(func)
def wrapper(*args, **kwargs):
# 编写函数执行前逻辑
# Do before
value = func(*args, **kwargs)
# 编写函数执行后逻辑
# Do after
return value
return wrapper

@decorator_test
def show_input():
pass


if __name__ == '__main__':
show_input()

第三部分 最佳实践

在日常编码中函数装饰器用途主要有:

  • 权限管理

    在函数运行前,提前校验用户数时候有执行权限;

  • 日志管理

    函数执行前后补充打印响应的日志信息;

  • 函数执行监控

    输出相关函数执行信息;

3.1 权限管理

直接看案例:

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
from functools import wraps

def check_auth(user: str, passwd: str):
auth_dict = {"Tom": "admin_role"}
auth_passwd = {"Tom": "oper1234"}

if auth_dict[user] == "admin_role" and auth_passwd[user] == passwd:
return True
else:
return False

def get_user():
return "Tom", "oper1234"

def auth_checker(func):
@wraps(func)
def wrapper(*args, **kwargs):
if check_auth(get_user()[0], get_user()[1]):
value = func(*args, **kwargs)
return value
else:
print("no auth")
return wrapper

@auth_checker
def show_input():
print("test")

if __name__ == '__main__':
show_input()

3.2 日志管理

看案例:

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
import logging


def decorator_logger(func):
@wraps(func)
def wrapper(*args, **kwargs):
logging.warning('start calling: ' + func.__name__)
value = func(*args, **kwargs)
logging.warning('end calling: ' + func.__name__)
return value
return wrapper


@decorator_logger
def show_input():
print("test")


if __name__ == '__main__':
show_input()

3.3 函数执行监控

例如我们监控统计函数执行的耗时:

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

def decorator_time(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
value = func(*args, **kwargs)
print('function {} run cost time: {}'.format(func.__name__, time.perf_counter() - start))
return value
return wrapper

@decorator_time
def show_input():
time.sleep(3)
print("test")

if __name__ == '__main__':
show_input()

第四部分 高阶使用

4.1 带参装饰器

前面的例子中装饰器都是没有传参的,那么如何实现传参呢?例如日志应用场景,我们将日志内容,输出到指定的文件中。

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
from functools import wraps
import logging


def logger(log_file_path: str):
def decorator_logger(func):
@wraps(func)
def wrapper(*args, **kwargs):
logging.basicConfig(filename=log_file_path,
filemode="a",
format="%(asctime)s %(name)s:%(levelname)s:%(message)s",
datefmt="%d-%M-%Y %H:%M:%S",
level=logging.INFO)
logging.warning('start calling: ' + func.__name__)
value = func(*args, **kwargs)
logging.warning('end calling: ' + func.__name__)
return value
return wrapper
return decorator_logger


@logger("file.log")
def show_input():
print("test")


if __name__ == '__main__':
show_input()
# 最后日志输出到file.log中,文件内容:
# 03-49-2022 13:49:36 root:WARNING:start calling: show_input
# 03-49-2022 13:49:36 root:WARNING:end calling: show_input

看这个原理其实就是套娃,等价于:

1
logger(log_file_path='file.log')(show_input)

4.2 装饰器类

前面例子里的装饰器都是函数,其实装饰器语法其实并不要求本身是函数,而只要是一个可调用对象即可。我们将上面日志装饰器改造成类装饰器:

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
from functools import wraps
import logging


class Logger:
def __init__(self, log_file_path):
self.log_file_path = log_file_path

def __call__(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
logging.basicConfig(filename=self.log_file_path,
filemode="a",
format="%(asctime)s %(name)s:%(levelname)s:%(message)s",
datefmt="%d-%M-%Y %H:%M:%S",
level=logging.INFO)
logging.warning('start calling: ' + func.__name__)
value = func(*args, **kwargs)
logging.warning('end calling: ' + func.__name__)
return value

return wrapper


@Logger("file.log")
def show_input():
print("test")


if __name__ == '__main__':
show_input()

实现效果是相同的。

4.3 叠加装饰器

装饰器多个能否一同使用,即叠加使用。是可以的,例如同时使用日志装饰器和函数执行监控装饰器:

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
43
from functools import wraps
import logging
import time


def decorator_time(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
value = func(*args, **kwargs)
print('function {} run cost time: {}'.format(func.__name__, time.perf_counter() - start))
return value
return wrapper


class Logger:
def __init__(self, log_file_path):
self.log_file_path = log_file_path

def __call__(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
logging.basicConfig(filename=self.log_file_path,
filemode="a",
format="%(asctime)s %(name)s:%(levelname)s:%(message)s",
datefmt="%d-%M-%Y %H:%M:%S",
level=logging.INFO)
logging.warning('start calling: ' + func.__name__)
value = func(*args, **kwargs)
logging.warning('end calling: ' + func.__name__)
return value

return wrapper


@Logger("file.log")
@decorator_time
def show_input():
print("test")


if __name__ == '__main__':
show_input()

实际执行逻辑如下(套娃):

1
logger(log_file_path='file.log')(decorator_time(show_input))

其中__call__() 的作用是使实例能够像函数一样被调用。

4.4 装饰器和闭包

通常函数体重变量是局部变量,函数调用执行完毕后,该变量将被回收。但是对于局部函数(前文”函数返回函数对象”)情况有点不同了,即闭包(Closure)。

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 counter(func):
count = 0

@wraps(func)
def wrapper(*args, **kwargs):
print(count)
return func(*args, **kwargs)

return wrapper

@counter
def show_input():
pass

if __name__ == '__main__':
show_input()
show_input()
# 0
# 0

例子中count变量称为自由变量,又称为外层变量被闭包捕获。这样看装饰器天然就是一个闭包。

既然装饰器就是闭包,那么其中的自由变量就不会随着原函数的返回而销毁,而是伴随着原函数一直存在。利用这一点,装饰器就可以携带状态。

我们看下面的案例:

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 counter(func):
count = 0
@wraps(func)
def wrapper(*args, **kwargs):
nonlocal count
count += 1
print(count)
return func(*args, **kwargs)
return wrapper

@counter
def show_input():
pass

if __name__ == '__main__':
show_input()
show_input()
# 1
# 2

例子中我们在局部函数中声明count变量不是一个局部变量。

4.5 小结

  • 装饰器是闭包的一种应用,是返回值为函数的高阶函数;
  • 装饰器修饰可调用对象,也可以带有参数和返回值;
  • 装饰器中可以保持状态。

第五部分 Python内置装饰器

Python中内置装饰器有三个:@property@staticmethod@classmethod。我们尽量在一个案例中体现并说明:

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
class Person:
test_name = "jack"
def __init__(self,name,sex,age):
self.name = name
self.sex = sex
self.age = age

@property
def info(self):
return self.name

@staticmethod
def sleep():
print("person should sleep!")

@classmethod
def learn(cls):
print("{} need learn!".format(cls.test_name))

if __name__ == '__main__':

Person.sleep()
# person should sleep!
Tom = Person("Tom", "man", 6)
Tom.sleep()
# person should sleep!
Tom.learn()
Person.learn()
# jack need learn!

print(Tom.info)
# Tom

5.1 特性装饰器@property

例子中info函数使用了@property注释,这样调用类中的info方法,像引用类中的字段属性一样。注意下面调用时候没有括号。

1
2
3
4
5
@property
def info(self):
return self.name

print(Tom.info)

5.2 静态方法装饰器@staticmethod

将类中的方法装饰为静态方法,即类不需要创建实例的情况下,可以通过类名直接引用。

1
2
3
4
5
6
7
    @staticmethod
def sleep():
print("person should sleep!")
Person.sleep()
# person should sleep!
Tom.sleep()
# person should sleep!

案例中,在类没有实例化前就可以直接调用:Person.sleep(),当然实例化后也可以调用:Tom.sleep()

5.3 类方法装饰器@classmethod

1
2
3
4
5
6
7
test_name = "jack"
@classmethod
def learn(cls):
print("{} need learn!".format(cls.test_name))
Tom.learn()
# jack need learn!
Person.learn()

方法调用的时候无需实例化类。

参考文献及资料

1、PEP 318 – Decorators for Functions and Methods,链接:https://peps.python.org/pep-0318/

0%