【python】一文搞懂 python 高阶函数、decorator-装饰器及其本质(通俗)

0. 引言

装饰器是 Python 中一种强大的语法特性,它可以用于动态修改、扩展或包装函数或方法的行为。

简言之就是我现在有个函数,但是我想扩充函数功能,比如:在调用函数前后加上日志、或者记录一下函数运行时间等功能。 首先想到的就是在函数的最前面和最后加上相关的代码。

但是如果我们并不想改变函数的定义,又或者我有很多函数都需要打印日志、计时器的功能,这时候 decorator-装饰器 就发挥了其作用。

1. 高阶函数

想了解装饰器首先需要知道高阶函数相关的知识。

其实在 python 中一切都是对象。函数名其实也是一个变量,只不过它指向了一个 callable的函数对象。

  • 我们知道函数的参数能接受变量,那么函数可以接受 函数名 这个变量。
  • 函数也可以返回变量,所以函数也可以返回 函数名 这个变量。

这样接受函数作为参数、返回函数、接受+返回函数的函数就称为高阶函数

1.1 接受函数参数:

# 一个简单的例子:计算两个数 x, y 的平方和 

# 先写一个计算平方的函数
def calc_pow(num):
    """返回平方
    Args:
        num (数字): 待求平方的数
    Returns:
        数字: 返回平方
    """
    return num*num

# 再写一个和函数
def add_pows(num1, num2, func):
    """计算平方和
    Args:
        num1 (数字): 第一个数
        num2 (数字): 第二个数
        func (函数): 计算平方的函数

    Returns:
        数字: 返回平方和
    """
    return func(num1) + func(num2)

# 使用高阶函数
result = add_pows(1, 2, calc_pow)
print(result)  # 输出 5

上面就是一个高阶函数的例子,他接受一个函数为参数,然后在内部调用了这个函数。

1.2 返回函数

# 计算某个数的n倍
def func_maker(n):
    """生成计算 n 倍的函数
    Args:
        n (数字): 需要的倍数
    Returns:
        函数: 计算 n 倍的函数
    """
    def func(x):
        return n * x
    return func

# 使用
f = func_maker(3)  # 生成计算 3 倍的函数
print(type(f))     # 输出 <class 'function'>
result = f(1)      # 计算 1 的 3 倍
print(result)      # 输出 3

上面这个例子中 func_maker 就是一个返回函数的高阶函数。

2. 装饰器

2.0 原函数

考虑现在我们有个原始函数

def hello(word = 'world'):
    """Say hello    
    Args:
        word (str, optional): 随便一个字符串. 默认值是 'world'.
    """
    time.sleep(1)  # 等待一秒
    print(f'hello {word}') # 默认

# 调用
hello()  # 输出 hello world
hello('girl') # 输出 hello girl

我需要给它加上运行前后打印日志的功能:

2.1 修改原函数方法

def hello_new(word = 'world'):
    """Say hello    
    Args:
        word (str, optional): 随便一个字符串. 默认值是 'world'.
    """
    print('函数开始运行——————')
    time.sleep(1)  # 等待一秒
    print(f'hello {word}') # 默认
    print('函数运行结束!!!')

# 调用
hello_new()  
hello_new('girl') 

"""输出
函数开始运行——————
hello world
函数运行结束!!!
函数开始运行——————
hello girl
函数运行结束!!!
"""

但是我不想修改函数或者是我有很多函数都需要打印日志,一个个改不方便,

2.2 高阶函数方法:

def add_log_func(func):
    """给接受的函数两头加上日志,并返回原函数运行结果
    Args:
        func (函数): 你要加日志的函数
                    也能接受其他的参数(其实就是原来的函数的参数)
    Returns:
        Any: 原函数的返回值
    """
    def nw_func(*args, **kwargs):
        print('函数开始运行——————')
        result_of_originfunc = func(*args, **kwargs)
        print('函数运行结束!!!')
        return result_of_originfunc
    return nw_func

# 前面讲了函数名其实也是个变量
# 我们让我原来的函数名指向新的函数对象(也就是 c 函数返回的新函数
# 这个新函数本质上就是在原来的函数调用前后添加了我们需要的功能
hello = add_log_func(hello)  
print(hello)  # <function hello_log.<locals>.nw_func at 0x7fb1b8361000>


# 然后函数已经指向了一个新的功能扩展的函数,之后的调用也不用变
# 如果其他函数也需要打印日志:f = hello_log(f) 其他不用变

hello()  # 输出 hello world
hello('girl') # 输出 hello girl

"""输出
函数开始运行——————
hello world
函数运行结束!!!
函数开始运行——————
hello girl
函数运行结束!!!
"""

上面其实就是一个高阶函数的用法,使用 add_log_func 函数返回一个新的带有前后打印日志功能的函数,并把它赋给我原来功能少的函数,而且如果我其他的函数也可以复用这个 add_log_func 函数增加打印日志功能。

这个其实就是 decorator-装饰器 的本质!

2.3 decorator-装饰器 方法

import functools

