鱼C论坛

 找回密码
 立即注册
查看: 1441|回复: 3

[已解决]python闭包

[复制链接]
发表于 2019-4-11 21:43:03 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能^_^

您需要 登录 才可以下载或查看,没有账号?立即注册

x
本帖最后由 牧码人 于 2019-4-11 22:02 编辑
def f1():
        x = 1
        def f2():
                x= x + 2
        return f2()

print(f1())

如上,当函数运行到局部函数f2()中x=x+2表达式时,会报错,UnboundLocalError: local variable 'x' referenced before assignment
局部变量x未定义。

python中,如果局部函数对外部函数进行了赋值操作,也就是说外部函数的变量被放在局部函数的一个等式中,那么python会新建一个跟外部函数一样名字的变量来进行赋值操作,
由于新建的变量并未定义,所以这个时候会报错。这就是python的屏蔽机制。

上述问题在python2中的解决方式是使用容器变量,如下
def f1():
        x = [1] #在外部函数中声明变量x为一个列表。
        def f2():
                x[0] = x[0] + 2
                return x
        return f2()

print(f1())

我听到的解释是,因为函数存储在栈中,容器变量跟函数不是一个存储空间,所以容器变量在嵌套函数中不会被屏蔽。

我的理解是,这里的容器变量实际上就是说的python的可变数据类型(字典,列表,元组,集合),而python的不可变数据类型指的是int,string,float.
可变数据类型:改变引用的值,内存地址不会改变。
不可变数据:改变引用的值,内存地址也会随之改变。

这里,我可不可以理解为,因为只是改变了容器的数据,但是实际上容器的内存地址并没有改变,python中的定义实际上就是变量的分配内存地址。
>>> a = [1,2,3]  #假定一个列表
>>> id(a)
1742351783816  #这里是列表[1,2,3]的引用a指向的内存地址。
>>> id(a[0])
140712228545360 #这里是列表的第一个元素的内存地址。
>>> a.append(4)  #将列表a添加元素4
>>> a
[1, 2, 3, 4]
>>> id(a)
1742351783816  #a指向的内存地址没有变,因为并没有新建对象。
>>> a = [1,2,3,4] #这里再次对a进行赋值,那么内存地址会不会不变呢?
>>> id(a)
2937274002504     #内存地址既然发生了改变,看来这里的[1,2,3,4]是一个新的对象,看来python只要对对象进行赋值操作,都会开辟一个存放新对象的内存空间。

很明显,容器变量的地址与其内部的数据的地址不是一个地址。

我们继续,如果变量的内存地址被定义了,那么接下来就不需要定义它。这里因为用的是容器变量,在外部函数中列表x的内存地址已经被声明了,所以接下来
在局部函数中不需要再次对列表x定义。但是如果,用的不是容器变量,而是可变数据类型,局部函数对外部函数变量的赋值操作会改变其内存地址,也就是
新建了一个对象,任何新建的对象都需要将其定义。
>>> b = 3
>>> id(b)
140712228545424  # 3的内存地址
>>> b = 4
>>> id(b)
140712228545456 # 改变变量值后,引用b从之前的3指向了现在4的内存地址。

下面的方式函数也可以成功输出3.
def f1():
        x = 1
        def f2():
                #x= x + 2
                return x+2   #这里并不叫对x赋值,改成 return x+2 也可以成功执行,因为x还是原来的x,内存地址没有变。参与了=号的x才叫被赋值,才会改变其内存地址。
        return f2()

print(f1())

python3中,可以借助nolocal对局部函数的外部变量进行声明,nolocal x,可以达到被屏蔽的效果。

不好意思,让大家看了这么久,还没有抛出我的问题。哈哈哈,这里我有两个问题:

小甲鱼说上述例子中容器变量x不是存放在栈中,所以可以避免被屏蔽,但是并没有具体说容器变量存放在哪里,python中的栈是一个实体还是一个虚拟的概念?

变量一旦被重新赋值,其内存就会被回收,假设这里的变量引用计数为1.大家看下面的例子
>>> b = 3   
>>> id(b)
140712228545424    #对象3的地址
>>> b = 4
>>> id(b)
140712228545456    #对象4的地址,同时引用b已经从原来对象指向对象3的地址变为指向对象4
>>> id(3)
140712228545424    #为什么这里的对象3的地址还在呢?而且跟之前在内存中的存放地址是一样的,不是会被回收么?
>>> id(1)                #这里并没有给对象1定义,但是对象1依旧被存放在内存中的一个地址
140712228545360
>>> a = 1
>>> id(a)
140712228545360

