引言
函数是编程中最基本的抽象单元。很多 Python 初学者写函数时,只关注"把一段逻辑包起来",却对函数的三个核心机制一知半解:参数到底是怎么传进去的?为什么函数里修改一个变量有时影响外面、有时不影响?闭包到底是什么、为什么装饰器离不开它?
这些问题不搞清楚,写复杂函数时就会频繁踩坑,也很难理解装饰器、偏函数、回调等进阶概念。本文将从底层原理出发,系统讲解 Python 函数的参数传递机制、作用域规则和闭包的本质,帮你把函数彻底搞透。
一、参数传递:既不是传值,也不是传引用
1.1 一个经典困惑
先看两段代码,你能预测结果吗?
# 代码 A
def func(x):
x = 100
a = 1
func(a)
print(a) # 输出什么?
# 代码 B
def func(lst):
lst.append(4)
my_list = [1, 2, 3]
func(my_list)
print(my_list) # 输出什么?
答案:代码 A 输出 1,代码 B 输出 [1, 2, 3, 4]。
同样是把变量传进函数,为什么整数没被改变,列表却被改变了?这就是 Python 参数传递机制的核心问题。
1.2 Python 的传递方式:传对象引用(Pass by Object Reference)
很多教材会说 Python 是"传值"或"传引用",其实都不准确。准确的说法是传对象引用(也叫"传对象"或"赋值传递")。
它的规则只有一条:函数调用时,实参对象的引用被赋值给形参变量,形参和实参指向同一个对象。
结合我们之前讲过的"变量是标签"模型,就很好理解了:
def func(x):
# x 这个新标签,贴到了实参指向的同一个对象上
print(id(x)) # 和外面的 a 同一个 id
a = 1
print(id(a))
func(a)
关键在于对象本身是可变还是不可变:
- 如果对象是不可变的(int、str、tuple 等),函数里对形参重新赋值,只是让形参标签贴到新对象上,实参标签不受影响。
- 如果对象是可变的(list、dict、set 等),函数里通过形参修改对象内容(如
append),因为是同一个对象,实参看到的也会变化。
# 不可变对象:重新赋值不影响外部
def func1(x):
x = x + 1 # 创建新对象 2,x 贴到新对象
print("函数内 x =", x) # 2
a = 1
func1(a)
print("外部 a =", a) # 1
# 可变对象:原地修改影响外部
def func2(lst):
lst.append(4) # 修改同一个列表对象
print("函数内 lst =", lst) # [1, 2, 3, 4]
my_list = [1, 2, 3]
func2(my_list)
print("外部 my_list =", my_list) # [1, 2, 3, 4]
# 可变对象:如果是重新赋值,则不影响外部
def func3(lst):
lst = [4, 5, 6] # 创建新列表,lst 贴到新对象
print("函数内 lst =", lst) # [4, 5, 6]
my_list = [1, 2, 3]
func3(my_list)
print("外部 my_list =", my_list) # [1, 2, 3]
记住这个判断标准:看函数里对形参做的是"重新赋值"还是"原地修改"。重新赋值永远不影响外部;原地修改只有可变对象才影响外部。
1.3 如何避免函数意外修改外部数据
如果不希望函数修改传入的可变对象,传入副本即可:
def func(lst):
lst.append(4)
my_list = [1, 2, 3]
func(my_list.copy()) # 传入浅拷贝
# 或 func(my_list[:])
print(my_list) # [1, 2, 3],不受影响
对于嵌套结构,需要 copy.deepcopy()。
二、参数类型全解析
Python 的函数参数非常灵活,掌握各种参数类型是写出优雅函数的基础。
2.1 位置参数与关键字参数
def greet(name, age):
print(f"{name} 今年 {age} 岁")
# 位置参数:按顺序传递
greet("小明", 18)
# 关键字参数:按名字传递,顺序可换
greet(age=20, name="小红")
# 混合使用:位置参数必须在前
greet("小刚", age=22)
# greet(name="小李", 25) # 语法错误,关键字参数不能在位置参数前
2.2 默认参数
def greet(name, age=18, city="北京"):
print(f"{name} 今年 {age} 岁,住在 {city}")
greet("小明") # 小明 今年 18 岁,住在 北京
greet("小红", 20) # 小红 今年 20 岁,住在 北京
greet("小刚", city="上海") # 小刚 今年 18 岁,住在 上海
经典坑点:可变对象作为默认参数
# 错误写法
def add_item(item, lst=[]):
lst.append(item)
return lst
print(add_item(1)) # [1]
print(add_item(2)) # [1, 2],不是预期的 [2]!
print(add_item(3)) # [1, 2, 3]
原因:默认参数在函数定义时只创建一次,多次调用共享同一个对象。正确写法:
def add_item(item, lst=None):
if lst is None:
lst = []
lst.append(item)
return lst
print(add_item(1)) # [1]
print(add_item(2)) # [2]
2.3 可变参数:*args 和 **kwargs
# *args:收集任意数量的位置参数为元组
def sum_all(*args):
print(args) # 是一个元组
return sum(args)
print(sum_all(1, 2, 3)) # 6
print(sum_all(1, 2, 3, 4, 5)) # 15
# **kwargs:收集任意数量的关键字参数为字典
def print_info(**kwargs):
print(kwargs) # 是一个字典
for key, value in kwargs.items():
print(f"{key}: {value}")
print_info(name="小明", age=18, city="北京")
*args 和 **kwargs 可以组合使用,顺序必须是:位置参数 → *args → 默认参数 → **kwargs。
def demo(a, b, *args, c=10, **kwargs):
print(f"a={a}, b={b}, args={args}, c={c}, kwargs={kwargs}")
demo(1, 2, 3, 4, c=20, x=100, y=200)
# a=1, b=2, args=(3, 4), c=20, kwargs={'x': 100, 'y': 200}
2.4 参数解包
调用函数时,* 和 ** 可以把可迭代对象/字典"解包"成参数:
def add(a, b, c):
return a + b + c
# 列表/元组解包为位置参数
nums = [1, 2, 3]
print(add(*nums)) # 6,等价于 add(1, 2, 3)
# 字典解包为关键字参数
params = {"a": 1, "b": 2, "c": 3}
print(add(**params)) # 6
2.5 强制位置参数与强制关键字参数(Python 3.8+)
# / 之前的参数只能按位置传递
def func(a, b, /, c, d):
print(a, b, c, d)
func(1, 2, 3, 4) # 正确
func(1, 2, c=3, d=4) # 正确
# func(a=1, b=2, c=3, d=4) # 错误,a 和 b 不能用关键字
# * 之后的参数只能按关键字传递
def func(a, b, *, c, d):
print(a, b, c, d)
func(1, 2, c=3, d=4) # 正确
# func(1, 2, 3, 4) # 错误,c 和 d 必须用关键字
这两个语法让 API 设计更清晰,防止调用方用错误的方式传参。
三、作用域:LEGB 规则
3.1 什么是作用域
作用域决定了变量在哪些地方可以被访问。Python 中有四种作用域,按查找优先级从高到低排列:
| 字母 | 作用域 | 说明 |
|---|---|---|
| L | Local(局部) | 函数内部 |
| E | Enclosing(嵌套) | 外层函数的局部作用域 |
| G | Global(全局) | 模块顶层 |
| B | Built-in(内置) | Python 内置命名空间 |
这就是著名的 LEGB 规则:查找变量时,从 L → E → G → B 依次查找,找到第一个就停止,都找不到则报 NameError。
x = "global" # G:全局作用域
def outer():
x = "enclosing" # E:嵌套作用域
def inner():
x = "local" # L:局部作用域
print(x) # 找到 L 中的 x,输出 "local"
inner()
outer()
把 inner 里的 x = "local" 注释掉,就会找到 E 中的 "enclosing";再注释掉 E 中的,就会找到 G 中的 "global"。
3.2 全局变量与 global 关键字
函数内部可以读取全局变量,但默认不能修改全局变量——赋值会被当作创建局部变量:
count = 0
def increment():
count = count + 1 # UnboundLocalError! Python 认为 count 是局部变量,但还没赋值就用了
increment()
要在函数内修改全局变量,需要用 global 声明:
count = 0
def increment():
global count
count = count + 1
increment()
print(count) # 1
3.3 嵌套作用域与 nonlocal 关键字
类似地,在内层函数修改外层函数的变量,需要用 nonlocal:
def outer():
count = 0
def inner():
nonlocal count
count += 1
return count
return inner
counter = outer()
print(counter()) # 1
print(counter()) # 2
print(counter()) # 3
nonlocal 声明的变量会沿着嵌套作用域向上查找(不包括全局),找到第一个匹配的变量。
3.4 作用域的常见坑
# 坑 1:闭包中的延迟绑定
def create_multipliers():
return [lambda x: i * x for i in range(5)]
multipliers = create_multipliers()
print(multipliers[0](2)) # 8,不是 0!
print(multipliers[2](2)) # 8,不是 4!
# 所有 lambda 共享同一个 i,循环结束后 i = 4
# 解决方案:用默认参数捕获当前值
def create_multipliers():
return [lambda x, i=i: i * x for i in range(5)]
multipliers = create_multipliers()
print(multipliers[0](2)) # 0
print(multipliers[2](2)) # 4
四、闭包:捕获了环境的函数
4.1 什么是闭包
闭包(Closure)是指一个函数对象,它记住了定义时所在的作用域环境,即使在外部调用,也能访问那个作用域中的变量。
通俗地说:闭包 = 函数 + 它捕获的自由变量。
def outer(x):
def inner(y):
return x + y # x 是外层函数的变量,被 inner 捕获
return inner
add5 = outer(5) # add5 是一个闭包,x 被固定为 5
print(add5(3)) # 8
print(add5(10)) # 15
add10 = outer(10)
print(add10(3)) # 13
inner 函数引用了外层函数 outer 的参数 x。当 outer 执行完毕返回 inner 后,按常理 x 应该被销毁,但闭包让 x 存活了下来——add5 和 add10 各自保存了不同的 x 值。
4.2 闭包的本质:closure 属性
我们可以通过 __closure__ 属性查看闭包捕获的变量:
def outer(x):
def inner(y):
return x + y
return inner
add5 = outer(5)
print(add5.__closure__) # (<cell at 0x...: int object at 0x...>,)
print(add5.__closure__[0].cell_contents) # 5,捕获的 x 值
每个被捕获的变量被包装成一个 cell 对象,存在 __closure__ 元组中。这就是闭包"记住环境"的底层实现。
4.3 闭包的典型应用
应用 1:工厂函数(生成配置好的函数)
def make_multiplier(factor):
def multiply(n):
return n * factor
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
应用 2:计数器(状态保持)
def make_counter():
count = 0
def counter():
nonlocal count
count += 1
return count
return counter
c1 = make_counter()
print(c1()) # 1
print(c1()) # 2
print(c1()) # 3
c2 = make_counter() # 独立的计数器
print(c2()) # 1
闭包让 count 变量变成了"私有状态",外部无法直接访问,只能通过返回的函数操作——这其实就是面向对象中封装的一种轻量实现。
应用 3:装饰器的基础
Python 装饰器本质上就是一个闭包。理解了闭包,装饰器就不再神秘:
def log_decorator(func):
def wrapper(*args, **kwargs):
print(f"调用函数: {func.__name__}")
result = func(*args, **kwargs)
print(f"函数 {func.__name__} 执行完毕")
return result
return wrapper
@log_decorator
def add(a, b):
return a + b
print(add(1, 2))
# 调用函数: add
# 函数 add 执行完毕
# 3
wrapper 捕获了外层的 func,形成闭包。装饰器的详细内容我们会在后续文章中专门讲解。
4.4 闭包 vs 类:什么时候用哪个?
闭包和类都能实现"状态 + 行为"的封装。选择原则:
- 状态简单、只有一个主要行为 → 用闭包,更轻量
- 状态复杂、有多个方法/行为 → 用类,更清晰
# 闭包版本:简单计数器
def make_counter():
count = 0
def counter():
nonlocal count
count += 1
return count
return counter
# 类版本:支持更多操作的计数器
class Counter:
def __init__(self):
self.count = 0
def increment(self):
self.count += 1
return self.count
def reset(self):
self.count = 0
def get_value(self):
return self.count
五、最佳实践总结
- 理解参数传递是传对象引用:判断是否影响外部,看是重新赋值还是原地修改,以及对象是否可变。
- 默认参数不用可变对象:用
None作为哨兵值,在函数体内初始化。 - 善用
*args和**kwargs:让函数更灵活,但不要滥用,参数清晰优先。 - 减少全局变量的使用:全局变量增加耦合、降低可读性,尽量通过参数传递和返回值通信。
global和nonlocal谨慎使用:它们打破了作用域的清晰边界,只在确实需要时使用。- 闭包用于轻量级状态封装:简单场景用闭包,复杂场景用类。
- 注意闭包的延迟绑定问题:循环中创建闭包时,用默认参数捕获当前值。
结语
函数的三个核心机制——参数传递、作用域、闭包——是层层递进的:参数传递决定了数据如何进出函数,作用域决定了变量在哪里可见,而闭包则是作用域的"延伸",让函数带着自己的环境行走天下。
理解了这三者,你不仅能避开大多数函数相关的 bug,还能轻松掌握装饰器、偏函数、回调函数等进阶概念。

971

被折叠的 条评论
为什么被折叠?



