ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

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

Python 函数进阶:参数传递、作用域与闭包的底层逻辑 引言函数是编程中最基本的抽象单元。很多 Python 初学者写函数时只关注把一段逻辑包起来却对函数的三个核心机制一知半解参数到底是怎么传进去的为什么函数里修改一个变量有时影响外面、有时不影响闭包到底是什么、为什么装饰器离不开它这些问题不搞清楚写复杂函数时就会频繁踩坑也很难理解装饰器、偏函数、回调等进阶概念。本文将从底层原理出发系统讲解 Python 函数的参数传递机制、作用域规则和闭包的本质帮你把函数彻底搞透。一、参数传递既不是传值也不是传引用1.1 一个经典困惑先看两段代码你能预测结果吗# 代码 Adeffunc(x):x100a1func(a)print(a)# 输出什么# 代码 Bdeffunc(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 是传值或传引用其实都不准确。准确的说法是传对象引用也叫传对象或赋值传递。它的规则只有一条函数调用时实参对象的引用被赋值给形参变量形参和实参指向同一个对象。结合我们之前讲过的变量是标签模型就很好理解了deffunc(x):# x 这个新标签贴到了实参指向的同一个对象上print(id(x))# 和外面的 a 同一个 ida1print(id(a))func(a)关键在于对象本身是可变还是不可变如果对象是不可变的int、str、tuple 等函数里对形参重新赋值只是让形参标签贴到新对象上实参标签不受影响。如果对象是可变的list、dict、set 等函数里通过形参修改对象内容如append因为是同一个对象实参看到的也会变化。# 不可变对象重新赋值不影响外部deffunc1(x):xx1# 创建新对象 2x 贴到新对象print(函数内 x ,x)# 2a1func1(a)print(外部 a ,a)# 1# 可变对象原地修改影响外部deffunc2(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]# 可变对象如果是重新赋值则不影响外部deffunc3(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 如何避免函数意外修改外部数据如果不希望函数修改传入的可变对象传入副本即可deffunc(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 位置参数与关键字参数defgreet(name,age):print(f{name}今年{age}岁)# 位置参数按顺序传递greet(小明,18)# 关键字参数按名字传递顺序可换greet(age20,name小红)# 混合使用位置参数必须在前greet(小刚,age22)# greet(name小李, 25) # 语法错误关键字参数不能在位置参数前2.2 默认参数defgreet(name,age18,city北京):print(f{name}今年{age}岁住在{city})greet(小明)# 小明 今年 18 岁住在 北京greet(小红,20)# 小红 今年 20 岁住在 北京greet(小刚,city上海)# 小刚 今年 18 岁住在 上海经典坑点可变对象作为默认参数# 错误写法defadd_item(item,lst[]):lst.append(item)returnlstprint(add_item(1))# [1]print(add_item(2))# [1, 2]不是预期的 [2]print(add_item(3))# [1, 2, 3]原因默认参数在函数定义时只创建一次多次调用共享同一个对象。正确写法defadd_item(item,lstNone):iflstisNone:lst[]lst.append(item)returnlstprint(add_item(1))# [1]print(add_item(2))# [2]2.3 可变参数*args 和 **kwargs# *args收集任意数量的位置参数为元组defsum_all(*args):print(args)# 是一个元组returnsum(args)print(sum_all(1,2,3))# 6print(sum_all(1,2,3,4,5))# 15# **kwargs收集任意数量的关键字参数为字典defprint_info(**kwargs):print(kwargs)# 是一个字典forkey,valueinkwargs.items():print(f{key}:{value})print_info(name小明,age18,city北京)*args和**kwargs可以组合使用顺序必须是位置参数 →*args→ 默认参数 →**kwargs。defdemo(a,b,*args,c10,**kwargs):print(fa{a}, b{b}, args{args}, c{c}, kwargs{kwargs})demo(1,2,3,4,c20,x100,y200)# a1, b2, args(3, 4), c20, kwargs{x: 100, y: 200}2.4 参数解包调用函数时*和**可以把可迭代对象/字典解包成参数defadd(a,b,c):returnabc# 列表/元组解包为位置参数nums[1,2,3]print(add(*nums))# 6等价于 add(1, 2, 3)# 字典解包为关键字参数params{a:1,b:2,c:3}print(add(**params))# 62.5 强制位置参数与强制关键字参数Python 3.8# / 之前的参数只能按位置传递deffunc(a,b,/,c,d):print(a,b,c,d)func(1,2,3,4)# 正确func(1,2,c3,d4)# 正确# func(a1, b2, c3, d4) # 错误a 和 b 不能用关键字# * 之后的参数只能按关键字传递deffunc(a,b,*,c,d):print(a,b,c,d)func(1,2,c3,d4)# 正确# func(1, 2, 3, 4) # 错误c 和 d 必须用关键字这两个语法让 API 设计更清晰防止调用方用错误的方式传参。三、作用域LEGB 规则3.1 什么是作用域作用域决定了变量在哪些地方可以被访问。Python 中有四种作用域按查找优先级从高到低排列字母作用域说明LLocal局部函数内部EEnclosing嵌套外层函数的局部作用域GGlobal全局模块顶层BBuilt-in内置Python 内置命名空间这就是著名的LEGB 规则查找变量时从 L → E → G → B 依次查找找到第一个就停止都找不到则报NameError。xglobal# G全局作用域defouter():xenclosing# E嵌套作用域definner():xlocal# L局部作用域print(x)# 找到 L 中的 x输出 localinner()outer()把inner里的x local注释掉就会找到 E 中的enclosing再注释掉 E 中的就会找到 G 中的global。3.2 全局变量与 global 关键字函数内部可以读取全局变量但默认不能修改全局变量——赋值会被当作创建局部变量count0defincrement():countcount1# UnboundLocalError! Python 认为 count 是局部变量但还没赋值就用了increment()要在函数内修改全局变量需要用global声明count0defincrement():globalcount countcount1increment()print(count)# 13.3 嵌套作用域与 nonlocal 关键字类似地在内层函数修改外层函数的变量需要用nonlocaldefouter():count0definner():nonlocalcount count1returncountreturninner counterouter()print(counter())# 1print(counter())# 2print(counter())# 3nonlocal声明的变量会沿着嵌套作用域向上查找不包括全局找到第一个匹配的变量。3.4 作用域的常见坑# 坑 1闭包中的延迟绑定defcreate_multipliers():return[lambdax:i*xforiinrange(5)]multiplierscreate_multipliers()print(multipliers[0](2))# 8不是 0print(multipliers[2](2))# 8不是 4# 所有 lambda 共享同一个 i循环结束后 i 4# 解决方案用默认参数捕获当前值defcreate_multipliers():return[lambdax,ii:i*xforiinrange(5)]multiplierscreate_multipliers()print(multipliers[0](2))# 0print(multipliers[2](2))# 4四、闭包捕获了环境的函数4.1 什么是闭包闭包Closure是指一个函数对象它记住了定义时所在的作用域环境即使在外部调用也能访问那个作用域中的变量。通俗地说闭包 函数 它捕获的自由变量。defouter(x):definner(y):returnxy# x 是外层函数的变量被 inner 捕获returninner add5outer(5)# add5 是一个闭包x 被固定为 5print(add5(3))# 8print(add5(10))# 15add10outer(10)print(add10(3))# 13inner函数引用了外层函数outer的参数x。当outer执行完毕返回inner后按常理x应该被销毁但闭包让x存活了下来——add5和add10各自保存了不同的x值。4.2 闭包的本质closure属性我们可以通过__closure__属性查看闭包捕获的变量defouter(x):definner(y):returnxyreturninner add5outer(5)print(add5.__closure__)# (cell at 0x...: int object at 0x...,)print(add5.__closure__[0].cell_contents)# 5捕获的 x 值每个被捕获的变量被包装成一个cell对象存在__closure__元组中。这就是闭包记住环境的底层实现。4.3 闭包的典型应用应用 1工厂函数生成配置好的函数defmake_multiplier(factor):defmultiply(n):returnn*factorreturnmultiply doublemake_multiplier(2)triplemake_multiplier(3)print(double(5))# 10print(triple(5))# 15应用 2计数器状态保持defmake_counter():count0defcounter():nonlocalcount count1returncountreturncounter c1make_counter()print(c1())# 1print(c1())# 2print(c1())# 3c2make_counter()# 独立的计数器print(c2())# 1闭包让count变量变成了私有状态外部无法直接访问只能通过返回的函数操作——这其实就是面向对象中封装的一种轻量实现。应用 3装饰器的基础Python 装饰器本质上就是一个闭包。理解了闭包装饰器就不再神秘deflog_decorator(func):defwrapper(*args,**kwargs):print(f调用函数:{func.__name__})resultfunc(*args,**kwargs)print(f函数{func.__name__}执行完毕)returnresultreturnwrapperlog_decoratordefadd(a,b):returnabprint(add(1,2))# 调用函数: add# 函数 add 执行完毕# 3wrapper捕获了外层的func形成闭包。装饰器的详细内容我们会在后续文章中专门讲解。4.4 闭包 vs 类什么时候用哪个闭包和类都能实现状态 行为的封装。选择原则状态简单、只有一个主要行为 → 用闭包更轻量状态复杂、有多个方法/行为 → 用类更清晰# 闭包版本简单计数器defmake_counter():count0defcounter():nonlocalcount count1returncountreturncounter# 类版本支持更多操作的计数器classCounter:def__init__(self):self.count0defincrement(self):self.count1returnself.countdefreset(self):self.count0defget_value(self):returnself.count五、最佳实践总结理解参数传递是传对象引用判断是否影响外部看是重新赋值还是原地修改以及对象是否可变。默认参数不用可变对象用None作为哨兵值在函数体内初始化。善用*args和**kwargs让函数更灵活但不要滥用参数清晰优先。减少全局变量的使用全局变量增加耦合、降低可读性尽量通过参数传递和返回值通信。global和nonlocal谨慎使用它们打破了作用域的清晰边界只在确实需要时使用。闭包用于轻量级状态封装简单场景用闭包复杂场景用类。注意闭包的延迟绑定问题循环中创建闭包时用默认参数捕获当前值。结语函数的三个核心机制——参数传递、作用域、闭包——是层层递进的参数传递决定了数据如何进出函数作用域决定了变量在哪里可见而闭包则是作用域的延伸让函数带着自己的环境行走天下。理解了这三者你不仅能避开大多数函数相关的 bug还能轻松掌握装饰器、偏函数、回调函数等进阶概念。
返回列表