Python从入门到精通(第11章):函数进阶:作用域与闭包
Python从入门到精通第11章函数进阶作用域与闭包开头导语这是本系列第11章。前面你已经掌握函数的基本定义和调用方式这一章在此基础上向前一步解决三个实际问题变量名冲突时 Python 到底用了哪一个、为什么内部函数能够“记住”外部函数的变量、闭包在真实业务里能做什么。阅读时建议边看边动手改代码尤其要关注“错误示例”部分——这些错误在后续阅读他人代码时几乎一定会遇到。章节摘要本章围绕“作用域”与“闭包”两条主线展开。作用域决定了一个名字在哪个范围内有效Python 按 LEGB 顺序查找变量闭包则是函数作为返回值时把外部变量“携带”出来的机制常用于状态保持和函数生成。掌握这两点之后你读别人代码时不会再被“变量到底用的是哪个”这类问题困扰写自己的代码时也能设计出更干净的接口。关键词LEGBglobalnonlocal闭包嵌套函数函数工厂状态保持学习目标能用 LEGB 规则解释任意一行代码里变量的来源能区分局部变量和全局变量知道什么情况下必须用 global/nonlocal能看懂闭包的运行机制能自己写一个闭包函数能把“状态保持”需求迁移到闭包实现而不是用全局变量凑合先修知识会定义函数、传递参数、设置默认参数知道列表、字典等可变对象的基本操作理解 if/for/while 的缩进含义环境准备python--versionpython-mvenv .venv# Windows PowerShell.venv\Scripts\Activate.ps1核心知识讲解知识点1LEGB 规则Python 查找一个变量名时按以下顺序逐层搜索LLocal当前函数内部定义的变量EEnclosing外层函数的变量嵌套函数场景GGlobal模块顶层函数外部定义的变量BBuilt-inPython 内置名字如len、print、list下面这段代码演示了一次完整的 LEGB 查找过程bglobal bdefouter():eenclosing edefinner():llocal lprint(l)# L 层找到local lprint(e)# E 层找到enclosing eprint(b)# G 层找到global bprint(len)# B 层找到内置函数 leninner()outer()运行输出local l enclosing e global b built-in function len这个顺序是固定的无论变量名是否相同Python 都按 LEGB 顺序查找不会在找完 L 层之后继续向上找。知识点2局部变量与 global 声明函数内部赋值的变量默认是局部的和函数外部同名变量没有任何关系。错误示例不要这样写count0definc():# 期望对全局 count 1但实际上这里是在创建局部变量 countcountcount1# UnboundLocalError: cannot assign to global variableinc()print(count)问题说明count count 1在 Python 里是一个赋值语句Python 解释器在看到左侧的count时在函数开头就把count作为局部变量处理了所以右侧的count 1去找局部变量但它尚未赋值于是报错。正确写法如果要修改全局变量count0definc():globalcount# 显式声明下面的 count 引用全局变量countcount1inc()print(count)# 输出 1正确写法更推荐的方式通过参数传入、返回值传出count0definc(value):returnvalue1countinc(count)print(count)# 输出 1全局变量看似方便但会导致函数行为依赖外部状态测试困难、难以复用。除非真的需要跨模块共享否则优先通过参数和返回值传递数据。知识点3nonlocal 声明当嵌套函数需要修改外层函数的变量时需要用nonlocal声明。和global不同nonlocal不会穿透到模块顶层只作用于外层函数。错误示例不要这样写defcounter():count0definc():countcount1# 同样是 UnboundLocalErrorreturncountreturninc ccounter()print(c())# 报错问题说明内层函数inc在看到count count 1时把count当作局部变量处理但右侧的count 1在函数执行时还没有值所以报错。正确写法defcounter():count0definc():nonlocalcount countcount1returncountreturninc ccounter()print(c())# 1print(c())# 2print(c())# 3这里inc是一个闭包函数它定义在counter内部但返回后仍然保留对counter的count变量的引用。正是nonlocal count让修改成为可能。知识点4闭包的定义与运行机制闭包Closure是指一个函数记住创建时所在作用域的变量的能力。Python 中只要函数引用了外层作用域的变量Python 就会把被引用的变量绑定到函数对象上这称为“闭包”。下面用图示说明闭包的形成过程counter() 调用 ├── 创建局部变量 count 0 ├── 定义 inc 函数此时 inc 捕获了 count 的引用 └── 返回 inc 函数对象 后续 c() 调用 ├── c 是一个函数对象 ├── c.__closure__ 包含对原 count 单元的引用 └── 每次调用 c() 都修改同一个 count验证闭包是否真正生效defcounter():count0definc():nonlocalcount count1returncountreturninc ccounter()print(c.__closure__)# (cell at ...,) 有值说明是闭包print(c.__code__.co_freevars)# (count,) 捕获的外层变量名print(c())# 1print(c())# 2__closure__不为None是判断闭包是否生效的直接依据。如果__closure__为None说明函数没有捕获外层变量不构成闭包。知识点5闭包的实际用途——函数工厂闭包最常见的用途是生成“带不同初始参数的专业函数”。在没有类的情况下闭包提供了一种轻量级的状态保持方式。错误示例不要这样写用全局变量模拟“配置”base_urlhttps://api.example.compath/usersdefmake_request():returnbase_urlpath# base_url 和 path 是全局变量容易被其他代码污染# 如果需要另一组配置只能再定义新函数或改全局变量base_urlhttps://api2.example.com# 影响之前创建的 make_request问题说明全局变量在程序任何位置都可以被修改多个函数依赖同一全局变量时修改一个会影响另一个难以追踪。正确写法用闭包生成函数工厂defmake_api_client(base_url):path/usersdefmake_request(endpoint):returnf{base_url}{path}{endpoint}defset_path(new_path):nonlocalpath pathnew_pathreturnmake_request,set_path# 生成两个不同配置的客户端req1,set_path1make_api_client(https://api1.example.com)req2,set_path2make_api_client(https://api2.example.com)print(req1(/list))# https://api1.example.com/users/listprint(req2(/list))# https://api2.example.com/users/list# 两个客户端互不影响set_path1(/admin)print(req1(/list))# https://api1.example.com/admin/listprint(req2(/list))# https://api2.example.com/users/list不受影响这里make_api_client是一个函数工厂每次调用都生成一组相互独立的make_request和set_path函数两个客户端之间完全隔离互不干扰。知识点6 nonlocal 与 global 的区别两者名字相似作用域层级不同关键字作用层级影响范围global x模块顶层整个模块文件内有效nonlocal x外层函数仅外层函数作用域不穿透到模块顶层xglobal xdefouter():xouter xdefinner_global():globalx# 修改模块顶层的 xxmodified global xdefinner_nonlocal():nonlocalx# 修改 outer 的 xxmodified outer xprint(fbefore global:{x})# outer xinner_global()print(fafter global:{x})# outer xouter 未变因为改的是全局print(fbefore nonlocal:{x})# outer xinner_nonlocal()print(fafter nonlocal:{x})# modified outer xouter()print(fmodule level:{x})# modified global x实际工程里nonlocal比global少见但在装饰器和嵌套回调里会用到。优先通过返回值传递数据其次nonlocalglobal只在模块级配置常量时使用。案例实战主案例带记忆功能的线性回归器本案例目标用闭包实现一个简易的“带记忆的线性回归器”。需求是每次输入一个新数据点模型记录下来可以查询当前累计了多少个数据点可以清除所有历史数据重新开始不同模型之间数据完全隔离互不影响defmake_regressor():返回一个带记忆的线性回归计算器data_x[]data_y[]defadd_point(x:float,y:float):添加一个数据点nonlocaldata_x,data_y data_x.append(x)data_y.append(y)defmean(values:list)-float:returnsum(values)/len(values)ifvalueselse0.0defslope()-float:计算斜率 b Σ(x-x̄)(y-ȳ) / Σ(x-x̄)²iflen(data_x)2:return0.0x_meanmean(data_x)y_meanmean(data_y)numeratorsum((x-x_mean)*(y-y_mean)forx,yinzip(data_x,data_y))denominatorsum((x-x_mean)**2forxindata_x)returnnumerator/denominatorifdenominator!0else0.0defintercept()-float:计算截距 ā ȳ - b * x̄returnmean(data_y)-slope()*mean(data_x)defpredict(x:float)-float:根据当前数据拟合直线返回预测值returnintercept()slope()*xdefcount()-int:返回累计数据点数量returnlen(data_x)defreset():清除所有历史数据nonlocaldata_x,data_y data_x[]data_y[]defdescribe():return{n:len(data_x),slope:slope(),intercept:intercept(),}returnadd_point,predict,count,reset,describe# 使用方式add1,pred1,cnt1,reset1,desc1make_regressor()add2,pred2,cnt2,reset2,desc2make_regressor()# 模型1 添加数据add1(1.0,2.0)add1(2.0,4.1)add1(3.0,5.9)# 模型2 添加数据独立不受模型1影响add2(1.0,10.0)add2(2.0,20.0)print(desc1())# {n: 3, slope: 1.97, intercept: 0.02}print(desc2())# {n: 2, slope: 10.0, intercept: 0.0}print(cnt1())# 3print(cnt2())# 2print(pred1(4.0))# 用模型1预测 x4 时的 yprint(pred2(4.0))# 用模型2预测 x4 时的 y结果不同# 重置模型1不影响模型2reset1()print(cnt1())# 0print(cnt2())# 2模型2保持不变这个案例展示了闭包的核心价值每个模型的数据data_x、data_y完全隔离不需要用类只通过函数返回值和nonlocal声明就实现了状态管理。模型1和模型2之间没有任何共享变量互相独立。扩展案例ATM 账户模拟器需求用闭包实现一个 ATM 账户具备存钱、取钱、查询余额功能不允许余额为负。defmake_account(initial_balance:float0.0)-tuple:返回一个账户实例存款函数, 取款函数, 查询函数ifinitial_balance0:raiseValueError(初始余额不能为负)balanceinitial_balancedefdeposit(amount:float)-float:nonlocalbalanceifamount0:raiseValueError(存款金额必须为正)balanceamountreturnbalancedefwithdraw(amount:float)-float:nonlocalbalanceifamount0:raiseValueError(取款金额必须为正)ifamountbalance:raiseValueError(f余额不足当前{balance}需取{amount})balance-amountreturnbalancedefget_balance()-float:returnbalancereturndeposit,withdraw,get_balance# 创建两个独立账户dep_a,wit_a,bal_amake_account(1000.0)dep_b,wit_b,bal_bmake_account(500.0)print(bal_a())# 1000.0print(bal_b())# 500.0dep_a(200.0)wit_b(100.0)print(bal_a())# 1200.0print(bal_b())# 400.0try:wit_a(2000.0)# 余额不足应抛异常exceptValueErrorase:print(f取款失败:{e})扩展思考如何在不修改函数签名的情况下给账户加“日累计取款上限”功能提示用闭包再包装一层或者在withdraw函数内部再加一层计数器。常见错误与排查UnboundLocalError函数内部有赋值语句但没有声明global/nonlocal右侧又引用了同名变量。先检查变量是在哪里定义的再决定是用global、nonlocal还是通过参数传入。闭包捕获的是引用不是值如果外层变量是可变对象列表、字典在闭包里可以直接修改不需要nonlocal。闭包函数返回后外层变量消失Python 的垃圾回收机制不会回收仍被闭包引用的外层变量所以闭包是安全的不会产生悬空引用。过度使用 global所有函数都加global会使程序不可测试发现自己频繁使用 global 时优先考虑把数据封装到闭包或类里。性能与工程建议闭包的性能和普通函数几乎没有差别除非在极敏感的热路径上否则不需要担心。闭包适合“函数需要记住一些配置”的场景如果对象有多种方法且需要继承等特性优先用类。每个闭包函数尽量只对应一个明确职责不要在一个闭包里放太多状态。通过返回值显式暴露必要的操作不要把内部变量直接暴露给调用方。本章代码自测清单可打勾我已运行 LEGB 示例代码观察了各层变量的查找顺序。我已复现UnboundLocalError并用global/nonlocal修复。我已打印func.__closure__和func.__code__.co_freevars验证闭包生效。我已写出函数工厂示例理解“生成带不同配置的函数”的过程。我已在 IDE 里逐步调试 ATM 账户案例记录每次余额变化。我已思考扩展练习如何给账户加日累计取款上限。我能解释global和nonlocal的作用域层级差异。我能把本章闭包知识迁移到另一个业务场景如缓存器、计数器、配置隔离。我已完成章末提问至少检查过参考答案。我为下一章整理了待补充的基础清单。章末提问LEGB 分别指哪四个作用域查找顺序是什么什么情况下会产生UnboundLocalError如何修复global和nonlocal的作用范围有什么区别如何判断一个函数是否形成了闭包闭包函数工厂和类有什么相似和不同在 ATM 案例中deposit函数为什么要检查amount 0如果把data_x、data_y从列表换成普通整数闭包还能正常工作吗为什么闭包捕获的是变量的值还是引用这有什么区别在工程中什么情况下会优先选择闭包而不是类如果要消除一个 Python 程序的全局变量闭包能完全替代吗章末答案Local当前函数、Enclosing外层函数、Global模块顶层、Built-in内置。Python 按这个顺序逐层查找找到即停。函数内部对变量赋值且未声明global/nonlocal时Python 把该变量当作局部变量处理但右侧引用时它尚未赋值导致UnboundLocalError。修复方法是加global改模块顶层或nonlocal改外层函数声明或通过参数传入、返回值传出。global把名字绑定到模块顶层的全局变量影响整个文件nonlocal只作用于外层函数不会穿透到模块顶层。检查func.__closure__是否为非None或者检查func.__code__.co_freevars是否包含捕获的变量名。两者都能实现状态保持。类通过实例属性存储状态闭包通过捕获外层变量存储状态。类支持继承和方法多态闭包更轻量适合单一职责。如果需要多种方法且结构复杂优先用类如果只是“生成一批配置不同的函数”闭包更简洁。amount 0表示非正当存款金额应该拒绝否则可以存入负数导致余额凭空增加。不能。整数是不可变对象闭包只能引用外层变量不能在闭包里直接修改不可变对象。如果要修改必须用nonlocal声明而nonlocal只能指向已经存在的外层变量——整数可以但每次赋值会创建新对象并重新绑定。列表等可变对象可以直接在闭包里修改不需要nonlocal。Python 闭包捕获的是变量的引用不是值本身。这意味着外层变量被修改后闭包下次访问时能得到最新值但也意味着闭包持有对外层对象的引用可能阻止垃圾回收通常不是问题。场景简单只需要一两个函数、需要状态保持、不想引入类的开销、或者需要生成一批配置不同的函数时优先用闭包。需要多种方法、复杂状态、继承体系时用类。闭包可以在很大程度上替代全局变量但不能完全消除所有全局名字如模块级 import、函数定义本身。不过在数据层面用闭包或类封装状态可以做到“零全局可变数据”。本章小结作用域决定变量名的有效范围LEGB 是 Python 的查找顺序。global和nonlocal分别是向模块顶层和外层函数写入变量的声明慎用。闭包是函数记住创建时外层变量的机制通过__closure__和co_freevars可以观测。闭包常用于函数工厂、状态保持、轻量级配置隔离比全局变量更容易测试和维护。下一章预告下一章是第12章《匿名函数与高阶函数》。建议先理解函数是一等公民的概念可以把函数赋值给变量、作为参数传递、作为返回值再学习 lambda 表达式的适用场景和局限。章节导航上一篇第10章《文件读写与路径管理》下一篇第12章《匿名函数与高阶函数》版权声明本文为《Python从入门到精通》系列连载内容面向学习交流使用。版权归作者所有转载须保留出处与章节信息。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2460870.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!