理解Python的UnboundLocalError(Python的作用域)

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

意图很明显,首先我定义了一个全局的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语句中),甚至是变量引用之后再赋值(参考下面的代码),都被作为“被赋值过”,都变成了局部变量。

其实到这里这个问题的答案已经出来了,只要是在函数体内被赋值过,那么变量就是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验证一下:

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

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

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

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

参考下面的代码:

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

 Python为什么要这样处理?

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

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的变量。

但是依然存在一个问题:

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

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

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

 

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

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

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

 

参考资料

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

使命召唤全系列游戏汇总

整理了一下使命召唤所有的游戏,可以这么理解,使命召唤火起来的时候基本是从4(也就是现代战争开始),此后Activision(美国动视)每年发布一款游戏,手下有三个小组负责了现代战争、二战、黑色行动三个不同的支线,每年发布一款。所以基本上是间隔发布。这是理解整个使命召唤游戏剧情的基本,不然三条线穿插在一起都乱掉了。

作品序号
年份和开发小组
现代战争
二战
黑色行动 独立
1
2003年10月29日
使命召唤
2
2005年10月25日
使命召唤2
3
2006年11日7日
使命召唤3
4
2007年11月7日
Infinity Ward
5
2008年11月11日
Treyarch
6
2009年11月10日
Infinity Ward
使命召唤:现代战争2
7
2010年11月19日
Treyarch
8
2011年11月8日
Sledgehammer Games、Infinity Ward、Raven Software
9
2012年11月13日
Treyarch
10(登录ps4)
2013年11月5日
Infinity Ward、Raven Software、Neversoft
11
2014年11月4日
Sledgehammer Games
12
2015年11月6日
Treyarch
13
2016年11月4日
Infinity Ward
14
2017年11月3日
来源:维基百科
 

对去中心化网络的信仰

《美剧》硅谷中提到了一种去中心化的网络:人们不再是从网络的中心的服务器上获得资源,网络的资源存在于每个人的设备上。

我比较认同这种互联网,人才是互联网的真正用户,而不是服务器。互联网连接的应该是人与人,而不是人与服务器。

目前的中心化网络存在很多问题。比如审查,在现在的中国基本上不可能在国内的网络上讨论任何政治问题,在知乎、微博等国内社交媒体上存在大量“被删除的内容”,这已经让互联网失去和很大一部分意义。互联网为了分享知识和沟通而诞生,一个理想的互联网应该是可以自由发布任何内容、并且任何资源都不会被删除的。但是目前的互联网却走了一条错误的道路,集中化的网络让政府或者网站所有人对删除资源有了可能。但是设想一下,如果我们的网络是分布式的呢?如果网络存在于任何一个人的机器上,这样任何东西都不会被删除,一份资源可能有无数个冗余备份。Git和Bt就是一个成功的例子,github上有被勒令删除的仓库,但是因为git是分布式的,如果一个仓库足够流行,那么就会有无数份fork,官方仓库可以删除,项目却会永远存在。另外,去中心化的网络有着与生俱来的匿名性,资源从一台计算机到另一台计算机,发布者隐藏自己的身份轻而易举,如果不能保证匿名,如果保证言论自由呢?

安全问题。首先是文件的安全。一切存储介质都是有寿命的,目前可以想像到的最安全的存储方式就是放到云上。个人存储无论有多少冗余备份都觉得不安全,比如说家里着火,就可能把所有的磁盘损坏了。但是如果把文件通过BT的方式加密存储在别人的设备上(或者将适合共享的资源共享出去),可以免费使用很多备份,如果要使这份资源消失,基本上是不可能的(可能有一些隐患,但是这不正是互联网的构想吗?)。对于网站运营者来说,集中式的(比如说一台网站服务器)总有可能沦陷,通过各种各样的漏洞拿到服务器权限,那么攻击者基本上是占有了所有的内容。但是分布式提供了完全不同的另一种的认证机制。除此之外,还有DDoS的问题。DDoS目前基本上是不可解的,无论你有怎么样的策略,机房上层网络发现有巨大流量攻击的时候会采取最笨、也是最暴力的方式应对:将发送给你的网络包直接丢掉,让你的网站下线一段时间,等攻击结束,再恢复响应。如果你的业务足够大,可能还有和机房或运营商谈判的筹码,但是对于中小业务,人家根本不理你,直接给你切断。但是分布式的网络不会存在DDoS,况且如果网络涉及的优秀,你可以去离你最近的节点获取资源,相当于一个无形的超级CDN了(个人设想)。

节省资源。云存储是很昂贵的,带宽也很贵。集中化的网络避免不了要将所有的流量发往一处,将资源存放在一处,比如Google目前维护DC(数据中心)的一年费用大约是120亿,虽然有文章说,个人存储的成本更高,但是不要忘了,分布式的存储是利用的空闲空间!相比于集中式,去中心必然需要更多的冗余,但是却可以利用个人的空闲空间,个人与个人之见的空闲带宽。我认为成本肯定比集中的数据中心低。

