cook book:9:元编程+10:模块与包

1:在函数上添加包装器  在函数上添加一个包装器,增加额外的操作处理(比如日志、计时等)。

# 使用额外的代码包装一个函数,可以定义一个装饰器函数
import time
from functools import wraps


def timethis(func):
    """报告执行时间的装饰程序"""
    # @wraps接受一个函数来进行装饰,并加入了复制函数名称、注释文档、参数列表等等的功能。
    # 这可以让我们在装饰器里面访问在装饰之前的函数的属性
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(func.__name__, end - start)
        return result

    return wrapper

@timethis
def countdown(n):
    total = 0
    while n > 0:
        total += n
        n -= 1
    return total

print(countdown(10000000))
# 打印
countdown 0.8188097476959229
50000005000000
# 一个装饰器就是一个函数,它接受一个函数作为参数并返回一个新的函数
@timethis
def countdown(n):
    pass
# 上面等同于下面
def countdown(n):
    pass
countdown = timethis(countdown)

@timethis 等同 countdown = timethis(countdown)
# 内置的装饰器比如 @staticmethod, @classmethod,@property 原理也是一样的
class A:
    @classmethod
    def method(cls):
        pass

class B:
    # 类方法的等价定义
    def method(cls):
        pass
    method = classmethod(method)
# 上面的 wrapper() 函数中, 装饰器内部定义了一个使用 *args 和 **kwargs 来接受任意参数的函数。
在这个函数里面调用了原始函数并将其结果返回,不过你还可以添加其他额外的代码(比如计时)。
然后这个新的函数包装器被作为结果返回来代替原始函数 # 需要强调的是装饰器并不会修改原始函数的参数签名以及返回值。 使用 *args 和 **kwargs 目的就是确保任何参数都能适用。
而返回结果值基本都是调用原始函数 func(*args, **kwargs) 的返回结果,其中func就是原始函数 # 使用 @wraps(func) 注解是很重要的, 它能保留原始函数的元数据,新手经常会忽略这个细节。

2:创建装饰器时保留函数元信息  functools 库中的 @wraps 装饰器 

# 写了一个装饰器作用在某个函数上,但是这个函数的重要的元信息比如名字、文档字符串、注解和参数签名都丢失了
# 任何时候定义装饰器的时候,都应该使用 functools 库中的 @wraps 装饰器来注解底层包装函数
import time
from functools import wraps

def timethis(func):
    """报告执行时间的装饰程序"""
    # @wraps接受一个函数来进行装饰,并加入了复制函数名称、注释文档、参数列表等等的功能。
    # 这可以让我们在装饰器里面访问在装饰之前的函数的属性
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(func.__name__, end - start)
        return result
    return wrapper

@timethis
def countdown(n: int):
    """countdown doc"""
    while n > 0:
        n -= 1

# 使用这个被包装后的函数countdown并检查它的元信息: countdown(
100000) print(countdown.__name__) # countdown print(countdown.__doc__) # countdown doc print(countdown.__annotations__) # {‘n‘: <class ‘int‘>}
# 在编写装饰器的时候复制元信息是一个非常重要的部分。
# 忘记了使用 @wraps,那么你会发现被装饰函数丢失了所有有用的信息。如果忽略 @wraps 后的效果是下面这样的
import time

