第九章 元编程
9.1 给函数添加一个包装
简单案例
# 使用装饰器 给函数增加一个包装层添加额外的处理(如记录日志、计时统计)
import time
from functools import wraps
# 定义装饰器:用于统计函数执行时间
def timethis(func):
@wraps(func)
# 装饰器内部的代码一般会涉及创建一个新的函数,利用*args 和**kwargs来接受任意的参数
# 装饰器一般来说不会修改调用签名,也不会修改被包装函数返回的结果
def wrapper(*args, **kwargs):
start = time.time() # 记录开始时间
result = func(*args, **kwargs) # 执行原函数
end = time.time() # 记录结束时间
print(f'{func.__name__} 耗时: {end - start:.6f} 秒') # 输出耗时信息
return result
return wrapper
# 使用装饰器修饰 countdown 函数
@timethis
def countdown(n):
while n > 0:
n -= 1
# 主程序入口
if __name__ == '__main__':
# 调用测试
countdown(100000)
countdown(10000000)
装饰器的使用
# 装饰器就是一个函数,它可以接受一个函数作为输入并返回一个新的函数作为输出
# 如下两个操作时相同的
@timethis
def countdown(n):
...
def countdown(n):
...
countdown = timethis(countdown)
# Python 中的内置装饰器(如 @staticmethod、@classmethod 和 @property)本质上与我们自定义的装饰器工作方式一致
class A:
@classmethod
def method(cls):
pass
# 上述代码是等价的
class B:
def method(cls):
pass
method = classmethod(method)
class C:
@staticmethod
def method(x):
print(x)
# 等价于
class C:
def method(x):
print(x)
method = staticmethod(method)
9.2 编写装饰器时如何保存函数的元数据
# 使用wrap保留函数上如函数名、文档字符串、函数注解以及调用签名等重要的元数据
import time
from functools import wraps
def timethis(func):
# 使用functools.wraps可以保留被装饰函数的元数据
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f'{func.__name__} {end - start}')
return result
return wrapper
# 定义测试函数 countdown
@timethis
def countdown(n: int):
"""
Counts down
"""
while n > 0:
n -= 1
# 主程序入口
if __name__ == '__main__':
# 测试调用
countdown(100000) # 示例输出: countdown 0.008917808532714844
# 检查装饰器对元信息的影响
print(countdown.__name__) # 输出函数的名字: 'countdown'
print(countdown.__doc__) # 输出函数的文档字符串(函数开头使用""""""包裹的字符串): '\n\tCounts down\n\t'
print(countdown.__annotations__) # 输出表示函数参数和返回值的类型注解: {'n': <class 'int'>}
技巧
# 如果没有使用@wraps 会丢失相关的信息
print(countdown.__doc__) # 输出为空
print(countdown.__annotations__) # 输出{}
# 通过__wrapped__属性直接访问被包装的函数
countdown. wrapped (100000)
signature()的使用
# wraps 用于保留函数的元数据(名字、文档、注解),而signature用于获取/设置函数的参数签名信息
# signature()返回一个 Signature 对象,完整描述了函数的参数结构
from functools import wraps
from inspect import signature
# 示例函数
def foo(a: int, b: str) -> None:
pass
# 不带 wraps 的装饰器
def deco1(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
# 带 wraps 的装饰器
def deco2(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
# 使用装饰器
@deco1
def bar(x: float):
pass
@deco2
def baz(y: bool):
pass
print(signature(bar))
# 输出: (*args, **kwargs) ← 看不到原始参数信息!
print(signature(baz))
# 输出: (y: bool) ← 成功保留了原始签名!
9.3 对装饰器进行解包装
# 绕过装饰器直接访问原始函数
from functools import wraps
# 定义一个装饰器
def somedecorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("Before function call")
result = func(*args, **kwargs)
print("After function call")
return result
return wrapper
# 使用装饰器
@somedecorator
def add(x, y):
return x + y
# 主程序入口
if __name__ == '__main__':
# 通过 __wrapped__ 获取原始函数并调用
orig_add = add.__wrapped__
result = orig_add(3, 4) # 预期输出: 7
# 不会输出 Before function call之类的话
# 输出结果
print("Result:", result) # 输出: Result: 7
处理同时使用多个装饰器
from functools import wraps
def decorator1(func):
@wraps(func)
def wrapper(*args, **kwargs):
print('Decorator 1')
return func(*args, **kwargs)
return wrapper
def decorator2(func):
@wraps(func)
def wrapper(*args, **kwargs):
print('Decorator 2')
return func(*args, **kwargs)
return wrapper
@decorator1
@decorator2
def add(x, y):
return x + y
# 等价于add = decorator1(decorator2(add))
# 所以函数调用顺序是 add() → decorator1.wrapper() → decorator2.wrapper() → add()
print(add(2, 3)) # 输出 Decorator 1 Decorator 2 5
# 此时add.__wrapped__ 只会返回最外层装饰器(即 decorator1)的 wrapper 函数所包装的那个函数 —— 也就是 decorator2(wrapper)
# 当函数被多个装饰器修饰时,__wrapped__ 的行为是未定义的 ,不同版本或实现可能会有不同的结果
print(add.__wrapped__(2, 3)) # 输出 5
9.4 定义一个可接受参数的装饰器
# 编写一个为函数添加日志功能的装饰器 但是又允许用于指定日志等级比提供其余参数
from functools import wraps
import logging
# 设置基本的日志配置
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
# 最外层的 logged()函数接受所需的参数,并让它们对装饰器的内层函数可见
def logged(level, name=None, message=None):
def decorate(func):
log_name = name if name else func.__module__
log = logging.getLogger(log_name)
log_message = message if message else func.__name__
# 内层装饰器接受一个函数并给它加上一个包装层
@wraps(func)
def wrapper(*args, **kwargs):
log.log(level, log_message)
return func(*args, **kwargs)
return wrapper
return decorate
# 示例用法 1:只指定日志等级
@logged(logging.DEBUG)
def add(x, y):
return x + y
# 示例用法 2:指定日志等级和日志器名称
@logged(logging.CRITICAL, 'example')
def spam():
print('Spam!')
# 主程序入口
if __name__ == '__main__':
# 调用 add 函数,会打印日志
result = add(2, 3) # 输出: DEBUG - __main__ - add
print("add(2, 3) =", result) # 输出: add(2, 3) = 5
# 调用 spam 函数,会打印日志和 "Spam!"
spam()
# 输出:
# CRITICAL - example - spam
# Spam!
装饰器调用顺序
# 编写一个装饰器涉及调用的顺序
def decorator(x, y, z):
def decorate(func):
def wrapper(*args, **kwargs):
print(f"Decorator args: x={x}, y={y}, z={z}")
print(f"Calling function {func.__name__}")
return func(*args, **kwargs)
return wrapper
return decorate
@decorator(10, 20, 30)
def func(a, b):
pass
# 等价写法是func = decorator(10, 20, 30)(func)
# decorator(x, y, z)将返回一个装饰器 然后使用decorate(func)会返回一个包装函数 会真正的执行函数并添加功能
9.5 定义一个属性可由用户修改的装饰器
# 编写一个可以让用户调整属性的装饰器
# 引入了访问器函数(accessor function)
# 通过使用nolocal声明全局变量
from functools import wraps, partial
import logging
# 工具装饰器函数:将函数作为属性附加到对象上
def attach_wrapper(obj, func=None):
if func is None:
# 如果只提供了 obj,返回一个偏函数,等待后续的func
return partial(attach_wrapper, obj)
# 将func绑定为obj的一个属性 属性名为func.__name__
setattr(obj, func.__name__, func)
# 返回函数本身,不影响装饰器链式调用
return func
# 为任意函数添加日志记录功能,并允许在运行时动态修改日志级别和日志信息
# level 是日志级别,name 是日志记录器名称,message 是日志信息
# 未指定 name 和 message,则默认使用函数所属模块和函数名
def logged(level, name=None, message=None):
def decorate(func):
logname = name if name else func.__module__
log = logging.getLogger(logname)
logmsg = message if message else func.__name__
@wraps(func)
def wrapper(*args, **kwargs):
log.log(level, logmsg)
return func(*args, **kwargs)
# 访问器函数
@attach_wrapper(wrapper)
def set_level(newlevel):
# 使用nolocal表示不要创建新的局部变量 而是使用decorate函数闭包中的logmsg
nonlocal level
level = newlevel
# 访问器函数
@attach_wrapper(wrapper)
def set_message(newmsg):
nonlocal logmsg
logmsg = newmsg
return wrapper
return decorate
# 示例函数1:带DEBUG级别的日志
@logged(logging.DEBUG)
def add(x, y):
return x + y
# 示例函数2:带CRITICAL级别的日志,并指定logger名称为'example'
@logged(logging.CRITICAL, 'example')
def spam():
print('Spam!')
# 启用基础日志配置(在交互式环境中通常会调用)
logging.basicConfig(level=logging.DEBUG)
# 测试add函数
print(add(2, 3)) # 应输出DEBUG日志并返回5
# 修改日志消息内容
add.set_message('Add called')
print(add(2, 3)) # 应输出更新后的DEBUG日志并返回5
# 修改日志级别为WARNING
add.set_level(logging.WARNING)
print(add(2, 3)) # 应输出WARNING级别的日志并返回5
访问器可以跨越多个装饰器层进行传播
# 9.2装饰器代码回顾
def timethis(func):
# 使用functools.wraps可以保留被装饰函数的元数据
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f'{func.__name__} {end - start}')
return result
return wrapper
# 下述例子说明了访问器函数可以跨越多个装饰器进行传播
# 调换两个装饰器的顺序也可以
# 访问器函数之所以能跨越多个装饰器进行传播,是因为在使用 @wraps(func) 的前提下,装饰器返回的新函数保留了原函数的所有属性(包括附加的方法),使得这些方法可以在最外层被调用。
@timethis
@logged(logging.DEBUG)
def countdown(n):
while n > 0:
n -= 1
if __name__ == '__main__':
# 第一次调用
countdown(10000000)
# 输出 DEBUG: main : countdown countdown 0.8198461532592773
# 修改日志级别和消息
countdown.set_level(logging.WARNING)
countdown.set_message("Counting down to zero")
countdown(10000000)
# 输出WARNING: main : Counting down to zero countdown 0.8225970268249512
使用访问器函数返回内部的状态值
def logged(level, name=None, message=None):
def decorate(func):
logname = name if name else func.__module__
log = logging.getLogger(logname)
logmsg = message if message else func.__name__
@wraps(func)
def wrapper(*args, **kwargs):
log.log(level, logmsg)
return func(*args, **kwargs)
@attach_wrapper(wrapper)
def set_level(newlevel):
nonlocal level
level = newlevel
@attach_wrapper(wrapper)
def set_message(newmsg):
nonlocal logmsg
logmsg = newmsg
# 新增代码部分
# 添加 get_level
@attach_wrapper(wrapper)
def get_level():
return level
# 添加 get_message
@attach_wrapper(wrapper)
def get_message():
return logmsg
return wrapper
return decorate
# 使用代码示例
@logged(logging.INFO)
def say_hello():
print("Hello!")
# 获取当前状态
print(say_hello.get_level()) # 输出: 20 (即 logging.INFO)
print(say_hello.get_message()) # 输出: 'say_hello'
# 修改状态
say_hello.set_level(logging.WARNING)
say_hello.set_message("Greeting called")
# 再次查看
print(say_hello.get_level()) # 输出: 30 (即 logging.WARNING)
print(say_hello.get_message()) # 输出: 'Greeting called'
直接暴露属性和访问器对比
当我们使用装饰器且希望暴露一些内部状态或者提供修改接口时,有两种常见的思路:直接暴露属性或者使用访问器函数,直接暴露属性示例如下。
@wraps(func)
def wrapper(*args, **kwargs):
wrapper.log.log(wrapper.level, wrapper.logmsg)
return func(*args, **kwargs)
# 直接设置属性
wrapper.level = level
wrapper.logmsg = logmsg
wrapper.log = log
这样简单直观,易于读取状态。但是这些属性只存在于当前装饰器返回的函数对象上,如果你在它外面再加一层装饰器(例如 @timethis),那么最外层函数就无法访问这些属性。
1210

被折叠的 条评论
为什么被折叠?



