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

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

1

2

3

4

5

6

7

x=10

deffunc():

ifsomething_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语句中),甚至是变量引用之后再赋值(参考下面的代码),都被作为“被赋值过”,都变成了局部变量。

1

2

3

4

5

6

7

In[26]:deftest_assignment():

...:printx

...:x=20

...:

In[27]:test_assignment()

UnboundLocalError:localvariable"x"referencedbeforeassignment

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

importsymtable

code="""

x = 10

def foo():

x += 1

print(x)

"""

table=symtable.symtable(code,"","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

350LOAD_FAST0(x)

3LOAD_CONST1(1)

6INPLACE_ADD

7STORE_FAST0(x)

3610LOAD_GLOBAL0(print)

13LOAD_FAST0(x)

16CALL_FUNCTION1

19POP_TOP

20LOAD_CONST0(None)

23RETURN_VALUE

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

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

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

参考下面的代码:

1

2

3

4

5

6

x=10

deffoo():

ifsomething_true():

x=1

x+=1

print(x)

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

但是依然存在一个问题:

1

2

3

4

5

6

7

8

9

defexternal():

x=10

definternal():

globalx

x+=1

print(x)

internal()

external()

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

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

1

2

3

4

5

6

7

8

defexternal():

x=[10]

definternal():

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 就不存在了。一下代码为例:

1

2

3

4

5

6

7

8

deffoo()

x="old"

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

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

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

end

foo

参考资料

Effective Python:Item 15: Know How Closures Interact with Variable Scope

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/349625.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

threadlocals_如何使用ThreadLocals射击自己

threadlocals它将很好地启动。 像大多数故事一样。 您会发现一个新概念,并对其功能感到惊讶。 然后突然装备了这把新锤子,一切开始看起来像钉子。 根据我们过去几个月的经验, java.lang.ThreadLocal真是一锤定音。 我想这全都归结为ThreadLo…

ASP.NET Core Web API

1.简单介绍 ASP.NET Core Web API 是 ASP.NET Core MVC 的一个功能。ASP.NET Core MVC 包含了对 Web API 的支持。可以构建多种客户端的 HTTP 服务。ASP.NET Core Web API可用于在 .NET Core 上构建 RESTful 应用程序。 框架包含对 HTTP 内容协商的支持,内置支持以 …

win8配置_《FIFA 20》PC配置公布 最低仅需i3+GTX660

EA公布了新作《FIFA 20》的PC配置需求,玩家最低仅需 i3-2100 GTX 660 显卡即可进行游戏,推荐配置则为 i3-6300T GTX 670 显卡,考虑到本作的逼真画面效果,这配置可以说是非常亲民,另外硬盘空间需要大约 50 GB。最低配…

Thymeleaf + Spring中的验证

总览 我们将要讨论的重要主题涉及空值,空字符串和输入验证,因此我们不会在数据库中输入无效数据。 在处理空值时,我们使用了Java 1.8中引入的java.util.Optional 。 0 – Spring Boot Thymeleaf示例表单验证应用程序 我们正在为一所大学构…

技术管理规划-设定团队的职能

背景 职责 团队是干什么的 初步自查团队 1.公司为什么给我团队?希望我产出什么?完成对除了c端健康领域探索的研发任务,产出技术类产品 2.团队存在的独特价值是什么?研发过lx健康这款基础app,研发能力强,熟悉硬件相关技…

开发转测试没人要_前端开发,测试,后端,该如何选择?

一般来说前端会比后端简单一些的,初学者或者转行可能考虑前端多一点,但是后端开发的薪水又比前端高一些,就是比较枯燥。前端开发我目前一直在自学前端,从网上找资料,然后听课,只要是对编程有兴趣&#xff0…

技术管理规划-如何设定团队的目标

团队管理规划有4个互相关联的要素: 职能目标团队路径在未来的3个月,6个月,1年,2年,3年,5年中,你希望带着你的团队抵达一个什么样的目的地,也就是团队的目标。 更加清楚目标意味着什么…

a8处理器相当于骁龙几_天玑820相当于骁龙什么处理器?天梯图秒懂联发科天玑820性能排名...

5月18日,联发科发布了全新 天玑820处理器,号称目前最强中端芯,受到不少网友关注。联发科近年来在手机CPU市场沉寂多年,今年异常给力,先后发布了天玑1000/L、天玑800等多款处理器,采用了全新的命名方式&…

hibernate 继承_Hibernate继承:每个类层次结构的表

hibernate 继承在本教程中,我们将了解如何在hibernate中实现继承。可以通过3种方式在hibernate中实现继承。在本文中,我们将看到其中一种,即每个类层次结构一个表。 Hibernate中的继承: Java是面向对象的语言,继承是J…

vue - cli 脚手架安装

一、 node安装 1)如果不确定自己是否安装了node,可以在命令行工具内执行: node -v (检查一下 版本); 2)如果 执行结果显示: xx 不是内部命令,说明你还没有安装node , node 安装地址…

