python元编程笔记

第九章 元编程

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),那么最外层函数就无法访问这些属性

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值