哪道说python中的存储数值的地址不会被回收?难到在python中所有的数值在被定义前都已经有了自己的地址?后来我把IDLE关掉了,重新打开IDLE后,
对象1的内存地址发生了改变,如下。
>>> id(1)
140712229004112

这是为什么呢?
最佳答案
2019-4-11 22:03:07
小甲鱼说上述例子中容器变量x不是存放在栈中,所以可以避免被屏蔽,但是并没有具体说容器变量存放在哪里,python中的栈是一个实体还是一个虚拟的概念?
----可以换一个方式理解,列表是可变变量,函数中是可以局部修改其值的。例如:
x = [1]
def f():
    x.append(3)
    print(x)
f()
print(x)
[1, 3]
[1, 3]
但重新赋值是不行的:
x = [1]
def f():
    x = [3]
    print(x)
f()
print(x)
[3]
[1]

第二个问题的理解要从python的赋值语句谈起,赋值就是贴标签,a=1,就是在内存中先创建一个值1,再给它贴上标签a,id(3)虽然3没有赋值给一个变量,但已经创建出来了,id(3)以后又马上回收掉。
例如
>>> id(12134)
2812284612816
>>> id(12134)
2812284612880
两次的地址不同,顺便说一下,python为了方便预先在内存中创建了-5~255的值,即使不使用也不会被回收掉。
重新打开idle创建的地址是不同的。
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

发表于 2019-4-11 22:03:07 | 显示全部楼层    本楼为最佳答案   
小甲鱼说上述例子中容器变量x不是存放在栈中,所以可以避免被屏蔽,但是并没有具体说容器变量存放在哪里,python中的栈是一个实体还是一个虚拟的概念?
----可以换一个方式理解,列表是可变变量,函数中是可以局部修改其值的。例如:
x = [1]
def f():
    x.append(3)
    print(x)
f()
print(x)
[1, 3]
[1, 3]
但重新赋值是不行的:
x = [1]
def f():
    x = [3]
    print(x)
f()
print(x)
[3]
[1]

第二个问题的理解要从python的赋值语句谈起,赋值就是贴标签,a=1,就是在内存中先创建一个值1,再给它贴上标签a,id(3)虽然3没有赋值给一个变量,但已经创建出来了,id(3)以后又马上回收掉。
例如
>>> id(12134)
2812284612816
>>> id(12134)
2812284612880
两次的地址不同,顺便说一下,python为了方便预先在内存中创建了-5~255的值,即使不使用也不会被回收掉。
重新打开idle创建的地址是不同的。
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2019-4-11 22:40:53 | 显示全部楼层
本帖最后由 牧码人 于 2019-4-11 22:42 编辑
冬雪雪冬 发表于 2019-4-11 22:03
小甲鱼说上述例子中容器变量x不是存放在栈中,所以可以避免被屏蔽,但是并没有具体说容器变量存放在哪里,p ...


谢谢大牛细心解答
x = [1]
def f():
    x.append(3) #这个操作并没有改变列表x的内存地址,但是重新赋值的话x=[3]就会改变了,两者的操作对象不一样,一个是对列表的元素操作,一个是对列表操作,也就是说python只有在对对象的最外层引用进行赋值操作(有等号出现)时才会触发屏蔽机制,不可变数据最外层对象就是其自身,没有内层对象,可变数据类型最外层也是其自身,但是对内层对象的操作不会触发屏蔽机制,会直接改变其数据。这回我算是加深理解了。
    print(x)
f()
print(x)
另外,容器变量不是存放在栈中,那它存放在哪呢?python的函数是存放在栈的,栈是实体还是虚拟概念?



关于问题的解答,我做了一下复盘,就像您说的,python给-5~255的值即使不适用也不会被回收掉.
>>> id(1)
140712175199056
>>> id(1)
140712175199056 #两个1的内存地址一样
>>> id(300000)
2362364578608
>>> id(300000)   #两个300000的内存地址不一样, 前一个被回收了。
2362364576688

想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2019-4-11 22:48:54 | 显示全部楼层
另外,容器变量不是存放在栈中,那它存放在哪呢?python的函数是存放在栈的,栈是实体还是虚拟概念?
这个我不太明白,还是得请教小甲鱼。
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Archiver|鱼C工作室 ( 粤ICP备18085999号-1 | 粤公网安备 44051102000585号)

GMT+8, 2025-1-13 07:50

Powered by Discuz! X3.4

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表