def add_log(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print('函数开始运行——————')
        result_of_originfunc = func(*args, **kwargs)
        print('函数运行结束!!!')
        return result_of_originfunc
    return wrapper

@add_log
def hello(word = 'world'):
    """Say hello    
    Args:
        word (str, optional): 随便一个字符串. 默认值是 'world'.
    """
    time.sleep(1)  # 等待一秒
    print(f'hello {word}') # 默认

# 调用
hello()  
hello('girl') 

"""输出
函数开始运行——————
hello world
函数运行结束!!!
函数开始运行——————
hello girl
函数运行结束!!!
"""

装饰器 @decorator 的本质就是执行了 function_name = decorator(function_name)
@functools.wraps(func):这个后面解释。

2.4 给装饰器传参

有些时候需要给装饰器穿参数(例如:动态配置、多次执行、条件执行等)。
比如:

import functools

def add_log_timer(condition=None):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            if condition == 'log':  # 记录日志
                print('函数开始运行——————')
                result_of_originfunc = func(*args, **kwargs)
                print('函数运行结束!!!')
               
            elif condition == 'timer':  # 记录时间
                start = time.time()
                result_of_originfunc = func(*args, **kwargs)
                end = time.time()
                print(f'函数运行时间为:{end - start} ms.')
                
            else:  # 什么也不干,只运行原函数
                result_of_originfunc = func(*args, **kwargs)
            return result_of_originfunc
        return wrapper
    return decorator

这个装饰器接受一个可选参数 condition: 然后根据其值动态应用装饰器。
(1) 如果传入 'log': 就是日志打印

@add_log_timer('log')
def hello(word = 'world'):
    """Say hello    
    Args:
        word (str, optional): 随便一个字符串. 默认值是 'world'.
    """
    time.sleep(1)  # 等待一秒
    print(f'hello {word}') # 默认

# 调用
hello()  
hello('girl') 

"""输出
函数开始运行——————
hello world
函数运行结束!!!
函数开始运行——————
hello girl
函数运行结束!!!
"""

(2) 如果传入 'timer': 就是计时器

@add_log_timer('timer')
def hello(word = 'world'):
    """Say hello    
    Args:
        word (str, optional): 随便一个字符串. 默认值是 'world'.
    """
    time.sleep(1)  # 等待一秒
    print(f'hello {word}') # 默认

# 调用
hello()  
hello('girl') 

"""输出
hello world
函数运行时间为:1.001284122467041 ms.
hello girl
函数运行时间为:1.001204252243042 ms.
"""

(3) 如果什么也不传,就是默认参数 None,运行原函数:

@add_log_timer()
def hello(word = 'world'):
    """Say hello    
    Args:
        word (str, optional): 随便一个字符串. 默认值是 'world'.
    """
    time.sleep(1)  # 等待一秒
    print(f'hello {word}') # 默认

# 调用
hello()  
hello('girl') 

"""输出
hello world
hello girl
"""

相同的上面等价于: function_name = decorator(arg)(function_name):

  • decorator(arg) 返回一个装饰器,它接受一个函数作为参数。
  • decorator(arg)(function_name) 这层就跟前面一样,返回一个扩展了功能的函数。

2.5 @functools.wraps(func) 功能

@functools.wraps(func):其实也是个是一个装饰器(已经实现好了的),它用于保留原始函数(func)的元信息,包括函数的名称、文档字符串、参数签名等。可以确保被装饰的函数在调用时保持原始函数的属性,而不是被装饰器函数的属性。否则,有些依赖函数签名的代码执行就会出错。

直接上例子:

  1. 原函数属性(名称、文档字符串)
import inspect

def hello(word = 'world'):
    """Say hello    
    Args:
        word (str, optional): 随便一个字符串. 默认值是 'world'.
    """
    time.sleep(1)  # 等待一秒
    print(f'hello {word}') # 默认

print(f'函数名:{hello.__name__}\n')
print(f'文档字符串:\n{hello.__doc__}')
# 获取函数签名
signature = inspect.signature(hello)
print(f"参数: {', '.join(str(param) for param in signature.parameters.values())}")


"""输出
函数名:hello

文档字符串:
Say hello    
    Args:
        word (str, optional): 随便一个字符串. 默认值是 'world'.
        
参数: word='world'
"""
  1. 没加 @functools.wraps(func)
import inspect

def add_log(func):
    
    def wrapper(*args, **kwargs):
        print('函数开始运行——————')
        result_of_originfunc = func(*args, **kwargs)
        print('函数运行结束!!!')
        return result_of_originfunc
    return wrapper

@add_log
def hello(word = 'world'):
    """Say hello    
    Args:
        word (str, optional): 随便一个字符串. 默认值是 'world'.
    """
    time.sleep(1)  # 等待一秒
    print(f'hello {word}') # 默认
    
    
print(f'函数名:{hello.__name__}\n')
print(f'文档字符串:\n{hello.__doc__}')
# 获取函数签名
signature = inspect.signature(hello)
print(f"参数: {', '.join(str(param) for param in signature.parameters.values())}")

"""输出
函数名:wrapper

文档字符串:
None
参数: *args, **kwargs
"""
  1. 加了 @functools.wraps(func)
import inspect
import functools

def add_log(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print('函数开始运行——————')
        result_of_originfunc = func(*args, **kwargs)
        print('函数运行结束!!!')
        return result_of_originfunc
    return wrapper

@add_log
def hello(word = 'world'):
    """Say hello    
    Args:
        word (str, optional): 随便一个字符串. 默认值是 'world'.
    """
    time.sleep(1)  # 等待一秒
    print(f'hello {word}') # 默认
    
    
print(f'函数名:{hello.__name__}\n')
print(f'文档字符串:\n{hello.__doc__}')
# 获取函数签名
signature = inspect.signature(hello)
print(f"参数: {', '.join(str(param) for param in signature.parameters.values())}")

"""输出
函数名:hello

文档字符串:
Say hello    
    Args:
        word (str, optional): 随便一个字符串. 默认值是 'world'.
    
参数: word='world'
"""

再读一下本小节开头,就明白了。

  • 37
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值