Python 高阶语法(一):高阶函数、闭包、lambda 与 functools——从底层真正理解装饰器

一、前言

在前面的 Python 学习中,我们已经接触过函数、类、装饰器,以及 FastAPI 中常见的:

python 复制代码
@app.get("/tasks")
def list_tasks():
    ...

但很多人在刚接触装饰器时,只是记住了写法,却不理解为什么函数上面加一行 @xxx,函数就"增强"了。真正的答案藏在 Python 的高阶函数机制中。

这一篇我们会解决以下问题:

复制代码
函数为什么能赋值给变量?
函数为什么能作为参数传入另一个函数?
函数为什么能作为返回值返回?
什么是 lambda?
什么是闭包?
为什么装饰器依赖高阶函数和闭包?
functools 中的 partial、wraps、lru_cache 有什么用?

这些知识不只是 Python 语法技巧。在 FastAPI、LangChain、回调函数、权限校验、日志记录、缓存、任务处理等真实项目中都会反复出现。

二、建立核心概念:Python 中函数是一等对象

在 Python 中,函数不仅仅是一段"可以执行的代码"。函数可以像字符串、列表、字典一样:

  1. 赋值给变量
  2. 作为参数传给另一个函数
  3. 作为另一个函数的返回值
  4. 放入列表或字典中

这种特性通常称为:函数是一等对象。先看一个普通函数:

python 复制代码
def greet(name):
    return f"你好,{name}"
python 复制代码
message = greet("kankan")
print(message)
复制代码
你好,kankan

对于:

复制代码
greet("kankan")

表示立即执行 greet 函数。而如果只写:

复制代码
greet

没有括号,表示这个函数对象本身。例如:

python 复制代码
def greet(name):
    return f"你好,{name}"


say_hello = greet

print(say_hello("kankan"))
python 复制代码
你好,kankan

这里发生的事情是:

复制代码
greet      → 原函数对象
say_hello  → 指向同一个函数对象的新变量名

所以:

python 复制代码
print(greet)
print(say_hello)

会发现它们都指向同一个函数。请记住这个特别重要的区别:

复制代码
greet      # 函数本身,不执行
greet()    # 调用函数,立即执行

这和 FastAPI 中的写法很像:

python 复制代码
@app.get("/tasks")
def list_tasks():
    ...

FastAPI 需要的是下面函数本身,才能在用户请求 /tasks 时再调用它。

三、函数作为参数:把"行为"传进函数

我们平时传参数,常见的是传字符串、整数、列表:

python 复制代码
def print_name(name):
    print(name)


print_name("kankan")

但 Python 允许把函数也作为参数传进去。看一个简单例子:

python 复制代码
def add(a, b):
    return a + b


def multiply(a, b):
    return a * b

def calculate(operation, a, b):
    return operation(a, b)

这里:

复制代码
operation

不是数字,也不是字符串,而是一个函数参数。调用时:

python 复制代码
result1 = calculate(add, 3, 5)
result2 = calculate(multiply, 3, 5)

print(result1)
print(result2)

输出:

复制代码
8
15

执行过程如下:

复制代码
calculate(add, 3, 5)
    ↓
operation 指向 add
    ↓
operation(3, 5)
    ↓
等价于 add(3, 5)

这就是高阶函数最基础的形式:

接收函数作为参数的函数,叫做高阶函数。

四、真实例子:根据不同规则排序任务

假设有一组任务:

python 复制代码
tasks = [
    {"title": "学习 FastAPI", "priority": 2, "done": False},
    {"title": "完成 Docker 笔记", "priority": 1, "done": True},
    {"title": "学习 SQLAlchemy", "priority": 3, "done": False},
]

如果你想按任务优先级排序,可以写:

python 复制代码
sorted_tasks = sorted(tasks, key=lambda task: task["priority"])

for task in sorted_tasks:
    print(task["title"], task["priority"])

输出:

复制代码
完成 Docker 笔记 1
学习 FastAPI 2
学习 SQLAlchemy 3

这里sorted(...)就是一个高阶函数,它的第一个参数是要排序的列表:tasks;它的 key 参数接收一个函数:

复制代码
lambda task: task["priority"]

这个函数会告诉 sorted():

每一条任务应该根据哪个值进行排序?

