一、前言
在前面的 Python 学习中,我们已经接触过函数、类、装饰器,以及 FastAPI 中常见的:
python
@app.get("/tasks")
def list_tasks():
...
但很多人在刚接触装饰器时,只是记住了写法,却不理解为什么函数上面加一行 @xxx,函数就"增强"了。真正的答案藏在 Python 的高阶函数机制中。
这一篇我们会解决以下问题:
函数为什么能赋值给变量?
函数为什么能作为参数传入另一个函数?
函数为什么能作为返回值返回?
什么是 lambda?
什么是闭包?
为什么装饰器依赖高阶函数和闭包?
functools 中的 partial、wraps、lru_cache 有什么用?
这些知识不只是 Python 语法技巧。在 FastAPI、LangChain、回调函数、权限校验、日志记录、缓存、任务处理等真实项目中都会反复出现。
二、建立核心概念:Python 中函数是一等对象
在 Python 中,函数不仅仅是一段"可以执行的代码"。函数可以像字符串、列表、字典一样:
- 赋值给变量
- 作为参数传给另一个函数
- 作为另一个函数的返回值
- 放入列表或字典中
这种特性通常称为:函数是一等对象。先看一个普通函数:
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的作用,避免变量作用域混乱。
- 装饰器要使用
@wraps否则会影响函数名称、调试信息和框架识别。
- 缓存不是万能优化
缓存能减少重复计算,但也可能带来旧数据问题。
本文总结:
这一篇的主线是:
函数是一等对象
↓
函数可以作为参数
↓
函数可以作为返回值
↓
内部函数可以记住外部变量
↓
形成闭包
↓
高阶函数 + 闭包
↓
形成装饰器
你现在应该能够理解:
@app.get("/tasks")
@lru_cache
@wraps(func)
这些写法不是"特殊魔法",而是建立在 Python 函数能力之上的正常语法。
下一篇我们会学习另一个在后端中非常重要的能力:
with
__enter__
__exit__
contextmanager
yield
async with
它们会解释为什么文件、数据库连接、HTTP 客户端、模型资源都需要被正确打开和关闭。