牧码人 发表于 2019-4-11 21:43:03

python闭包

本帖最后由 牧码人 于 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 = #在外部函数中声明变量x为一个列表。
        def f2():
                x = x + 2
                return x
        return f2()

print(f1())


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

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

这里,我可不可以理解为,因为只是改变了容器的数据,但是实际上容器的内存地址并没有改变,python中的定义实际上就是变量的分配内存地址。

>>> a = #假定一个列表
>>> id(a)
1742351783816#这里是列表的引用a指向的内存地址。
>>> id(a)
140712228545360 #这里是列表的第一个元素的内存地址。
>>> a.append(4)#将列表a添加元素4
>>> a

>>> id(a)
1742351783816#a指向的内存地址没有变,因为并没有新建对象。
>>> a = #这里再次对a进行赋值,那么内存地址会不会不变呢?
>>> id(a)
2937274002504   #内存地址既然发生了改变,看来这里的是一个新的对象,看来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 =
def f():
    x.append(3)
    print(x)
f()
print(x)


但重新赋值是不行的:
x =
def f():
    x =
    print(x)
f()
print(x)



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

牧码人 发表于 2019-4-11 22:40:53

本帖最后由 牧码人 于 2019-4-11 22:42 编辑

冬雪雪冬 发表于 2019-4-11 22:03
小甲鱼说上述例子中容器变量x不是存放在栈中,所以可以避免被屏蔽,但是并没有具体说容器变量存放在哪里,p ...

谢谢大牛细心解答{:5_109:}!
x =
def f():
    x.append(3) #这个操作并没有改变列表x的内存地址,但是重新赋值的话x=就会改变了,两者的操作对象不一样,一个是对列表的元素操作,一个是对列表操作,也就是说python只有在对对象的最外层引用进行赋值操作(有等号出现)时才会触发屏蔽机制,不可变数据最外层对象就是其自身,没有内层对象,可变数据类型最外层也是其自身,但是对内层对象的操作不会触发屏蔽机制,会直接改变其数据。这回我算是加深理解了。
    print(x)
f()
print(x)

另外,容器变量不是存放在栈中,那它存放在哪呢?python的函数是存放在栈的,栈是实体还是虚拟概念?



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

{:5_95:}

冬雪雪冬 发表于 2019-4-11 22:48:54

另外,容器变量不是存放在栈中,那它存放在哪呢?python的函数是存放在栈的,栈是实体还是虚拟概念?
这个我不太明白,还是得请教小甲鱼。
页: [1]
查看完整版本: python闭包