对于第一条任务:

复制代码
{"title": "学习 FastAPI", "priority": 2, "done": False}

执行:

复制代码
lambda task: task["priority"]

2

因此 sorted() 会按照每条任务的 priority 值排序。如果改成按标题长度排序:

python 复制代码
sorted_tasks = sorted(
    tasks,
    key=lambda task: len(task["title"])
)

如果想让未完成任务排在前面:

python 复制代码
sorted_tasks = sorted(
    tasks,
    key=lambda task: task["done"]
)

因为布尔值可以参与排序:

复制代码
False 会排在 True 前面

所以未完成任务会排在前面。这在真实项目中非常常见。例如:

复制代码
按创建时间排序
按优先级排序
按价格排序
按点赞数排序
按相似度排序
按模型评分排序

看到这里大家可能对lambda这个函数不太了解,没关系,下一步我们来进入lambda匿名函数的学习。

五、什么是 lambda 匿名函数

python 复制代码
lambda task: task["priority"]

这就是 lambda 表达式,也常被称为匿名函数。

普通函数写法:

python 复制代码
def get_priority(task):
    return task["priority"]
python 复制代码
sorted_tasks = sorted(tasks, key=get_priority)

lambda 写法:

python 复制代码
sorted_tasks = sorted(
    tasks,
    key=lambda task: task["priority"]
)

这两种效果是相同的。lambda 的基本语法:

复制代码
lambda 参数: 返回表达式

例如:

复制代码
add = lambda a, b: a + b

print(add(3, 5))

8

这相当于:

python 复制代码
def add(a, b):
    return a + b

但 lambda 更适合短小、只使用一次的逻辑。

例如:

python 复制代码
names = ["Tom", "kankan", "Alice"]

result = sorted(names, key=lambda name: len(name))

print(result)

输出:

复制代码
['Tom', 'Alice', 'kankan']

这里的原因就是某些地方没有必要专门定义一个 get_name_length() 函数,因为排序规则只用一次。所以就有了匿名函数这种更加轻便、简洁的写法。需要注意,lambda 只能写一个表达式,不能写多行复杂逻辑。

不适合这样写:

python 复制代码
lambda x:
    if x > 0:
        return x

当逻辑复杂、有多个判断、需要可读性时,应该使用普通 def 函数。

六、常见高阶函数:map、filter 与 reduce

6.1 map:对每个元素执行相同转换

假设有一组任务标题:

复制代码
titles = ["fastapi", "sqlalchemy", "docker"]

想把所有标题改成大写:

python 复制代码
upper_titles = map(str.upper, titles)

print(list(upper_titles))
复制代码
['FASTAPI', 'SQLALCHEMY', 'DOCKER']

这里的str.upper是字符串的转大写方法。也可以配合 lambda:

python 复制代码
numbers = [1, 2, 3, 4]

result = map(lambda number: number * 2, numbers)

print(list(result))
复制代码
[2, 4, 6, 8]

6.2 filter:筛选符合条件的数据

假设要筛选所有未完成任务:

python 复制代码
tasks = [
    {"title": "学习 FastAPI", "done": False},
    {"title": "学习 MySQL", "done": True},
    {"title": "学习 Docker", "done": False},
]

可以写:

python 复制代码
unfinished_tasks = filter(
    lambda task: not task["done"],
    tasks,
)

print(list(unfinished_tasks))
python 复制代码
[
    {"title": "学习 FastAPI", "done": False},
    {"title": "学习 Docker", "done": False},
]

6.3 reduce:把多个值逐步累计成一个值

例如计算订单金额:

python 复制代码
from functools import reduce

prices = [99, 199, 49]

total = reduce(lambda current, price: current + price, prices)

print(total)
复制代码
347

它的过程是:

复制代码
第一次:99 + 199 = 298
第二次:298 + 49 = 347

不过在现代 Python 中,如果逻辑只是遍历、转换、筛选,很多时候列表推导式可读性更好。

例如 map:

python 复制代码
result = list(map(lambda number: number * 2, numbers))

通常可以写成:

python 复制代码
result = [number * 2 for number in numbers]

例如 filter:

python 复制代码
unfinished_tasks = list(
    filter(lambda task: not task["done"], tasks)
)

