Python scope

学过 C 的同学还是很好理解 Python 的变量名称搜索策略的。但 Python 还是有它自己的特点

对于一个函数,有 3 种类型的变量:

  1. 如果变量在 def 内部定义,那么对于该函数来说,是本地变量 (local variable)

  2. 如果变量在 enclosing def (可以理解为该函数是内部函数,变量是外部函数的变量)定义,那么对于该函数来说,是非本地变量 (nonlocal variable)
    这种函数在 Java 中非常常见

  3. 如果变量定义在所有 def 的外部,那么认为它对于真个文件是 全局的 (global),
    请注意,这里的全局所指的范围仅仅是变量所在的单个文件,又或者说变量所在的 module (a file is a module)
    C 语言中,如果我们需要另外一个文件的全局变量,则必须使用 extern 语法声明该变量存在,同时还要 include 头文件;同样的,在 Python 中,如果我们需要使用另外文件的全局变量,则必须 import module, 然后使用 module.xxx 即可

请牢记两点:

  1. 函数内部任何形式的赋值 (assignment), 默认会将该变量分类为局部变量,效果是创建或改变该变量

  2. 但注意 in-place changes (mutable object 具有的特性)不会将变量分类为局部变量,这很好理解,因为它不会创建变量,它只是利用 name reference, 因此只需要搜索该变量即可
    问题来了,如何搜索变量 ??

1. LEGB Rule

当我们在函数内部使用 unqualified name (这里打个伏笔,针对 qualified attribute name, 还有其他的规则),Python 按下面次序进行范围搜索: 局部范围 (Local -L),包裹该函数的外部函数范围 (enclosing – E),全局范围 (Global – G),最后是内建范围 (built-in B)。一旦找到,便不再继续搜索

下图展示了四个范围:

但是如果我们在函数内部进行赋值操作,而不是在表达式中进行引用,那么 Python 总是默认在局部范围内创建或改变名字,除非声明该名字为 global 或者 nonlocal

还有两类特殊变量

  1. comprehension variables
    所有生成式格式 (comprehension expression): generator, list, set, dictionary,内部创建的变量都是限定在表达式范围内的,比如 [X for X in I] 中的 X

  2. exception variables
    except E as X 这里 X 在异常处理代码块看来也是局部的,并在代码块运行结束被销毁

那么问题来了,如果我们想在函数内部改变一个全局变量的值,就必须使用赋值语句,而赋值语句又默认将变量视为局部变量,也就是说,如果赋值语句在局部范围内找不到变量,就是创建一个局部变量。针对这种清理,我们必须用 global 语法 !!!

2. global

The global statement consists of the keyword global, followed by one or more names separated by commas. All the listed names will be mapped to the enclosing module’s scope when assigned or referenced within the function body.

但还是提醒读者两个设计原则:

  1. 减少全局变量的使用,
  2. 减少跨文件的变量改变,减小耦合

这两点和 C/C++ 程序设计思想是一致的

3. factory functions: closures

或者我们也可以用 lambda ,由于 enclosing scopes lookup 机制的存在, lambda 表达式内部的变量搜索可以搜寻在定义该表达式的外部函数中寻找

但是 !!!

如果我们使用 loop variables, 需要把该变量传递给 lambda 表达式

可以看到,list acts 中的每个 lambda 表达式使用的变量 i 都是 4, 结果都是 2**4 ,即 16

改进如下:

4. nonlocal

类似 global, the name listed in a nonlocal must have been previously defined in an enclosing def when the nonlocal is reached, or an error is raised. 如果我们不需要改变

The global means the names reside in the enclosing module, and nonlocal means they reside in an enclosing def

为了解决上面的问题,我们必须标注 statenonlocal 。在 enclosing scope 内的 state 对象和 闭包返回的 nested function object 紧密联系。

同时,多次调用 tester 工厂函数 (闭包 closure) 会进行多个 state 变量的拷贝,相互之间不会互相干扰。每次返回的 nested 函数对象都会同时拷贝一次 state object,并且在 enclosing function 外部是无法使用或获得 state object 的。看下面的例子:

为什么在已经有 global 的情况下,还提出 nonglobal 呢 ?

保留状态信息是最重要的因素

While functions can return results, their local variables won’t normally retain other values that must live on between calls. Moreover, many applications requires such values to differ per context of use.

nonlocal 实现了函数状态信息保留和不同上下文之间的隔离。the nonlocal statements allows multiple copies of changeable state to be retained in memory. nonlocal 实现了简单的状态保存,非常适合轻量级 (lightweight) 的上下文保存, 同时也没有将状态暴露给全局范围 (而且使用全局变量会在不同上下文之间产生干扰,因为只有一个变量啊)!!!

那还有没有其他的解决方案呢 ??

  1. 使用更重的 class 也是可以的:
  2. 使用 function attributes
    下面例子中,我们使用 nested.state ,并且在 def nested 后初始化

    为什么可以这样做 ?
    函数名称 nested 是 enclosing scope tester 的局部变量,因此在 tester 内部可以自由引用。同时 in-place change 不是赋值语句,因此当执行 nested.state += 1 时,它改变的是 nested 引用的 object 的一部分,而不是 nested 本身。

  3. 使用 mutables 的 in-place change 特性
    还记得在博客开头说过吧, mutable 的 in-place change 不是 assignment ,而是名字引用,因此我们会直接用 LEGB 规则进行名字搜索。

最后给出一个自定义 open() 的例子:

One thought on “Python scope

发表评论

电子邮件地址不会被公开。 必填项已用*标注