蒙珣的博客

活好当下,做好今天该做的事情。

0%

关于python-functools

  • functools.wraps
  • functools.lru_cache(待补充)
  • functools.total_ordering(待补充)
  • functools.total_ordering(待补充)
  • functools.singledispatch(待补充)
  • functools.partial(待补充)
  • functools.partialmethod(待补充)
  • functools.reduce(待补充)

functools.wraps

functools.wraps 是一个 Python 的装饰器工厂函数,用于更新一个函数对象,将另一个函数对象的元信息(如名称、文档字符串、注解和模块)复制到它上面。这通常用于创建装饰器,以确保被装饰的函数在装饰之后仍然保持其原始的元信息。

当你创建一个装饰器时,你通常会定义一个接受函数作为参数的函数,并返回一个新的函数对象。这会导致原始函数的元信息(如函数名)丢失,因为返回的是一个全新的函数对象。使用 functools.wraps 可以帮助解决这个问题。

先看一个例子

1
2
3
4
5
6
7
8
9
10
11
12
def hello():
"print a well-known message"
print("hello world")

hello()
> hello world

hello.__doc__
> 'print a well-known message'

hello.__name__
> 'hello'

接着我们构建一个装饰器并使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def noop(func):
def noop_wrapper():
"this is a decorator"
return func()
return noop_wrapper

@noop
def hello():
"print a well-known message"
print("hello world")

hello()
> hello world

hello.__doc__
> 'this is a decorator'

hello.__name__
> 'noop_wrapper'

可以发现,在套用装饰器的时候,函数的名称和文档都被修改成了装饰器的了

我们有两种方法可以修改回来,第二种方法就是使用functools

方法一:对__name____doc__重新赋值

__name____doc__重新赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def noop(func):
def noop_wrapper():
"this is a decorator"
return func()
noop_wrapper.__name__ = func.__name__
noop_wrapper.__doc__ = func.__doc__
return noop_wrapper

@noop
def hello():
"print a well-known message"
print("hello world")

hello()
> hello world

hello.__doc__
> 'print a well-known message'

hello.__name__
> 'hello'

方法二:使用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
22
from functools import wraps

def noop(func):
@wraps(func)
def noop_wrapper():
"this is a decorator"
return f()
return noop_wrapper

@noop
def hello():
"print a well-known message"
print("hello world")

hello()
> hello world

hello.__doc__
> 'print a well-known message'

hello.__name__
> 'hello'

实用性例子

下面是一个日志模块,其他程序套用日志模块来进行日志输出

先来看下大致的目录结构

1
2
3
4
5
6
7
8
9
10
├── logs
│   └── __init__.py
├── testcase
│   ├── __pycache__
│   ├── demo
│      └── test_logger_demo.py
└── utils
├── __init__.py
├── __pycache__
└── log_manager.py

log_manager.py

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
from loguru import logger
from time import strftime
import os, sys
from functools import wraps
import time

class LoggerManager:

def __init__(self):
self.logger = logger
# 移除自带的logger控制台输出,防止重复打印日志信息
logger.remove()
filename = strftime('%Y%m%d-%H%M%S')
# 如果testcase目录下还有一个目录比如demo,demo里是执行文件,就用../../logs
# 如果testcase目录下就是test_demo.py 就用../logs/
log_file_path = os.path.join('../../logs/', filename + '.log')
print(log_file_path)
log_format = '<green>{time: YYYY-MM-DD HH:mm:ss.SSS}</green> {level} {message}'
level_ = 'DEBUG'
rotation_ = '5MB'

"""
:enqueue=True 是否使用队列异步地将日志消息写入文件,
日志消息会被排入队列中,然后由后台线程或进程异步地写入文件。
这样可以提高日志记录的性能,避免阻塞主线程。
:backtrace=True 这个参数用于指定是否记录追溯信息
日志中将包含追溯信息,帮助你追踪日志消息的来源
:diagnose=True 这个参数指定是否在日志文件中包含诊断信息,
例如记录日志消息时的函数调用栈等。这对于排查日志问题很有用。
:rotation=rotation_ 这个参数用于指定日志文件的轮转策略
"""
self.logger.add(log_file_path,
enqueue=True,
backtrace=True,
diagnose=True,
encoding='UTF-8',
rotation=rotation_)

"""
标准错误流 (sys.stderr): 用于输出错误信息和警告,通常用于指示程序运行中的问题
"""
self.logger.add(sys.stderr,
format=log_format,
enqueue=True,
backtrace=True,
diagnose=True,
colorize=True,
level=level_)

# 方法的logger装饰器
def runtime_logger(self, func):
#@wraps(func)
def wrapper(*args, **kwargs):
self.logger.info(f"{func.__name__} 当前开始执行")

now1 = time.time()

try:
func(*args, **kwargs)
except Exception as e:
self.logger.error(f"{func.__name__} 当前用例执行失败,失败的原因是: {e}")
now2 = time.time()
self.logger.success(f"{func.__name__} 当前执行成功,耗时:{now2 - now1}ms")

return func

return wrapper

# 类的logger装饰器
def runtime_logger_class(self, cls):
for attr_name in dir(cls):
if attr_name in dir(cls):
if attr_name.startswith('test_') and callable(getattr(cls, attr_name)):
setattr(cls, attr_name, self.runtime_logger(getattr(cls, attr_name)))
return cls


my_logger = LoggerManager()

test_logger_demo.py

1
2
3
4
5
6
7
8
9
10
11
12
13
import pytest
from utils.log_manager import my_logger

#@my_logger.runtime_logger_class
class TestDemo:

@my_logger.runtime_logger
def test_01_demo(self):
my_logger.logger.info("这是一条日志")

if __name__ == '__main__':
pytest.main()

如果不使用functools.wraps用例的__name__就会被修改成wrapper,这是我们不希望看到的

修改前:

1
2
3
4
========================= 1 passed, 1 warning in 0.03s =========================
2024-03-10 23:12:09.733 INFO wrapper 当前开始执行
2024-03-10 23:12:09.733 INFO 这是一条日志
2024-03-10 23:12:09.733 SUCCESS wrapper 当前执行成功,耗时:0.00012969970703125ms

修改后:

1
2
3
4
========================= 1 passed, 1 warning in 0.03s =========================
2024-03-10 23:12:09.733 INFO test_01_demo 当前开始执行
2024-03-10 23:12:09.733 INFO 这是一条日志
2024-03-10 23:12:09.733 SUCCESS test_01_demo 当前执行成功,耗时:0.00012969970703125ms