通常也可以写成:

python 复制代码
unfinished_tasks = [
    task
    for task in tasks
    if not task["done"]
]

经验是:

复制代码
逻辑简单、数据转换直观:优先列表推导式
需要把函数作为参数动态传递:高阶函数更自然

七、函数作为返回值:创建"功能工厂"

高阶函数的第二种核心形式是:

一个函数返回另一个函数。

先看例子:

python 复制代码
def create_greeter(prefix):
    def greet(name):
        return f"{prefix},{name}"

    return greet

调用:

python 复制代码
morning_greeter = create_greeter("早上好")
evening_greeter = create_greeter("晚上好")

print(morning_greeter("kankan"))
print(evening_greeter("kankan"))
复制代码
早上好,kankan
晚上好,kankan

执行过程是:

python 复制代码
create_greeter("早上好")
    ↓
创建内部 greet 函数
    ↓
内部函数记住 prefix = "早上好"
    ↓
返回 greet 函数
    ↓
morning_greeter 指向这个函数

这类"根据配置创建一个新函数"的模式,可以理解为函数工厂。

真实项目中常见的用途包括:

python 复制代码
创建不同权限校验函数
创建不同日志格式函数
创建不同重试策略函数
创建不同数据处理规则函数
创建不同模型调用配置函数

八、闭包:函数为什么能记住外层变量

上面的 create_greeter() 中,内部函数:

复制代码
def greet(name):
    return f"{prefix},{name}"

使用了外层函数的变量:prefix,即使 create_greeter() 已经执行结束,内部函数仍然记得这个变量。这就是闭包。闭包可以简单理解为:

内部函数引用了外部函数的变量,并且在外部函数结束后仍然保留这些变量的访问能力。

再看一个计数器:

python 复制代码
def create_counter():
    count = 0

    def increment():
        nonlocal count
        count += 1
        return count

    return increment
python 复制代码
counter = create_counter()

print(counter())
print(counter())
print(counter())
复制代码
1
2
3

为什么每次调用都能继续累加?

因为:counter保存的是内部函数 increment,而这个内部函数一直记得:count

这里的nonlocal count表示:

我想修改外层函数中的 count,不是在内部重新创建一个同名局部变量。

如果不写 nonlocal:

python 复制代码
def increment():
    count += 1

Python 会认为 count 是内部的新局部变量,但它还没有值,就会报错。

九、闭包的实际例子:生成不同权限检查函数

假设系统中有普通用户和管理员:

python 复制代码
user = {
    "username": "kankan",
    "role": "user",
}

可以通过闭包创建权限检查函数:

python 复制代码
def require_role(required_role):
    def check(user):
        if user["role"] != required_role:
            raise PermissionError("权限不足")

        return True

    return check

创建两个不同的规则:

python 复制代码
require_admin = require_role("admin")
require_user = require_role("user")

调用:

python 复制代码
require_user(user)

print("普通用户验证通过")

如果普通用户尝试管理员权限:

复制代码
require_admin(user)

就会抛出:

复制代码
PermissionError: 权限不足

这里:

复制代码
require_role("admin")

返回的 check() 函数记住了:

复制代码
required_role = "admin"

这和后面带参数的装饰器是同一套底层思路。

十、闭包常见陷阱:循环变量的延迟绑定

看下面的代码:

python 复制代码
functions = []

for number in range(3):
    def show():
        print(number)

    functions.append(show)

for func in functions:
    func()

很多人预期输出:

复制代码
0
1
2

但实际可能得到:

复制代码
2
2
2

原因是三个 show() 函数都引用同一个外部变量:number

循环结束后:

复制代码
number == 2

所以三个函数最后看到的都是 2。

常见解决方法是使用默认参数保存当前值:

python 复制代码
functions = []

for number in range(3):
    def show(current_number=number):
        print(current_number)

    functions.append(show)

for func in functions:
    func()
复制代码
0
1
2

这里:

复制代码
current_number=number

会在每轮循环创建函数时,把当前 number 的值保存下来。

十一、*args 与 **kwargs:让函数接收不确定数量的参数

你在装饰器和框架源码中经常会看到:

复制代码
def wrapper(*args, **kwargs):
    ...

先看 *args:

python 复制代码
def add_all(*args):
    return sum(args)
