windcut 发表于 2023-1-16 11:14:02

为什么内部函数可以修改外部函数的列表而不能修改变量

本帖最后由 windcut 于 2023-1-16 11:16 编辑

rt,如果代码这样写
def a():
    cnt = 0
    def b():
      cnt += 1 #修改外部函数的变量
    return b

c = a()
c()
会报错:UnboundLocalError: local variable 'cnt' referenced before assignment
必须使用nonlocal语句才可以修改外部函数的变量

但如果代码这样写
def a():
    cnt = []
    def b():
      cnt.append("qwq") #修改外部函数的列表
    return b

c = a()
c()
可以正常运行,无报错

为什么列表可以修改而变量不可以呢

阿奇_o 发表于 2023-1-16 11:14:03

本帖最后由 阿奇_o 于 2023-1-16 15:16 编辑

阿奇_o 发表于 2023-1-16 11:20
你试试把 cnt += 1 改为 cnt = cnt + 1
^_^
----- 啊也不行?


自己吃饱后,滚回去翻书复习了。。这里涉及几个技术术语和概念:
    作用域、自由变量、闭包、可变数据类型和不可变数据类型

我尝试着解释吧:

作用域,我就不多解释了,分:局部(本地)作用域、全局作用域。(基本就看变量被定义相对位置)
自由变量free variable:指 没有在本地作用域里进行绑定的非全局变量,如你这里b函数作用域里的cnt
闭包closure:是一个"函数"(或叫一种机制),它可以保存自由变量的绑定。
   为什么要这样?—— 因为调用时,因为a函数的return b的缘故,a函数的作用域已经不存在了!
   所以,为了能继续使用原定义的变量绑定,特别地,采用了【闭包】这一函数/机制来保存这些"自由变量"。

但问题是。。如果你在"内部函数"里修改、更新(重新绑定)这些变量 的时候,它就会创建的是"本地变量"(local variable),而非"自由变量"
—— 本地变量,就要遵守本地变量的"先声明后使用"的规则(其实任何变量实质上都要),于是你就看到了那样的报错。   

那 cnt.append('qwq') 的情况,为什么没报错?—— 因为这不是更新或修改cnt,而是更新了其内部的元素。
cnt = [] 是列表,是可变类型,append()并没有改变cnt(它还是它,id没变),所以 cnt 依然是自由变量。

而 对于不可变类型的 cnt = 0 , 后cnt += 1 或 cnt = cnt + 1 这样的情况,就重新绑定cnt了,这会 隐式地创建局部变量,
于是,cnt并没有成为"自由变量",故 执行时,它会报错 UnboundLocalError: local variable 'cnt' referenced before assignment。

其实,你在 cnt.append('qwq') 后,加多一句 cnt = cnt[:] ,执行时,基于上述原因,它同样会报错。
def f2():
    cnt = []
    def b():
      cnt.append(10)
      cnt = cnt[:]
    return b

t = f2()
t()
Traceback (most recent call last):
File "<pyshell#27>", line 1, in <module>
    t()
File "<pyshell#25>", line 4, in b
    cnt.append(10)
UnboundLocalError: local variable 'cnt' referenced before assignment

如果你能理解这个,应该可以"有点明白了"。。 哈哈

ps: 我想我没完全解释清楚。。主要是太细了。。另外也需要先搞明白有些前置知识和概念。。

ps2: 偷偷告诉你,你可以通过 函数对象的 __code__属性来查看其包含的 局部变量和自由变量, 如
t.__code__.co_varnames# 局部(本地)变量有哪些?
t.__code__.co_freevars    # 自由变量有吗?有哪些?

Mefine 发表于 2023-1-16 11:16:01

Python 的变量分可变和不可变

可变:

list
set
dict
不可变:

int
float
tuple
str
对于不可变变量,作为参数的时候,会被复制一份,可以看做是值传递

对于可变变量,作为参数的时候,可以看做是引用传递

阿奇_o 发表于 2023-1-16 11:20:57

本帖最后由 阿奇_o 于 2023-1-16 11:32 编辑

你试试把 cnt += 1 改为 cnt = cnt + 1
^_^
----- 啊也不行?
这就尴尬了。。 待我研究研究。。

Mta123456 发表于 2023-1-17 08:44:24

本帖最后由 Mta123456 于 2023-1-17 08:46 编辑

别忘了变量是全局变量,内部函数只是局部变量,你要么用nonlocal声明,否则只能在内部函数中复制一个一模一样的变量。但列表和变量是有区别的。你在内部函数使用append()方法,并没有改变原来列表的id值,所以列表还是原来的列表,只不过在内部函数中多了一个名称一样但id值不同的列表

月光沙漠 发表于 2023-1-17 09:29:36

本帖最后由 月光沙漠 于 2023-1-17 09:41 编辑

cnt = 0      cnt 指向0的内存地址
cnt+=1      0是字符串,是不可变类型,cnt+=1后,只能在内存中新建一个字符串1,将cnt指向1的内存地址
cnt = []   cnt 指向空列表[]的内存地址,列表是可变类型,append后,列表中的元素变化了,但还是同一个内存地址

你可以在函数中print(id(cnt))来比较一下。
页: [1]
查看完整版本: 为什么内部函数可以修改外部函数的列表而不能修改变量