惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

WordPress大学
WordPress大学
The GitHub Blog
The GitHub Blog
T
Threatpost
人人都是产品经理
人人都是产品经理
大猫的无限游戏
大猫的无限游戏
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
博客园 - Franky
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Apple Machine Learning Research
Apple Machine Learning Research
酷 壳 – CoolShell
酷 壳 – CoolShell
M
MIT News - Artificial intelligence
小众软件
小众软件
Hugging Face - Blog
Hugging Face - Blog
云风的 BLOG
云风的 BLOG
S
Security Affairs
P
Proofpoint News Feed
L
LINUX DO - 最新话题
宝玉的分享
宝玉的分享
S
Security @ Cisco Blogs
H
Hacker News: Front Page
Security Archives - TechRepublic
Security Archives - TechRepublic
Vercel News
Vercel News
Engineering at Meta
Engineering at Meta
Know Your Adversary
Know Your Adversary
Y
Y Combinator Blog
美团技术团队
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
月光博客
月光博客
量子位
博客园_首页
The Last Watchdog
The Last Watchdog
D
DataBreaches.Net
www.infosecurity-magazine.com
www.infosecurity-magazine.com
P
Privacy International News Feed
The Register - Security
The Register - Security
Schneier on Security
Schneier on Security
H
Help Net Security
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
V
Visual Studio Blog
Google DeepMind News
Google DeepMind News
F
Full Disclosure
C
Cyber Attacks, Cyber Crime and Cyber Security
MyScale Blog
MyScale Blog
aimingoo的专栏
aimingoo的专栏
S
Schneier on Security
L
Lohrmann on Cybersecurity
S
Secure Thoughts
Stack Overflow Blog
Stack Overflow Blog
Cloudbric
Cloudbric
Microsoft Security Blog
Microsoft Security Blog

卡瓦邦噶!

服务器高性能网络调优 | 卡瓦邦噶! 为何写作 | 卡瓦邦噶! 读《金阁寺》 | 卡瓦邦噶! 雨季又来 | 卡瓦邦噶! MTU Probe 引起的初始延迟 | 卡瓦邦噶! 3.5 秒的固定延迟问题 | 卡瓦邦噶! 学习网络的一点经验 | 卡瓦邦噶! ARP 问题诊断 | 卡瓦邦噶! 网络断断续续…… | 卡瓦邦噶! Piccolo P2P 镜像分发 | 卡瓦邦噶! 一起看电影 | 卡瓦邦噶! 《征服C指针》 | 卡瓦邦噶! 我的姥姥 | 卡瓦邦噶! Python的哲学 Python 3.5的新特性 学校不教的计算机课 垃圾回收(GC)的三种基本方式 在编程中体验纯粹的快乐 从《美丽新世界》谈自由 在快钱实习 迷人的嗓音和迷人的故事——《Sleepyhead》 Python 的十个自然语言处理工具 记一个愚蠢的bug 一年炉石传说的游戏体验 《以撒的结合:重生》网页版图鉴 分清 C++的指针、引用和数组 笑话三则 自由比皇帝更伟大——《悲惨世界》笔记 Git 10 周年访谈:Linus 讲述背后故事 用 0x3f3f3f3f 设定最大int值的优点 Joel给计算机系学生的建议 一个词法分析器的简单实现 怎样才算健康的生活方式 MacVim 配置攻略 学习培训课程的视频效果好吗? 语言的控制 可悲的大多数 CSS样式思维导图 2014年终总结 奥巴马成为首位写程序的美国总统 青岛老城区的下水道好在哪里? 做优秀 UI 的七个建议(第二部分) 选择爱情的骑士 Git简明教程 Java集合总览 读 《1984》 java问答:终极父类(六)——等待/唤醒和接口 Java 问答:终极父类(五)——toString() Hyperlapse快速视频背后的技术细节 平庸之恶 Java程序员须知的七个日志管理工具 苏州 逛书摊随想 关于考试作弊 你的工作不仅仅是编程 推荐在线学习Java的英文资源 关注女性命运——《千禧年三部曲》 用好你的幻灯片——《演说之禅》 使用ReentrantLock和Lambda表达式让同步更纯净 新手学编程,从哪里开始? 程序员都是工程师吗? 不要学习代码,要学会思考 Java 问答:终极父类(四)——hashCode() Java 问答:终极父类(三)——finalize()和 getClass() 程序员职业之路的选择 我是一名摄影家 写给何小树的城市指南 为什么一些语言会比别的快? Java的常见误区与细节 跟朋友在一起玩游戏 死神永生——读《三体》 五种类型的程序员 创业圣经——读《黑客与画家》 纪念加西亚·马尔克斯 Junit中处理异常的另一种方式:catch-exception Java8采用Martin Fowler的方法创建内部DSL Linux HotSopt虚拟机GC线程的CPU占用率 J2EE概念介绍 如何成为一名黑客 Java 问答:终极父类(二)——equals()方法 为什么我喜欢Java Java 问答:终极父类(一)——clone()方法 七个改变世界的Java项目 java中默认类型转换的小问题 传统与创新 欢迎来到互联网 莫言和马尔克斯——读《生死疲劳》 位运算的妙用 读《人为什么活着》 写博客教会我的事情 中国特色操作系统 2013年总结 《永不妥协》影评 恨不相逢未嫁时——《廊桥遗梦》影评 如何优雅地使用PPT 给明年依然年轻的我们 天才与柱子 黑客守则和黑客精神 Looking for Freedom——《被解救的姜戈》 简洁之道
理解Python的UnboundLocalError(Python的作用域)
laixintao · 2017-08-08 · via 卡瓦邦噶!