python 复制代码
print(add_all(1, 2))
print(add_all(1, 2, 3, 4, 5))
复制代码
3
15

这里:*args会把多个位置参数收集成一个元组。

例如:

复制代码
add_all(1, 2, 3)

函数内部的:args大致是(1,2,3)。

再看 **kwargs:

python 复制代码
def print_user(**kwargs):
    for key, value in kwargs.items():
        print(f"{key}: {value}")

调用:

python 复制代码
print_user(
    username="kankan",
    role="admin",
    active=True,
)
python 复制代码
username: kankan
role: admin
active: True

这里:**kwargs会把多个关键字参数收集为字典。

python 复制代码
{
    "username": "kankan",
    "role": "admin",
    "active": True,
}

11.1 为什么装饰器中几乎总有 *args, **kwargs

假设你有不同参数形式的函数:

python 复制代码
def greet(name):
    return f"你好,{name}"


def add(a, b):
    return a + b

如果装饰器只接收一个参数:

python 复制代码
def wrapper(name):
    ...

它只能包装类似 greet(name) 的函数,无法包装:

复制代码
add(a, b)

但是:

python 复制代码
def wrapper(*args, **kwargs):
    return func(*args, **kwargs)

可以兼容绝大多数函数调用。

例如:

复制代码
func(*args, **kwargs)

会把收集到的参数重新传给原函数。

因此:

复制代码
greet("kankan")

会被正确转发。

复制代码
add(3, 5)

也会被正确转发。这就是装饰器常见固定写法的原因。

十二、functools.partial:提前固定一部分参数

partial 的意思是"部分应用"。

例如你有一个计算折扣的函数:

python 复制代码
def calculate_price(price, discount):
    return price * (1 - discount)

正常调用:

复制代码
print(calculate_price(100, 0.2))

80.0

如果你的商城经常使用 20% 折扣,可以提前固定:

python 复制代码
from functools import partial

vip_price = partial(calculate_price, discount=0.2)

现在:

复制代码
print(vip_price(100))
print(vip_price(200))

输出:

复制代码
80.0
160.0

这里:vip_price是一个新函数,它已经记住:discount=0.2。

在后端项目中,partial 常用于:

预先固定配置

预先固定超时时间

预先固定模型名称

预先固定日志前缀

预先固定某个权限规则

12.1 functools.lru_cache:缓存重复计算结果

假设有一个很耗时的递归函数:

python 复制代码
def fibonacci(number):
    if number <= 1:
        return number

    return fibonacci(number - 1) + fibonacci(number - 2)

计算:fibonacci(35)会进行很多重复计算。这时候可以使用:

python 复制代码
from functools import lru_cache


@lru_cache
def fibonacci(number):
    if number <= 1:
        return number

    return fibonacci(number - 1) + fibonacci(number - 2)

@lru_cache 的意思是:

保存函数以前计算过的结果;下次参数相同,就直接从缓存取结果。

例如第一次调用:fibonacci(30)需要计算。第二次再调用:fibonacci(30)会直接使用缓存结果。在项目中,它适合缓存不常变化、重复计算、且输入可哈希的数据,例如:

配置读取

静态规则计算

重复的复杂转换

模型或客户端初始化

**但不要不加判断地给数据库查询加 lru_cache。**例如:

复制代码
@lru_cache
def get_user(user_id):
    ...

如果用户信息已经修改,缓存可能返回旧数据。缓存的核心问题从来不是"能不能加",而是:

数据变化后,旧缓存何时失效?

十三、装饰器的本质:高阶函数 + 闭包

现在我们可以正式拆解装饰器。先写一个记录函数执行时间的装饰器:(在这里我想解释一下,可能很多同学刚接触python的高阶函数语法的时候可能会搞不懂装饰器这个概念,不知道它到底是干什么用的,大家此时就可以先来理解我的一个意图:给任何一个函数加上一个计数器,用来计算函数完成的时间,那么一般情况下就是在函数内部增加计时起始时间和末尾时间,但是这样的话岂不是每一个函数都要写?就带来了不方便,这个时候就出现了装饰器,我们可以制造一个统一的函数来包装我们要运行的函数,不改变函数内部的逻辑,而是在函数执行前后增加计时,这样也就解耦了业务和附加功能,在之后的实际工程项目中,解耦也是非常重要的,它能够很方便快捷的删除或者新增我们想要的逻辑,比如计时、日志等等)

