问题的答案
Python通过一个涉及cell对象的机制实现词法作用域,这些cell对象充当嵌套函数与它们的外层作用域之间的中介。当嵌套函数引用外层作用域中的变量时,编译器将其标记为自由变量(存储在co_freevars中),而包围的函数将该变量的值存储在cell对象中,而不是标准的局部变量插槽中。nonlocal关键字指示解释器将名称查找解析到这个现有的cell对象,而不是创建新的局部绑定,从而允许内层作用域读写与外层作用域相同的内存位置。
生活中的情况
我们需要为数据处理管道实现一个轻量级审计日志记录器,以在多个回调调用之间维护已清洗记录的运行计数,而不会污染全局命名空间或创建完整的类层次结构。挑战在于确保计数器状态在对内部日志记录函数的多次调用之间持续存在,同时保持在创建它的工厂函数内部的封装。
考虑的一个解决方案是使用一个全局字典来存储以记录器ID为键的计数器。这种方法提供了简单性,并允许对状态的外部检查,但引入了全局命名空间污染,并需要复杂的锁机制以确保整个应用程序的线程安全。此外,它通过向其他模块暴露实现细节而破坏了封装。
另一种方法涉及创建一个专用类,具有一个实例属性来保存计数器。这提供了适当的封装和熟悉的面向对象语义,但为本质上是单一功能的工具添加了不必要的样板代码,并且实例创建的开销对于要实例化数千次的高频日志操作被认为过于庞大。
选择的解决方案利用了闭包,并使用nonlocal声明将计数器绑定到包围作用域中的cell对象。这种方法保持了干净的函数封装,没有类开销,确保状态对闭包保持私密,并利用了Python的优化的cell解引用机制,尽管比局部变量稍慢,但与I/O操作相比微不足道。结果是与基于类的方法相比,内存开销减少了40%,并消除了全局状态冲突。
候选人常常遗漏的内容
为什么在没有nonlocal关键字的情况下,从外层作用域对变量的赋值会创建一个新的局部变量,而不是修改外层变量?
在Python中,赋值是一条语句,默认情况下将名称绑定到当前局部作用域内的值。当编译器在嵌套函数中遇到赋值时,它会确定该变量在该函数中是局部的,除非另有声明。如果没有nonlocal,内层函数将在其自己的f_locals字典中创建一个新的条目,完全遮蔽外层变量。nonlocal声明迫使编译器将该变量视为对包围作用域中创建的cell对象的引用,允许对共享内存位置的读写访问。
关于作用域解析,nonlocal和global之间的根本区别是什么?
虽然这两个关键字都修改赋值操作所在的作用域,但global将名称解析限制在模块级全局命名空间,绕过任何中介的外层函数作用域。相反,nonlocal特定地跳过当前局部作用域,并在包围的函数定义中搜索(但不包括模块全局)以找到与该名称相关的最近cell对象。这意味着nonlocal不能用于修改模块级变量,而global也无法看到嵌套函数内部的变量,除非它们在这些外层函数中被显式声明为全局。
多个嵌套函数如何通过cell对象共享相同的状态,这些cell实际上何时被分配?
当外层函数定义多个内层函数并引用外层作用域中的相同变量时,Python编译器在外层函数的帧中为该变量创建一个单一的cell对象。所有内层函数在它们的__closure__元组中接收对这个相同cell对象的引用。这些cell在运行时分配,而不是在代码编译时,并且只要任何内层函数(或对它们的引用)存在,这些cell就会保持存在。这个共享的cell对象使不同的内层函数能够观察彼此对包围变量的修改,从而创建类似于实例变量但没有类的共享状态机制。