0x00 前言

因为最早用的是 Java 和 C#,写 Python 的时候自然也把 Python 作用域的想的和原有的一致。

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

本文着通过遇到的一个作用域的小问题来说说 Python 的作用域

0x01 作用域的几个实例

但也有部分例外的情况,比如:

1.1 第一个例子

作用域第一版代码如下

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_VALUEPS: 行 4 表示 代码行数 0 / 3 / 9 ... 不知道是啥,我就先管他叫做条吧 是 load global PPS: 注意条 3/6 LOAD_GLOBAL 为从全局变量中加载

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

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 进来的就可以修改咯?

1.2 第二个例子

然而并不是,我们看作用域第二版对应代码如下

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 作者就是觉得这种编码方式好你爱写不写 orPython 作者对于变量作用域的权衡。

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

这不是 BUG

这不是 BUG

这不是 BUG

这是一种设计权衡 Python 认为 虽然不强求强制声明类型,但假定被赋值的变量是一个 Local 变量。这样减少避免动态语言比如

这也就解释了第四个例子中赋值操作报错,以及第五个例子 append 为什么可以正常执行。

如果我偏要勉强呢? 可以通过 global 和 nonlocal 来 引入模块级变量 or 上一级变量。PS: JS 也开始使用 let 进行声明,小箭头函数内部赋值查找变量也是向上查找。

0x03 后续

在Python的FAQ 里面回答了这个问题This is because when you make an assignment to a variable in a scope, that variable becomes local to that scope and shadows any similarly named variable in the outer scope. Since the last statement in foo assigns a new value to x, the compiler recognizes it as a local variable. Consequently when the earlier print(x) attempts to print the uninitialized local variable and an error results.

至于评论区举出的精彩例子究其本质并不算做的突破了限制原因1 : int 实例没有 __iadd__

原因2 : UnboundLocalError 的原因在于编译阶段判定变量为local 而不是 执行阶段执行iadd

原因3 : 不用 operator 库也可以

def new_iadd(x,y):

x += y

return x

该例子的其本质还是与第一个例子一样, 未经赋值直接引用了 a 实例, 并不是在函数内部赋值从而让编译器在编译阶段判定变量为local 从而在执行阶段报错 UnboundLocalError

当然, 我也犯了一个严重的错误, 就是想当然的认为 Python 和 Java 一样是有基础类型和引用类型的. 这里需要检讨一下自己.

0xEE 参考链接

ChangeLog:2017-11-20 从原有笔记中抽取本文整理而成

2018-03-05 补充评论区的例子,

python作用域链_Python 中的作用域准则相关推荐

  1. python内置作用域_python中的作用域

    python中的作用域分4种情况: L:local,局部作用域,即函数中定义的变量: E:enclosing,嵌套的父级函数的局部作用域,即包含此函数的上级函数的局部作用域,但不是全局的: G:glo ...

  2. python标志变量_Python 中的 global 标识对变量作用域的影响

    global 标识用于在函数内部,修改全局变量的值. 我们可以通过以下规则,来判定一个变量到底是在全局作用域还是局部作用域: 变量定义在全局作用域,那就是全局变量. 变量在函数中定义,并且加了 glo ...

  3. python命名空间特性_Python命名空间与作用域

    名称空间名称空间(namespaces):用于存放名字与内存地址绑定关系的地方,是对栈区的划分 作用:名称空间可以使栈区中存放相同的名字,从而解决命名冲突 名称空间分为三种:内置名称空间 全局名称空间 ...

  4. el 能否定义作用域变量_python命名空间和作用域

    一.命名空间 1.定义:命名空间(Namespace)是从名称到对象的映射 2.实现:大部分的命名空间都是通过 Python 字典来实现的 3.目的:命名空间提供了在项目中避免名字冲突的一种方法 4. ...

  5. python map用法_Python中ChainMap的一种实用用法

    Python部落(python.freelycode.com)组织翻译,禁止转载,欢迎转发. 简而言之ChainMap:将多个字典视为一个,解锁Python超能力. Python标准库中的集合模块包含 ...

  6. python基本统计量_Python中简单统计量的计算

    本篇文章给大家带来的内容是关于Python中简单统计量的计算,有一定的参考价值,有需要的朋友可以参考一下,希望对你有所帮助. 1.这些操作都要确保已经在电脑中安装好了Anaconda集成库,如果安装好 ...

  7. python解析原理_Python 中 -m 的典型用法、原理解析与发展演变

    在命令行中使用 Python 时,它可以接收大约 20 个选项(option),语法格式如下: python [-bBdEhiIOqsSuvVWx?] [-c command | -m module- ...

  8. python正则表达式空格_python中的正则表达式的使用

    一.正则表达式简介 正则表达式:又称正规表示式.正规表示法.正规表达式.规则表示式.常规表示法(英语:Regular Expression,在代码中常简写为regex.regexp或者是RE),是计算 ...

  9. python wraps模块_python中 wraps 的作用

    这里使用两段代码比较加入wraps装饰器后,函数打印的结果对比: 新建文件名:Testword 代码1:不加wraps装饰器 # coding=utf-8 from functools import ...

最新文章

  1. python函数拟合不规则曲线_python 对任意数据和曲线进行拟合并求出函数表达式的三种解决方案...
  2. 后盾网lavarel视频项目---5、淘宝镜像cnpm的原理及如何使用
  3. 误执行了rm -fr /*之后,除了跑路还能怎么办?!
  4. JDBC操作数据库就这八步!
  5. python清空文件夹
  6. java AST 表达式_java AST JCTree简要分析
  7. Old ST-LINK firmware detected.do you want to upgrade it?已解决,stlink升级
  8. 安装JDK出现问题 Error opening registry key'software\Javasoft\Java Runtime Environment'
  9. 文化的作用与本质是什么
  10. Android NDK开发之 与NEON相关的库
  11. TensorFlow变量:创建、初始化、保存和加载
  12. 自己写的一个分享按钮的插件(可扩展,内附开发制作流程)
  13. PHP添加网站版权信息,如何将版权和作者信息添加到用PHP创建的图像?
  14. 9月第2周网络安全报告:境内感染病毒主机68万个
  15. 运筹学及其matlab应用,运筹学基础及其MATLAB应用
  16. Android MediaProjection 代码分析
  17. 林群院士:从数学谈教育
  18. windows 鼠标突然变成锯齿状
  19. OpenCV 外接矩形框 cv2.boundingRect、cv2.minAreaRect
  20. 数据异常检测方法以及实际应用

热门文章

  1. CCF201403-1 相反数(100分)【序列处理】
  2. HDU5904 LCIS【LCIS】
  3. HDU2537 8球胜负【水题】
  4. CCF201409-3 字符串匹配(解法二)(100分)(废除!!!)
  5. Java 相关计数问题及其实现
  6. matlab 可视化 —— 高级 api(montage)、insertObjectAnnotation、insertMaker
  7. Identity of indiscernibles(不可分与同一性)
  8. Tricks(三十二)—— 遍历全部的子串(子数组)
  9. URL vs URI
  10. mercury无线路由器设置服务器无响应,有了这款路由器,从此卡顿不存在