Python函数装饰器语法

关于高阶函数

Posted by Miso on February 6, 2026

0. 前言 关于高阶函数


高阶函数的定义:能够接收其他函数作为参数,或者将函数作为返回值的函数。它是“函数式编程”的灵魂组成部分。

1
2
3
4
5
6
7
8
9
10
def add(a: int,b: int) -> int:
    return a + b

def sub(a: int,b: int) -> int:
    return a - b

def do_operation(a: int, b: int, operation: Callable[[int, int], int]) -> int:
    return operation(a, b)
    
do_operation(1,2,add) # 返回 3

在这里,我们定义了两个函数add和sub。一旦经由def定义,add和sub两个函数名便成为了“可调用对象”,类型提示为: Callable[[int, int], int],是接受两个整数、返回整数的函数。

请注意,这里我们讨论可调用对象时,没有在函数名后面加上括号填入参数。因为add(1,2)是3: int。在一个可调用对象后面加上括号传入参数的行为便是大名鼎鼎的“调用”,此时整个表达式等于函数的返回值。

1. 举个栗子:计时装饰器


1
2
3
4
5
6
7
8
9
def f_interface(n: int) -> int:
    if n <= 2:
        return 1
    return f_recursion(n)

def f_recursion(n: int) -> int:
    if n <= 2:
        return 1
    return f_recursion(n - 1) + f_recursion(n - 2)

这是一套运用递归方法求出斐波那契数列特定项的函数,其时间复杂度为$O(2^n)$

当n取30到40时,函数运行便需要较长的时间。我们希望有一种方法来计算函数运行的用时。

朴素的方法

用朴素的方法实现,我们需要修改f_interface,如下:

1
2
3
4
5
6
7
8
9
def f_interface_v1(n: int) -> int:
    start_time = time.time()
    if n <= 2:
        result = 1
    else:
        result = f_recursion(n)
    end_time = time.time()
    print(f"Function f_interface_v1 took {end_time - start_time} seconds to run")
    return result

这个改动……太过伤筋动骨了。

为了实现统计时间的功能,我们统一了函数出口,放弃了卫语句。我们还要环绕着核心业务写三行代码,把计时功能和原有业务紧紧耦合在一起,不便于观看,也不便于删除。

让高阶函数上场

与其在函数内部进行修改,不如尝试在函数外面修改。怎么做呢?

我们设想一下下面这样的函数:

1
2
3
4
5
6
def add_time_logger(func: Callable[[int], int])
	do_what?
	func_with_time_logger: Callable[[int], int] = what?
	return func_with_time_logger
	
f_interface = add_time_logger(f_interface)

这样,我们用带有时间统计的新函数覆盖了旧函数名。函数签名不变,意味着任何使用新旧函数的地方都不需要有任何修改。

现在,我们只需要专注实现这个函数。

do_what?

建立嵌套的def

显然,我们要在add_time_logger内定义一个新的函数,这个函数接受单个int参数,返回int. 此时我们需要嵌套的def。

1
2
3
4
5
def add_time_logger(func: Callable[[int], int]):
    def func_with_time_logger(arg: int):
        do_what?
        return what?
    return func_with_time_logger

现在我们面临两个问题。新函数要做什么?返回什么?

新函数的内部逻辑

新旧函数的参数列表和返回值含义是完全一致的,因此,必须调用旧函数来获取返回值。

旧函数便是最外层def形参中的func

1
2
3
4
5
def add_time_logger(func: Callable[[int], int]):
    def func_with_time_logger(arg: int):
        result = func(arg)
        return result
    return func_with_time_logger

此时,时间统计的逻辑可以环绕形参的调用func(arg)展开

1
2
3
4
5
6
7
8
def add_time_logger(func: Callable[[int], int]):
    def func_with_time_logger(arg: int):
        start = time.time()
        result = func(arg)
        end = time.time()
        print(f"Function {func.__name__} took {end - start} seconds to run")
        return result
    return func_with_time_logger

其中func.__name__用以获取函数名,使打印更清晰。

至此,高阶函数add_time_logger构建完成。

语法糖

直接写f_interface = add_time_logger(f_interface)两次书写了原函数名,显得非常臃肿。

并且,允许函数名覆盖以这种形式写在原def下游的任何位置是非常危险的。两个函数的签名和返回值完全一致,如果找不到这一行赋值,我们很难发现函数是否被覆盖。

两个函数少有的区别在于:__doc__会丢失。(这可不是什么好消息)

为了解决这一问题,Python提供了一种好用的语法糖。我们只需:

1
2
3
4
5
@add_time_logger
def f_interface(n: int) -> int:
    if n <= 2:
        return 1
    return f_recursion(n)

直接将@加上高阶函数名(我们称之为装饰器)置于需修饰的函数上方,这一操作等价于f_interface = add_time_logger(f_interface),但可保留__doc__

恭喜我们学会了函数装饰器。语法如此。

2. 上点硬菜:记忆化搜索装饰器


众所周知,$O(2^n)$时间复杂度在绝大多数情况下都是不可接受的。我们尝试引入记忆化搜索技术,将时间复杂度压缩至$O(n)$

我们需要用一个哈希表来存储已有的计算结果,其存取操作和包含判断均为$O(1)$

装饰器构造如下:

1
2
3
4
5
6
7
8
9
10
def memorize_result(func: Callable[[int], int]):
    result_dict = {1:1, 2:1}
    def wrapper(n: int) -> int:
        if n not in result_dict:
            result = func(n)
            result_dict[n] = result
        else:
            result = result_dict[n]
        return result
    return wrapper

用wrapper指代新函数为业界惯例。新函数只在没有搜索到结果的情况下才会执行旧函数,同时存储结果。

运用时,只需:

1
2
3
4
5
@memorize_result
def f_recursion(n: int) -> int:
    if n <= 2:
        return 1
    return f_recursion(n - 1) + f_recursion(n - 2)

即可对这个函数施加记忆化搜索功能,显著提高其计算速度。

3. 拓展:将装饰器应用到更多函数


刚才提到的装饰器只能应用于一个: Callable[[int], int]显得有些不足。尝试将其应用于所有类型组合的函数:

1
2
3
4
5
6
7
8
def add_time_logger(func: Callable[..., Any]):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"Function {func.__name__} took {end - start} seconds to run")
        return result
    return wrapper

我们引入了*args, **kwargs来接受任意参数,同时使用: Callable[..., Any]注解来适配任何返回值的函数。