python 复制代码
from functools import wraps
from time import perf_counter


def log_execution_time(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = perf_counter()

        result = func(*args, **kwargs)

        end = perf_counter()

        print(f"{func.__name__} 执行耗时:{end - start:.6f} 秒")

        return result

    return wrapper

wraps是用来保存原函数的名称不被忘记,wrapper相当于装饰器的壳子函数,里面传入的就是我们真正运行的函数参数,这也就是为什么传入的是(*args, **kwargs),保证任何形式的参数都能够传入进行,result接收真正的函数执行结果,return wrapper是返回壳函数对象。

使用:

python 复制代码
@log_execution_time
def calculate_total(prices):
    return sum(prices)

调用:

python 复制代码
result = calculate_total([99, 199, 49])

print(result)
复制代码
calculate_total 执行耗时:0.000002 秒
347

我们一步步理解。

第一步:

复制代码
def log_execution_time(func):

装饰器接收一个函数作为参数。

第二步:

复制代码
def wrapper(*args, **kwargs):

在内部定义一个新函数,用于包装原函数。

第三步:

复制代码
result = func(*args, **kwargs)

真正调用原函数。

第四步:

复制代码
return wrapper

把包装后的新函数返回。

第五步:

复制代码
@log_execution_time
def calculate_total(prices):
    ...

本质上等价于:

python 复制代码
def calculate_total(prices):
    return sum(prices)


calculate_total = log_execution_time(calculate_total)

因此之后调用:

复制代码
calculate_total([99, 199, 49])

实际上先调用的是:

复制代码
wrapper([99, 199, 49])

而 wrapper 再调用原来的 calculate_total()。这就是"在不修改原函数业务逻辑的前提下,为函数增加额外能力"。

13.1 为什么必须使用 @wraps

如果不写:@wraps(func)那么包装后的函数名称会变成:wrapper。例如:print(calculate_total.name)可能输出:wrapper。

这会影响:

日志

调试

接口文档

错误追踪

依赖注入框架

加入:

复制代码
@wraps(func)

后,Python 会尽可能保留原函数的名称、文档说明等元信息。

因此装饰器的推荐模板是:

python 复制代码
from functools import wraps


def decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)

    return wrapper

13.2 带参数的装饰器:权限控制示例

有时装饰器自身也需要接收参数。

例如:

复制代码
@require_role("admin")
def delete_user():
    ...

实现如下:

python 复制代码
from functools import wraps


def require_role(required_role):
    def decorator(func):
        @wraps(func)
        def wrapper(user, *args, **kwargs):
            if user["role"] != required_role:
                raise PermissionError("权限不足")

            return func(user, *args, **kwargs)

        return wrapper

    return decorator

使用:

python 复制代码
@require_role("admin")
def delete_task(user, task_id):
    return f"用户 {user['username']} 删除了任务 {task_id}"

管理员调用:

python 复制代码
admin = {
    "username": "admin",
    "role": "admin",
}

print(delete_task(admin, 1))
python 复制代码
用户 admin 删除了任务 1

普通用户调用:

python 复制代码
normal_user = {
    "username": "kankan",
    "role": "user",
}

delete_task(normal_user, 1)
python 复制代码
PermissionError: 权限不足

这里一共出现了三层函数:

复制代码
require_role("admin")
    ↓
decorator(func)
    ↓
wrapper(*args, **kwargs)

**这正是"函数返回函数"和"闭包"的组合。**需要注意:在 FastAPI 项目中,认证与权限通常更推荐使用:

复制代码
Depends(get_current_user)

而不是自己写这种简单装饰器,因为 FastAPI 的依赖注入、请求参数解析、接口文档生成会更自然。但理解装饰器机制,依然是看懂框架和源码的重要基础。

十四、小项目:给任务操作增加日志、耗时统计和权限检查

下面用一个小型任务操作系统,把今天的知识串起来。

python 复制代码
from functools import wraps
from time import perf_counter


tasks = [
    {"id": 1, "title": "学习 FastAPI", "done": False},
    {"id": 2, "title": "学习 SQLAlchemy", "done": False},
]