def timethis(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(func.__name__, end - start)
        return result
    return wrapper

@timethis
def countdown(n: int):
    """countdown doc"""
    while n > 0:
        n -= 1
    return n

countdown(100000)
print(countdown.__name__)       # wrapper
print(countdown.__doc__)        # None
print(countdown.__annotations__)    # {}
import time
from functools import wraps

def timethis(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(func.__name__, end - start)
        return result
    return wrapper

@timethis
def countdown(n: int):
    """countdown doc"""
    while n > 0:
        n -= 1
    return n

# @wraps 有一个重要特征是它能让你通过属性 __wrapped__ 直接访问被包装函数
print(countdown.__wrapped__)
  # <function countdown at 0x000001FD64BFC4C0>:返回的是countdown这个函数的内存地址
print(countdown.__wrapped__(10000))     # 0
  # countdown.__wrapped__(10000) 调用的是countdown原本的函数,调用的是没有被timethis装饰器装饰过的函数,
  所以只打印0,没有运行timethis装饰器里面的代码 # __wrapped__ 属性还能让被装饰函数正确暴露底层的参数签名信息
from inspect import signature print(signature(countdown)) # (n: int),显示的是底层被装饰函数countdown的参数信息 # 如果装饰没有@wraps(func)的话 print(signature(countdown)) 打印的是(*args, **kwargs) # 怎样让装饰器去直接复制原始函数的参数签名信息, 如果想自己手动实现的话需要做大量的工作, 最好就简单的使用 @wraps 装饰器。 通过底层的 __wrapped__ 属性访问到函数签名信息

3:解除一个装饰器  装饰器已经作用在一个函数上,你想撤销它,直接访问原始的未包装的那个函数  @wraps:访问 __wrapped__ 属性来访问原始函数

# 访问 __wrapped__ 属性来访问原始函数:
@somedecorator
def add(x, y):
    return x + y


orig_add = add.__wrapped__
orig_add(3, 4)    # 7
# 直接访问未包装的原始函数在调试、内省和其他函数操作时是很有用的。 
但是我们这里的方案仅仅适用于在包装器中正确使用了 @wraps 或者直接设置了 __wrapped__ 属性的情况 # 多个包装器,那么访问 __wrapped__ 属性的行为是不可预知的,应该避免这样做。

在Python3.3中,它会略过所有的包装层,
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 print(add(2, 3)) # 打印:Decorator 1,Decorator 2,5 add.__wrapped__(2, 3) # 打印:5


Python3.4中:
print(add(2, 3))        # 打印:Decorator 1,Decorator 2,5
add.__wrapped__(2, 3)   # 打印:Decorator 2,5
py3.4和py3.9一样的机制
# 当前最新的py 3.9中
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  # 先调用decorator2再调用decorator1
def add(x, y):
    return x + y

# print(add(2, 3))
# print(add.__wrapped__.__wrapped__)  # <function add at 0x00000148C28ED5E0>
print(add.__wrapped__(2, 3))    # 打印:Decorator 2, 5
print(add.__wrapped__.__wrapped__(2, 3))    # 打印:5

@decorator1
@decorator2
func()
如上一个func函数调用多个装饰器,先进后院,先调用
@decorator2装饰器生成一个函数1后
生成的函数1再调用
@decorator1这个装饰器,类似一个嵌套

func函数调用
decorator2装饰器后生成一个函数1,func这个变量名指向新的函数1,func=1函数
1这个函数由于使用wraps装饰器,所以函数1的__wrapped__信息还是func这个最底层函数的信息
新的函数1又被@decorator1这个装饰器装饰生成一个函数2,func这个变量名指向新的函数2,func=2函数
2这个函数
使用wraps装饰器,使用函数1的信息,但是函数1的__wrapped__信息还是来自func这个最底层函数的信息
所以
func.__wrapped__的信息是add函数:<function add at 0x00000148C28ED5E0>
但是
func.__wrapped__返回的函数的内存地址指向的是函数1,因为被@decorator1装饰的函数是函数1
@decorator2装饰的函数是func,@decorator2装饰的函数func才生成函数1

所以例子:
add.__wrapped__(2, 3)调用的是被@decorator2装饰器装饰的函数func
    add.__wrapped__.__wrapped__(2, 3) 这个才是调用最底层的func
# 并不是所有的装饰器都使用了 @wraps ,
# 所以这里的方案并不全部适用。 
# 特别的,内置的装饰器 @staticmethod 和 @classmethod 就没有遵循这个约定 (它们把原始函数存储在属性 __func__ 中)

4:定义一个带参数的装饰器  定义一个可以接受参数的装饰器(三层装饰器)

# 装饰器,给函数添加日志功能
# 同时允许用户指定日志的级别和其他的选项

 

cook book:9:元编程+10:模块与包

上一篇:12课 按键消抖


下一篇:Super MB Pro M6, any better than MB sdconnect C4 Plus DoIP / C4 / C6?