今天写代码碰到一个百思不得解为什么会出错的代码,简化如下:

Python

1

2

3

4

5

6

7

x = 10

def func():

    if something_true():

        x = 20

    print(x)

func()

意图很明显,首先我定义了一个全局的x,在函数中,如果有特殊需要,就重新重新赋值一下x,否则就使用全局的x。

可以这段代码在运行的时候抛出这个Error:

UnboundLocalError: local variable ‘a’ referenced before assignment

研究了一番,觉得挺有意思的。而且这是一个比较常见的问题,在Stack Overflow的Python tag下面基本上是个周经问题。


出现赋值就是局部变量!

基本的原理很简单,在Python FAQ中提到了:

在Python中,如果变量仅仅是被引用而没有被赋值过,那么默认被视作全局变量。如果一个变量在函数中被赋值过,那么就被视作局部变量。

Effective Python也提到过:

Python是这样处理赋值操作的:如果变量在当前的作用域已经定义过,那么就会变成赋值操作。否则的话会在当前的作用域定义一个新的变量。(Assigning a value to a variable works differently. If the variable is already defined in the current scope, then it will just take on the new value. If the variable doesn’t exist in the current scope, then Python treats the assignment as a variable definition. The scope of the newly defined variable is the function that contains the assignment.)

重点强调一下,这里的被赋值过,指的是在函数体内任何地方被赋值过。无论是否会被执行到(比如在if语句中),甚至是变量引用之后再赋值(参考下面的代码),都被作为“被赋值过”,都变成了局部变量。

Python

1

2

3

4

5

6

7

In [26]: def test_assignment():

    ...:     print x

    ...:     x = 20

    ...:

In [27]: test_assignment()

UnboundLocalError: local variable 'x' referenced before assignment

其实到这里这个问题的答案已经出来了,只要是在函数体内被赋值过,那么变量就是local的,任何赋值之前的操作都会出现一个RuntimeError。下面会深入解释一下。

赋值操作的编译过程(原理)

Python文档中有关赋值语句提到:

Assignment of an object to a single target is recursively defined as follows. If the target is an identifier (name):

  • If the name does not occur in a global statement in the current code block: the name is bound to the object in the current local namespace.
  • Otherwise: the name is bound to the object in the current global namespace.

就是说,如果赋值操作的变量没有用global声明,那么就将这个name绑定到局部名字空间,否则就绑定到全局名字空间。

我们可以使用symtable这个lib验证一下:

Python

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

import symtable

code = '''

x = 10

def foo():

    x += 1

    print(x)

'''

table = symtable.symtable(code, '<string>', 'exec')

foo_namespace = table.lookup('foo').get_namespace()

sym_x = foo_namespace.lookup('x')

print(sym_x.get_name())  # x

print(sym_x.is_local())  # True

可以看到,x变量确实被绑定到了局部。使用dis库可以看到编译的代码:

1

2

3

4

5

6

7

8

9

10

11

35           0 LOAD_FAST                0 (x)

             3 LOAD_CONST               1 (1)

             6 INPLACE_ADD

             7 STORE_FAST               0 (x)

