Python 作用域问题,int 型变量为什么会有些特殊呢

2019-12-08 19:04:02 +08:00
 whoops

是这样的,做练习时用闭包实现一个计数器,使用整型变量会报错

UnboundLocalError: local variable 'cnt' referenced before assignment

代码如下:

def counter():
   cnt = 0
   def add_one():
       cnt += 1
       return cnt
   return add_one
a=counter()
print(a()) 
#把整型变量换成列表就可以
def counter():
   cnt = [0]
   def add_one():
       cnt[0] += 1
       return cnt[0]
   return add_one
a=counter()
print(a()) 

初学不才,请教一下大家

5740 次点击
所在节点    Python
43 条回复
silkriver
2019-12-08 19:24:43 +08:00
内层作用域里要先写 nonlocal cnt
lspvic
2019-12-08 19:28:11 +08:00
在 add_one 中加上 nonloccal cnt
add_one 中第一句相当于 cnt=cnt+1,
函数中有赋值语句必须申明为 global 或 nonlocal 才表示为全局或对应作用域的变量,否则是为本地变量
所以表现为本地变量 cnt 在声明前引用
lucays
2019-12-08 19:28:30 +08:00
list 是全局的,int 是局部的
NeinChn
2019-12-08 19:42:53 +08:00
+1,感觉 Python 有点 trick
直观看出来的区别就是前者没办法创建 func_closure,后者创建了 func_closure,并且在 cell 内引用了 counter 内的数组
具体为啥两者会有区别,这个估计得从 Python 的实现 /定义上看
Herobs
2019-12-08 19:52:05 +08:00
#2 已经解释了,但是为什么列表可以,因为实际上列表的例子里并没有修改列表本身,只是调用了他的一个方法 __setitem__
111qqz
2019-12-08 19:54:34 +08:00
可以参考 fluent python 的第七章"the nonlocal declaration"一节.

![python_fluent.png]( https://i.loli.net/2019/12/08/h9exqwcN4JSBLkg.png)

区别在于 int 是 immutable 的,而 list 不是.
crella
2019-12-08 20:07:29 +08:00
如果是 ruby 的话,两种方法都不可以。不管是数组还是普通变量都要声明为全局~
superrichman
2019-12-08 20:28:32 +08:00
@lucahhai list 并不是全局的
我用 globals()和 locals()把外层函数和内层函数的全局变量和本地变量都打出来看了一下.

内层函数可以直接访问到外层的可变对象(list dict set 等), 但是无法访问到不可变对象(str int tuple 等). 还有就是如果外层函数有参数(比如 def counter(num) 的 num), 内层函数也可以访问到这些参数.

从测试结果上看, 外层函数的参数和可变对象会自动变成内层函数的本地变量. 如果要在内层函数访问外层的不可变对象, 需要用 nonlocal 进行修饰, 或者把不可变对象存到可变对象里进行间接的传递(就像第二个函数一样, 不是直接传 0 而是把 0 放进 list).
NeinChn
2019-12-08 20:47:37 +08:00
@superrichman
int/str/tuple 也可以被传递到闭包内的,只要不修改的情况下,打印 locals 可以看到
ethego
2019-12-08 21:34:33 +08:00
python 最开始并没有正确实现闭包,并不是 int 类型的问题,在 python 3 下可以在闭包内使用 nonlocal 声明闭包变量。
wuwukai007
2019-12-08 21:57:55 +08:00
就相当于全局变量,函数内修改要声明下,计数器用生成器实现更简单吧
superrichman
2019-12-08 22:15:44 +08:00
@NeinChn 不, 看不到的, 我试过了

def counter():
cnt = 0
name = 'counter'
cals = (2, 3, 4, 5, 6)
print('outer locals', locals())

def add_one():
print('inner locals', locals())
return add_one

a = counter()
a()

输出结果

outer locals {'cnt': 0, 'name': 'counter', 'cals': (2, 3, 4, 5, 6)}
inner locals {}
NeinChn
2019-12-08 22:21:15 +08:00
@superrichman 因为你没用到。
你的 add_one()里面 print 一下 cals,locals 就会有值了
superrichman
2019-12-08 22:36:32 +08:00
@NeinChn 真的可以诶, 好神奇. 用 print 的时候能读到外层的 cals, 并且之后能在 lcoals 能看到, 但是要对 cals 操作就抛异常. emmm... 这是把外层的不可对象当成了只读的数据? 有点迷.
NeinChn
2019-12-08 22:47:24 +08:00
@superrichman 猜测就是不能修改引用而已。只是 Python 的异常太迷了。
love
2019-12-08 23:08:36 +08:00
这个挺好理解的吧,主要是 python 没有一个类似 js 的 var 声明变量操作符,而是把首次赋值就当成是当前作用域变量声明了,所以内层的函数相当于新声明了一个变量。
ipwx
2019-12-08 23:43:47 +08:00
@ethego 什么鬼说法。
@NeinChn 这和引用无关,因为 Python 根本没有“引用”这个概念。
@superrichman 一切的关键都在于你在第一个例子里面,cnt 这个 **标识符** 在 add_one 局部函数里面,相对于外部 counter 函数,它的含义发生了变化。标识符变成了一个新的变量,而你此时 cnt += 1 相当于 cnt = cnt + 1,在右值表达式里面引用了这个新的变量。而这个新的变量在这句话执行完之前还没有创建,所以就会出错。

但是第二个例子里面,add_one 函数里面的 cnt 这个 **标识符** counter 函数里面的含义是一致的。cnt[0] += 1 相当于 cnt[0] = cnt[0] + 1,在右值表达式里面,此时 cnt 这个标识符存在,所以可以引用。
- - - -

**标识符** 这个概念就是 **标识符**,既不是“引用”,也不是“变量”,而就是“标识符”。要深刻理解这一点,你们可能需要一点编译原理,以及写编译器的实践过程。
ipwx
2019-12-08 23:45:20 +08:00
@superrichman 你在第一个例子里面,遇到的报错,就和你随手写 a=a 或者 a+=1 一样的。都是 a 这个变量还没有存在。
ethego
2019-12-09 00:08:25 +08:00
@ipwx 什么叫什么鬼说法,Python 2 没有正确实现闭包是众所周知的事实。
ethego
2019-12-09 00:14:54 +08:00
所以作为对函数上下文查找的修正,在 global 和 local 之间加入了 nonlocal 的关键字,来在不破坏兼容下实现正确的闭包 t 实现,即递归向上层 context 查找变量。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/627037

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX