Python 的做用域准则

0x00 前言

由于最先用的是 Java 和 C#,写 Python 的时候天然也把 Python 做用域的想的和原有的一致。python

Python 的做用域变量遵循在大部分状况下是一致的,但也有例外的状况。bash

本文着经过遇到的一个做用域的小问题来讲说 Python 的做用域app

0x01 做用域的几个实例

但也有部分例外的状况,好比:函数

1.1 第一个例子

做用域初版代码以下ui

a = 1
print(a, id(a)) # 打印 1 4465620064
def func1():
    print(a, id(a))
func1()  # 打印 1 4465620064复制代码

做用域初版对应字节码以下编码

4           0 LOAD_GLOBAL              0 (print)
              3 LOAD_GLOBAL              1 (a)
              6 LOAD_GLOBAL              2 (id)
              9 LOAD_GLOBAL              1 (a)
             12 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             15 CALL_FUNCTION            2 (2 positional, 0 keyword pair)
             18 POP_TOP
             19 LOAD_CONST               0 (None)
             22 RETURN_VALUE复制代码

PS: 行 4 表示 代码行数 0 / 3 / 9 ... 不知道是啥,我就先管他叫作吧 是 load global
PPS: 注意条 3/6 LOAD_GLOBAL 为从全局变量中加载spa

顺手附上本文须要着重理解的几个指令设计

LOAD_GLOBA          : Loads the global named co_names[namei] onto the stack.
LOAD_FAST(var_num)  : Pushes a reference to the local co_varnames[var_num] onto the stack.
STORE_FAST(var_num) : Stores TOS into the local co_varnames[var_num].复制代码

这点彷佛挺符合咱们认知的,那么,再深一点呢?既然这个变量是能够 Load 进来的就能够修改咯?code

1.2 第二个例子

然而并非,咱们看做用域第二版对应代码以下ip

a = 1
print(a, id(a)) # 打印 1 4465620064
def func2():
    a = 2
    print(a, id(a))
func2() # 打印 2 4465620096复制代码

一看,WTF, 两个 a 内存值不同。证实这两个变量是彻底两个变量。

做用域第二版对应字节码以下

4           0 LOAD_CONST               1 (2)
              3 STORE_FAST               0 (a)

  5           6 LOAD_GLOBAL              0 (print)
              9 LOAD_FAST                0 (a)
             12 LOAD_GLOBAL              1 (id)
             15 LOAD_FAST                0 (a)
             18 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             21 CALL_FUNCTION            2 (2 positional, 0 keyword pair)
             24 POP_TOP
             25 LOAD_CONST               0 (None)
             28 RETURN_VALUE复制代码

注意行 4 条 3 (STORE_FAST) 以及行 5 条 9/15 (LOAD_FAST)

这说明了这里的 a 并非 LOAD_GLOBAL 而来,而是从该函数的做用域 LOAD_FAST 而来。

1.3 第三个例子

那咱们在函数体重修改一下 a 值看看。

a = 1
def func3():
    print(a, id(a)) # 注释掉此行不影响结论
    a += 1
    print(a, id(a))
func3() # 当调用到这里的时候 local variable 'a' referenced before assignment
# 即 a += 1 => a = a + 1 这里的第二个 a 报错鸟复制代码
3           0 LOAD_GLOBAL              0 (print)
              3 LOAD_FAST                0 (a)
              6 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
              9 POP_TOP

  4          10 LOAD_FAST                0 (a)
             13 LOAD_CONST               1 (1)
             16 BINARY_ADD
             17 STORE_FAST               0 (a)

  5          20 LOAD_GLOBAL              0 (print)
             23 LOAD_FAST                0 (a)
             26 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             29 POP_TOP
             30 LOAD_CONST               0 (None)
             33 RETURN_VALUE复制代码

那么,func3 也就天然而言因为没有没法 LOAD_FAST 对应的 a 变量,则报了引用错误。

而后问题来了,a 为基本类型的时候是这样的。若是引用类型呢?咱们直接仿照 func3 的实例把 a 改为 list 类型。以下

1.4 第四个例子

a = [1]
def func4():
    print(a, id(a)) # 这条注不注释掉都同样
    a += 1 # 这里我故意写错 按理来讲应该是 a.append(1)
    print(a, id(a))
func4()

# 当调用到这里的时候 local variable 'a' referenced before assignment复制代码

╮(╯▽╰)╭ 看来事情那么简单,结果变量 a 依旧是没法修改。

可按理来讲跟应该报下面的错误呀

'int' object is not iterable复制代码

1.5 第五个例子