36          10 LOAD_GLOBAL              0 (print)

            13 LOAD_FAST                0 (x)

            16 CALL_FUNCTION            1

            19 POP_TOP

            20 LOAD_CONST               0 (None)

            23 RETURN_VALUE

其中,LOAD_FAST是从local的stack中读取变量名(LOAD_FAST对之后字节码的优化很重要)。由此可以看到,的确是在局部变量找x没有找到(前面并没有STORE_FAST操作),引发了UnboundLocalError。

所以我的理解是:Python编译建立抽象语法树的时候,根据语法书建立符号表,从语法书的函数体内决定符号是local的还是global(是否出现assignment语句),然后在编译其他语句生成字节码。

那么既然这样,为什么要等到运行的时候才报错,而不是编译的时候就报错呢?

参考下面的代码:

Python

1

2

3

4

5

6

x = 10

def foo():

    if something_true():

        x = 1

    x += 1

    print(x)

如果something_true(),x的赋值就会执行,那么代码不会抛异常。但是编译器并不会知道这个赋值语句会不会执行。换句话说,函数体内出现了赋值语句,但是Python编译过程无法得知赋值语句会不会执行到的。所以只要出现了赋值语句,就将变量视为局部。至于会不会出现未赋值就使用(UnboundLocalError),就运行看看了。

这并不是缺陷,而是一个设计选择。

Python不要求声明变量,但是假设函数体内定义的变量都是局部变量。这比JavaScript好多了,JavaScript也不要求声明变量,但是如果不是var声明的变量,可能在不知情的情况下修改了全局变量。《Fluent Python》7.4

(PS:ES6的 let 也有了类似的机制,叫做“temporal dead zone”,参考

这应该很好理解,试想一下,如果在函数中引用了一个函数内不存在的变量,后面又进行了赋值。而Python将这个变量当做全局变量,那么可能隐式地给你覆盖了全局变量。这如果是debug起来肯定是个噩梦。

这种设计选择正是提现了Python的设计哲学:“Explicit is better than implicit.”

解决方法

前面已经提到了,显示地指定使用global就可以,这样即使出现赋值,也不会产生作为local的变量,而是去改变global的变量。

但是依然存在一个问题:

Python

1

2

3

4

5

6

7

8

9

def external():

    x = 10

    def internal():

        global x

        x += 1

        print(x)

    internal()

external()

external的x既不是local,也不是global。这种情况应该使用Python3的nonlocal。这样Python不会在当前的作用域找x,会去上一层找。

可惜Python2不支持nonlocal,但是我们可以使用“闭包”来解决。其实思想就是,如果我们无法改变不可变的对象,就将这个对象变成可以改变的对象。

Python

1

2

3

4

5

6

7

8

def external():

    x = [10]

    def internal():

        x[0] += 1

        print(x)

    internal()

external()  # [11]

如上代码,x不是一个不可改变的int,而是一个可变的list对象。这样x[0] += 1就会变成一个赋值操作,而不会申请新的变量。

2018年8月30日更新:最近读《代码之髓》这本书,对 Python 的作用域以及它的行为有了新的理解。Python 是静态作用域的,而且变量无须声明,赋值即声明。像 Perl,JavaScript 这样的需要是需要声明的,比如带上 var 就是局部变量,否则就是全局变量。Python 这种赋值即声明的方式,好处就是我们在写的时候很爽,一般都符合我们的直觉。缺点就是在嵌套函数内部如果想要赋值,那么依据“赋值即声明”,我们就会创建新的变量,而不会去修改外部函数的变量。

与之类似的语言是 Ruby,在 Ruby 中同样“赋值即声明”,不过行为却与 Python 恰恰相反。

在 Ruby 中,如果嵌套方法,外部方法的变量在内部方法中依然视作外部方法的变量;如果在内部方法创建变量,那么只会存在于内部方法中,不会影响外部方法。通俗一点,如果内部方法对一个变量 a 赋值的话,如果外部方法有 a ,那么外部方法的 a 的值会被修改;否则,会在内部方法创建一个 a,内部方法结束之后,a 就不存在了。一下代码为例:

Ruby

1

2

3

4

5

6

7

8

def foo()

    x = "old"

    lambda {x = "new"; y= "new" }.call  # 相当于一个内部方法

    p x   # 外部的 x 被修改成 "new"

    p y   # y 是内部方法创建的,不存在于外部方法中,报错

end

foo

参考资料

  1. Understanding UnboundLocalError in Python
  2. Effective Python:Item 15: Know How Closures Interact with Variable Scope