Python 函数进阶:参数传递、作用域与闭包的底层逻辑

引言

函数是编程中最基本的抽象单元。很多 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 中有四种作用域,按查找优先级从高到低排列:

字母作用域说明
LLocal(局部)函数内部
EEnclosing(嵌套)外层函数的局部作用域
GGlobal(全局)模块顶层
BBuilt-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 存活了下来——add5add10 各自保存了不同的 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

五、最佳实践总结

  1. 理解参数传递是传对象引用:判断是否影响外部,看是重新赋值还是原地修改,以及对象是否可变。
  2. 默认参数不用可变对象:用 None 作为哨兵值,在函数体内初始化。
  3. 善用 *args**kwargs:让函数更灵活,但不要滥用,参数清晰优先。
  4. 减少全局变量的使用:全局变量增加耦合、降低可读性,尽量通过参数传递和返回值通信。
  5. globalnonlocal 谨慎使用:它们打破了作用域的清晰边界,只在确实需要时使用。
  6. 闭包用于轻量级状态封装:简单场景用闭包,复杂场景用类。
  7. 注意闭包的延迟绑定问题:循环中创建闭包时,用默认参数捕获当前值。

结语

函数的三个核心机制——参数传递、作用域、闭包——是层层递进的:参数传递决定了数据如何进出函数,作用域决定了变量在哪里可见,而闭包则是作用域的"延伸",让函数带着自己的环境行走天下。

理解了这三者,你不仅能避开大多数函数相关的 bug,还能轻松掌握装饰器、偏函数、回调函数等进阶概念。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值