目前,很多去中心化项目已经证实了分布式的成功:比特币证明了去掉中心服务器核准交易是可行的,Git证明了分布式的版本控制是可靠、成本低的,BT虽然在很多国家被禁止,但是目前依然有相当多的用户在使用,并且速度也尚可。

我也承认,去中心化的网络会带来一系列的社会问题,例如地下交易。技术是有两面性的,同时也是无罪的,但是不能因为社会问题而阻止技术的发展,当然这也是不现实的。

我相信将来,随着个人存储成本继续降低(摩尔定律),网络带宽越来越大(4G,5G),以及压缩技术、分布式技术的发展,总有一天,我们会看到一个理想的、完美的、真正的互联网。

 

路由器折腾记(1):刷小米青春版路由器

周五上班的时候听同事说,小米青春版的路由器也可以刷openwrt的固件,就忍不住想试一下。

小米青春版非常小巧,整个外表只有一个灯,很漂亮,也比较稳定,大约去年这个时候买的吧,服役一年了,我比较喜欢这个路由器。

在网上查了一下,之前了解到的小米青春版不能刷是因为官方并没有开放ssh,即使你绑定了路由器,在官方的获得ssh root密码页面依然是空白的。但是通过查了一些资料,发现小米路由器存在一个漏洞,可以让你在本地直接修改root密码,拿到权限。具体的步骤如下:

修改ssh密码

登录小米路由器后台,输入管理密码,这个时候网址如下:

http://192.168.31.1/cgi-bin/luci/;stok=<你的stok>/web/home#router

看来这个stok就是登录之后认证权限的。然后把地址替换成下面的。

http://192.168.31.1/cgi-bin/luci/;stok=《你的stok》/api/xqsystem/set_name_password?oldPwd=《你当前的后台管理密码》&newPwd=《新密码》

然后会返回{"code": 0},如果正常的话。看来小米路由器的后台认证只是简单的判断是否登录,只要登录就可以修改root密码啦。

网上的人是这么说的,但是我实际操作的时候,总是遇到下面这个返回结果:{"msg":"参数错误","code":1523},在试了好几次之后都是这样,我开始怀疑,是不是小米后来把这个漏洞补上了。于是打算重新刷一个旧版的官方固件:miwifi_r1cl_all_59371_2.1.26.bin。

果然,刷完这个之后返回了期待已久的code0.

(有点奇怪,既然小米路由器存在ssh这个功能,那官方为什么要把密码藏的这么深呢直接告诉用户不就得了,这又不是ps4破解之后后果不堪设想)

登录了小米的ssh,我发现这是个Linux啊,简直是学好Linux,走遍天下都不怕。(XiaoQiang什么鬼)

刷入潘多拉固件

对于Linux用户来说接下来的操作就很简单了,从网上下载了PandoraBox-ralink-mt7620-xiaomi-mini-squashfs-sysupgrade-r1024-20150608.bin,然后将文件scp到/tmp里面,执行mtd -r write /tmp/PandoraBox-ralink-mt7620-xiaomi-mini-squashfs-sysupgrade-r1024-20150608.bin开始刷入操作。

至此,我的小米青春版就正式退役了……

因为……

我下载错了固件,上面这个固件是小米3的,并不是青春版的固件……

再见……我的青春……

然后我在淘宝下单了极路由1S,期待下周可以到货,没想到今天就到了,于是开始拿这个刷Openwrt。

使用极路由1s刷Openwrt

首先,打开路由器的后台管理界面。准备开启ssh。

好吧朋友们我们15天后第二期见……

 

Python正则表达式解惑

其实我已经使用Python的正则表达式很久了,今天看了Pycon2017的一个speech[2],又加深了理解。这是一个很简单的Speech,概括了Python正则表达式的用法以及一些Best Practice。

其实正则表达式常用的就那几种,熟悉了就很强大了:

  1. [ ] 表示都可以匹配,比如[abc]匹配a或b或c
  2. {1,3}表示重复1-3次
  3. [^abc]表示非abc
  4. 一些常用的匹配比如.*?表示非贪婪地匹配任何东西

现在已经有很多正则表达式教程了(包括基本上任何Python入门教程都会涉及正则表达式),这里就不多说。我对Python的正则有几个疑惑,在这个视频里面找到了答案。

一、永远使用r前缀。

python的r前缀表示原字符串(raw_string),就是字面意思的字符串,\并不表示转移。比如说r’\n’表示的是一个反斜杠,一个字母n。如果不加r即’\n’,那么\就表示转移,\和n一起表示一个换行符。可以理解为r'\n' == '\\n'

二、正则表达式中需要转义的字符。

三、Python正则表达式groups和group的区别。

详细解释在我的这个gist:https://gist.github.com/laixintao/1a550e98726ddbada0268f6b6a6490cf

group – 是获得所有的匹配,第0个是整个匹配的字符串,后面的是子匹配

groups – 是获得所有的子匹配,接受一个参数作为default。类似dict的get用法。

参考资料:

  1. PyCon Slides: https://drive.google.com/file/d/0BxJ4y96AC8m3NEpreVZLcDl0Nlk/view
  2. PyCon youtube: https://www.youtube.com/watch?v=abrcJ9MpF60&t=205s