Python 嵌套函数本地变量

大家都知道这个:

>>> def g():
>>>    s = 'z'
>>>    def f():
>>>        s = 'k'
>>>        print('f: %s' % s)
>>>    f()
>>>    return s
>>> print(g())

在 Python 2.x/3.x 都会得到:

f: k
z

但是最近同事使用 list 写了一个这样的:

>>> def g():
>>>     s = ['z']
>>>     def f():
>>>         s[0] = 'k'
>>>         print('f: %s' % s[0])
>>>     f()
>>>     return s[0]
>>> print(g())

在 Python 2.x/3.x 都会得到:

f: k
k

这是?


updated on 2013,05,02

其实这个问题主要包括两个方面的原因:

1) Python 调用模型中对参数的传递使用的是传值,不过所传的值不是该参数对象具体的值,而是该参数对象的引用。

http://docs.python.org/2/tutorial/controlflow.html#defining-functions

The actual parameters (arguments) to a function call are introduced in the local symbol table of the called function when it is called; thus, arguments are passed using call by value (where the value is always an object reference, not the value of the object). [1] When a function calls another function, a new local symbol table is created for that call.

...

[1] Actually, call by object reference would be a better description, since if a mutable object is passed, the caller will see any changes the callee makes to it (items inserted into a list).

另外,也有称这种参数传递方式为 call by object 的:

http://effbot.org/zone/call-by-object.htm

2) 在 Python 中变量名只是其所对应的对象的一种引用,而且在对象与变量名的对应关系中使用 copy on write 的方式来优化性能。

要知道一个对象与变量名的关系。在 Python 中变量名与对象是不同的东西。一般来说,通过赋值语句会生成一个对象,但这个对象一般不能直接使用,需要通过一个引用来使用它,即给它一个名字。也就是等号左边是一个变量名,右边是生成对象的代码,因此在执行这条赋值语句后就存在了两个东西,一个是名字,一个是对象。在 Python 中把名字与变量关联起来叫绑定。因此在后面你如果继续对同一个变量名赋值的话,其实是实现了对变量名与新对象的重新绑定,在 Python 中这是允许的,原来绑定的对象如果无人再使用就自动回收,否则就将引用计数减1。而这个对象是无法改变类型的。

copy on write 在重新绑定对象与变量名的时候起作用。

无论对象是不是被作为参数调用并应用到局部作用域,这个 copy on write 方式都发生作用:

>>> # 不可变对象的情况
>>> ss = 'abc'
>>> id(ss)  # -> 36956344
>>> ss = 'xyz'
>>> id(ss)  # -> 59511896
>>> kk = ss  # point to the same object
>>> id(kk)  # -> 59511896
>>> kk = 'rst'  # copy on write
>>> id(kk)  # -> 59547520
>>> id(ss)  # -> 59511896
>>> ss  # -> 'xyz'

>>> # 可变对象的情况
>>> aa = [11,  22]
>>> id(aa)  # -> 59628168
>>> aa.append(33)
>>> id(aa)  # -> 59628168
>>> aa[2] = 44
>>> id(aa)  # -> 59628168
>>> aa = ['x', 'y', 'z']
>>> id(aa)  # -> 60494664
>>> bb = aa  # point to the same object
>>> id(bb)  # -> 60494664
>>> bb[1] = 's'
>>> id(bb)  # -> 60494664
>>> id(aa)  # -> 60494664
>>> bb  # -> ['x', 's', 'z']
>>> aa  # -> ['x', 's', 'z']
>>> bb = [111, 222, 333, 444]  # copy on write
>>> id(bb)  # -> 60483784
>>> id(aa)  # -> 60494664

updated on 2015,12,18

然后,注意这里其实主要是闭包的问题。

在 Python 中闭包的一些文章:

Python闭包详解

http://www.cnblogs.com/ChrisChen3121/p/3208119.html

Python深入04 闭包

http://www.cnblogs.com/vamei/archive/2012/12/15/2772451.html

Python中的闭包

http://www.the5fire.com/closure-in-python.html

有趣的Python闭包(Closures)

http://feilong.me/2012/06/interesting-python-closures

在内部函数bar里面,本地的a = 1定义了在bar函数范围内的新的一个局部变量,因为名字和外部函数foo里面的变量a名字相同,导致外部函数foo里的a在内部函数bar里实际已不可见。

再来说a = a + 1出错是怎么回事,首先a = xxx这种形式,Python解析器认为要在内部函数bar内创建一个新的局部变量a,同时外部函数foo里的a在bar里已不可见,而解析器对接下来对右边的a + 1的解析就是用本地的变量a加1,而这时左边的a即本地的变量a还没有创建(等右边赋值呢),因此就这就产生了一个是鸡生蛋还是蛋生鸡的问题,导致了上面说的UnboundLocalError的错误。

在Python3.x中引入了一个nonloacal的关键字来解决这个问题,只要在a = a + 1前加一句nonloacal a即可,即显式的指定a不是内部函数bar内的本地变量,这样就可以在bar内正常的使用和再赋值外部函数foo内的变量a了。


updated on 2016,01,06

当在内部函数使用 globals()locals() 时候,可能外部函数里的变量尚未被放入闭包中:

via: https://www.python.org/dev/peps/pep-0498/#id25

In the discussions on python-dev [4] , a number of solutions where presented that used locals() and globals() or their equivalents. All of these have various problems. Among these are referencing variables that are not otherwise used in a closure. Consider:

>>> def outer(x):
...     def inner():
...         return 'x={x}'.format_map(locals())
...     return inner
...
>>> outer(42)()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in inner
KeyError: 'x'

This returns an error because the compiler has not added a reference to x inside the closure. You need to manually add a reference to x in order for this to work:

>>> def outer(x):
...     def inner():
...         x
...         return 'x={x}'.format_map(locals())
...     return inner
...
>>> outer(42)()
'x=42'

In addition, using locals() or globals() introduces an information leak. A called routine that has access to the callers locals() or globals() has access to far more information than needed to do the string interpolation.

Guido stated [5] that any solution to better string interpolation would not use locals() or globals().

[4]: Formatting using locals() and globals() ( https://mail.python.org/pipermail/python-ideas/2015-July/034671.html )

[5]: Avoid locals() and globals() ( https://mail.python.org/pipermail/python-ideas/2015-July/034701.html )

Comments

Comment is disabled by administrator.