a = [1]
def func5():
    print(a, id(a))
    a.append(1)
    print(a, id(a))
func5()
# [1] 4500243208
# [1, 1] 4500243208复制代码

这下能够修改了。看一下字节码。

3           0 LOAD_GLOBAL              0 (print)
              3 LOAD_GLOBAL              1 (a)
              6 LOAD_GLOBAL              2 (id)
              9 LOAD_GLOBAL              1 (a)
             12 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             15 CALL_FUNCTION            2 (2 positional, 0 keyword pair)
             18 POP_TOP

  4          19 LOAD_GLOBAL              1 (a)
             22 LOAD_ATTR                3 (append)
             25 LOAD_CONST               1 (1)
             28 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             31 POP_TOP

  5          32 LOAD_GLOBAL              0 (print)
             35 LOAD_GLOBAL              1 (a)
             38 LOAD_GLOBAL              2 (id)
             41 LOAD_GLOBAL              1 (a)
             44 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             47 CALL_FUNCTION            2 (2 positional, 0 keyword pair)
             50 POP_TOP
             51 LOAD_CONST               0 (None)
             54 RETURN_VALUE复制代码

从全局拿来 a 变量,执行 append 方法。

0x02 做用域准则以及本地赋值准则

2.1 做用域准则

看来这是解释器遵循了某种变量查找的法则,彷佛就只能从原理上而不是在 CPython 的实现上解释这个问题了。

查找了一些资料,发现 Python 解释器在依据 基于 LEGB 准则 (顺手吐槽一下不是 LGBT)

LEGB 指的变量查找遵循

  • Local
  • Enclosing-function locals
  • Global
  • Built-In

StackOverFlow 上 martineau 提供了一个不错的例子用来讲明

x = 100
print("1. Global x:", x)
class Test(object):
    y = x
    print("2. Enclosed y:", y)
    x = x + 1
    print("3. Enclosed x:", x)

    def method(self):
        print("4. Enclosed self.x", self.x)
        print("5. Global x", x)
        try:
            print(y)
        except NameError as e:
            print("6.", e)

    def method_local_ref(self):
        try:
            print(x)
        except UnboundLocalError as e:
            print("7.", e)
        x = 200 # causing 7 because has same name
        print("8. Local x", x)

inst = Test()
inst.method()
inst.method_local_ref()复制代码

咱们试着用变量查找准则去解释 第一个例子 的时候,是解释的通的。

第二个例子,发现函数体内的 a 变量已经不是那个 a 变量了。要是按照这个查找原则的话,彷佛有点说不通了。

但当解释第三个例子的时候,就彻底说不通了。

a = 1
def func3():
    print(a, id(a)) # 注释掉此行不影响结论
    a += 1
    print(a, id(a))
func3() # 当调用到这里的时候 local variable 'a' referenced before assignment
# 即 a += 1 => a = a + 1 这里的第二个 a 报错鸟复制代码

按照个人猜测,这里的代码执行可能有两种状况:

  • 当代码执行到第三行的时候多是向从 local 找 a, 发现没有,再找 Enclosing-function 发现没有,最后应该在 Global 里面找到才是。注释掉第三行的时候也是同理。
  • 当代码执行到第三行的时候多是向下从 local 找 a, 发现有,而后代码执行,结束。

但若是真的和个人想法接近的话,这两种状况均可以执行,除了变量做用域以外仍是有一些其余的考量。我把这个叫作本地赋值准则 (拍脑壳起的名称)

通常咱们管这种考量叫作 Python 做者就是以为这种编码方式好你爱写不写 Python 做者对于变量做用域的权衡。

事实上,当解释器编译函数体为字节码的时候,若是是一个赋值操做 (list.append 之流不是赋值操做),则会被限定这个变量认为是一个 local 变量。若是在 local 中找不到,并不向上查找,就报引用错误。

这不是 BUG
这不是 BUG
这不是 BUG复制代码

这是一种设计权衡 Python 认为 虽然不强求强制声明类型,但假定被赋值的变量是一个 Local 变量。这样减小避免动态语言好比 JavaScript 动不动就修改掉了全局变量的坑。

这也就解释了第四个例子中赋值操做报错,以及第五个例子 append 为何能够正常执行。

若是我偏要勉强呢? 能够经过 global 和 nonlocal 来 引入模块级变量 or 上一级变量。

PS: JS 也开始使用 let 进行声明,小箭头函数内部赋值查找变量也是向上查找。

0xEE 参考连接


ChangeLog:

  • 2017-11-20 从原有笔记中抽取本文整理而成
相关文章
相关标签/搜索