python人工智能_人工智能福利丨Python核心语法实战

Python已正式跻身成熟语言行列,成为整个互联网的基础性语言之一,并以肉眼可见的速度,在全球攻城略地:——牢牢占据TIOBE世界编程语言排行榜第四名,且保持上升趋势——国家级人工智能四大平台确立——正式纳入全国计算机…

.NET Core 单元测试

应用程序测试的类型很多,包括集成测试,Web 测试,负载测试等。在最底层的是单元测试,此测试可以测试单个软件组件或方法。单元测试一般只测试开发人员的代码,不应该测试基础结构普、问题,如数据库&#xff0…

是什么原因导致OutOfMemoryError?

发生以下情况之一时,可能会引发OutOfMemoryError : JVM耗尽了本机内存 Java堆内存不足 PermGen或Metaspace内存不足 JVM花太多时间试图收集垃圾 通常可以从错误消息中OutOfMemoryError出OutOfMemoryError的根本原因。 让我们研究每种情况的细节。 …

win10电脑开机密码忘了怎么办_Mac电脑忘记开机密码怎么办?Mac开机密码快速恢复方法...

Mac忘记开机密码怎么办?虽然小编觉得大多数人应该都不会Mac忘记开机密码,但是如果真的有人忘记了怎么办呢?小编这里教你们一种方法,可以帮你1分钟快速恢复Mac电脑忘记开机密码,感兴趣的朋友快跟着小编一起来看看吧&…

技术管理规划-如何规划团队的架构

管理规划的4个要素 1.职能【清楚自己团队的基本职责和使命】 2.目标【为团队设定清晰的目标】 3.团队【团队的架构规划】 4.路径 团队目标 根据团队目标去梳理团队 团队目标: 某个时间节点,团队发展成什么状态。 要点说明规模实际人数和预算人数分工团队…

win10家庭版调出组策略_利用powershell为win10家庭版安装组策略

虽然win10家庭版阉割了组策略管理器,但至少到1607版本时还可以用cmd脚本安装它。可是随后win10更新了几个大版本,不知为何,cmd控制台常常会有诡异的Bug,导致无法用老办法安装组策略管理器。为了彻底解决这个问题,不妨另…

Spring批处理CSV处理

总览 我们将讨论的主题包括使用Spring Batch进行批处理的基本概念,以及如何将数据从CSV导入数据库。 0 – Spring Batch CSV处理示例应用程序 我们正在构建一个应用程序,该应用程序演示Spring Batch处理CSV文件的基础。 我们的演示应用程序将允许我们处…

NOIP模拟测试28「阴阳·虎·山洞」

写这几个题解我觉得我就像在按照官方题解抄一样 阴阳 题解 将题目中给的阴阳看作黑色和白色 首先我们观察到最后生成图中某种颜色必须是竖着单调递增或竖着单调递减 类似这样 否则不满足这个条件 但合法染色方案必须满足任意两个同颜色格子之间的格子也必须是该颜色。 然后我们…

linux设置环境变量_什么是linux环境变量

本来这篇文章好几天之前就写好了,但是媳妇儿跟我说工作日就不要发了,大家都在上班,哪有闲心思看你的文章。哎,可能大家用头条都是在放松刷娱乐,看小姐姐。所以就一直拖到现在。周末了,更是放松的好时候&…

理科卷math·english·chinese·biology·chemistry·physics

一套比一套炸,果然我只会做B卷,虽然我B也很差但没差到这种地步 $math$ 题解 看似没法做但总会有突破口 $70\%$ 发现和小凯的诱惑很像,于是看$gcd$是否为$1$只要为$1$可以凑齐所有数 $n^2$枚举两两$gcd$ $80\%$ 我考试时思路 找到每一个数和$mod$的$gcd$,发现只要是任一$gcd$倍数…