def log_operation(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"[日志] 正在执行:{func.__name__}")

        result = func(*args, **kwargs)

        print(f"[日志] 执行完成:{func.__name__}")

        return result

    return wrapper


def measure_time(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = perf_counter()

        result = func(*args, **kwargs)

        end = perf_counter()

        print(f"[耗时] {func.__name__}:{end - start:.6f} 秒")

        return result

    return wrapper


def require_admin(func):
    @wraps(func)
    def wrapper(user, *args, **kwargs):
        if user["role"] != "admin":
            raise PermissionError("只有管理员可以删除任务")

        return func(user, *args, **kwargs)

    return wrapper


@log_operation
@measure_time
def list_tasks():
    return sorted(tasks, key=lambda task: task["id"])


@log_operation
@require_admin
def delete_task(user, task_id):
    global tasks

    tasks = [
        task
        for task in tasks
        if task["id"] != task_id
    ]

    return f"任务 {task_id} 已删除"

普通查看任务:

python 复制代码
print(list_tasks())

管理员删除任务:

python 复制代码
admin = {
    "username": "admin",
    "role": "admin",
}

print(delete_task(admin, 1))

这里你能看到:

复制代码
高阶函数:装饰器接收函数
闭包:wrapper 记住 func
lambda:按任务 id 排序
列表推导式:筛选出未被删除的任务
多个装饰器:日志、耗时、权限各自独立

这就是 Python 高阶语法在真实业务中的样子。

十五、常见误区总结

15.1 忘记函数名和函数调用的区别

复制代码
func      # 函数对象
func()    # 执行函数

15.2 lambda 不是任何场景都更高级,逻辑复杂时,普通函数更清晰:

复制代码
def calculate_score(user):
    ...

通常比一长串 lambda 更容易维护。

15.3 闭包不要随意修改外层变量

需要修改外层变量时,要理解:nonlocal的作用,避免变量作用域混乱。

  1. 装饰器要使用 @wraps

否则会影响函数名称、调试信息和框架识别。

  1. 缓存不是万能优化

缓存能减少重复计算,但也可能带来旧数据问题。

本文总结:

这一篇的主线是:

复制代码
函数是一等对象
    ↓
函数可以作为参数
    ↓
函数可以作为返回值
    ↓
内部函数可以记住外部变量
    ↓
形成闭包
    ↓
高阶函数 + 闭包
    ↓
形成装饰器

你现在应该能够理解:

复制代码
@app.get("/tasks")
@lru_cache
@wraps(func)

这些写法不是"特殊魔法",而是建立在 Python 函数能力之上的正常语法。

下一篇我们会学习另一个在后端中非常重要的能力:

复制代码
with
__enter__
__exit__
contextmanager
yield
async with

它们会解释为什么文件、数据库连接、HTTP 客户端、模型资源都需要被正确打开和关闭。

相关推荐
xuxigifxfh5 分钟前
HJ5 进制转换
java·开发语言·华为机考
denlaku6 分钟前
JDK 12 新特性详解
java·开发语言
Nebula_g11 分钟前
JavaSE拓展:可变参数
java·开发语言·算法·安全·javase·可变参数
溪语流沙13 分钟前
【Web全栈进阶】Alembic数据库迁移:改表结构不再删库
前端·数据库·python·elasticsearch·postgresql
Evan_Lai29 分钟前
(一)独热编码、标签编码、目标编码、序数编码详解
python·机器学习
用户76241175670530 分钟前
第二章 Python语言基础
python
129Lab31 分钟前
BMS 核心算法:SOC 估算从安时积分到 LSTM-UKF 串级融合的 30 年演进(附 Python 实现)
python·算法·lstm·python3.11·bms·soc估算
129Lab39 分钟前
电池热管理仿真的AI加速:用Python+PINN物理信息神经网络替代传统CFD的可行性探索
pytorch·python·cfd·pinn·物理信息神经网络·仿真加速·电池热管理
anew___1 小时前
《从零手写操作系统 (30):环境变量与进程上下文——export/unset与继承语义》
java·开发语言·网络·jvm·算法
用户298698530141 小时前
Python 中 HTML 转 PDF 的实现方法
python·html·api