在函数内创建类并访问包含函数作用域内的函数
编辑:
请查看我在这个问题底部的完整回答。
简而言之: Python 有静态嵌套作用域。这个静态特性与隐式变量声明相互作用,可能会产生一些不明显的结果。
(这可能会让人感到特别惊讶,因为这个语言通常是动态的)。
我原本觉得自己对 Python 的作用域规则掌握得不错,但这个问题让我完全困惑了,我的搜索技能也没能帮上忙(这也不奇怪,看看问题标题就知道了;)
我将从几个按预期工作的例子开始,但如果你想直接看有趣的部分,可以跳到例子 4。
例子 1.
>>> x = 3
>>> class MyClass(object):
... x = x
...
>>> MyClass.x
3
这很简单:在类定义期间,我们可以访问外部(在这个例子中是全局)作用域中定义的变量。
例子 2.
>>> def mymethod(self):
... return self.x
...
>>> x = 3
>>> class MyClass(object):
... x = x
... mymethod = mymethod
...
>>> MyClass().mymethod()
3
同样(暂时不考虑为什么要这样做),这里没有什么意外的:我们可以访问外部作用域中的函数。
注意: 正如 Frédéric 在下面指出的,这个函数似乎不起作用。请查看例子 5(及之后的例子)。
例子 3.
>>> def myfunc():
... x = 3
... class MyClass(object):
... x = x
... return MyClass
...
>>> myfunc().x
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in myfunc
File "<stdin>", line 4, in MyClass
NameError: name 'x' is not defined
这本质上与例子 1 相同:我们从类定义中访问外部作用域,只不过这次那个作用域不是全局的,多亏了 myfunc()
。
编辑 5: 正如 @user3022222 在下面指出的,我在原始发布中搞错了这个例子。我认为这个例子失败是因为只有函数(而不是其他代码块,比如这个类定义)可以访问封闭作用域中的变量。对于非函数代码块,只有局部、全局和内置变量是可以访问的。更详细的解释可以在 这个问题中找到。
再来一个:
例子 4.
>>> def my_defining_func():
... def mymethod(self):
... return self.y
... class MyClass(object):
... mymethod = mymethod
... y = 3
... return MyClass
...
>>> my_defining_func()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in my_defining_func
File "<stdin>", line 5, in MyClass
NameError: name 'mymethod' is not defined
呃……请问?
这和例子 2 有什么不同吗?
我完全搞糊涂了。请帮我理清楚。谢谢!
附注:如果这不仅仅是我理解上的问题,我在 Python 2.5.2 和 Python 2.6.2 上试过这个。不幸的是,我目前只能使用这两个版本,但它们都表现出相同的行为。
编辑 根据 http://docs.python.org/tutorial/classes.html#python-scopes-and-namespaces:在执行期间,至少有三个嵌套作用域的命名空间是可以直接访问的:
- 最内层作用域,首先被搜索,包含局部名称
- 任何封闭函数的作用域,从最近的封闭作用域开始搜索,包含非局部但也非全局的名称
- 倒数第二个作用域包含当前模块的全局名称
- 最外层作用域(最后搜索)是包含内置名称的命名空间
#4. 似乎是对第二个的反例。
编辑 2
例子 5.
>>> def fun1():
... x = 3
... def fun2():
... print x
... return fun2
...
>>> fun1()()
3
编辑 3
正如 @Frédéric 指出的,给一个与外部作用域同名的变量赋值似乎会“遮蔽”外部变量,导致赋值无法正常工作。
所以这个修改后的例子 4 是有效的:
def my_defining_func():
def mymethod_outer(self):
return self.y
class MyClass(object):
mymethod = mymethod_outer
y = 3
return MyClass
my_defining_func()
然而这个不行:
def my_defining_func():
def mymethod(self):
return self.y
class MyClass(object):
mymethod_temp = mymethod
mymethod = mymethod_temp
y = 3
return MyClass
my_defining_func()
我仍然不完全理解为什么会发生这种遮蔽:难道不应该在赋值发生时进行名称绑定吗?
这个例子至少提供了一些线索(还有一个更有用的错误信息):
>>> def my_defining_func():
... x = 3
... def my_inner_func():
... x = x
... return x
... return my_inner_func
...
>>> my_defining_func()()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in my_inner_func
UnboundLocalError: local variable 'x' referenced before assignment
>>> my_defining_func()
<function my_inner_func at 0xb755e6f4>
所以看来局部变量是在函数创建时定义的(这成功了),导致局部名称被“保留”,因此在函数调用时遮蔽了外部作用域的名称。
有趣。
感谢 Frédéric 的回答!
作为参考,来自 Python 文档:
重要的是要意识到作用域是根据文本确定的:在模块中定义的函数的全局作用域是该模块的命名空间,无论该函数从哪里或通过什么别名被调用。另一方面,实际的名称搜索是在运行时动态进行的——然而,语言定义正在朝着静态名称解析的方向发展,在“编译”时进行,因此不要依赖动态名称解析! (实际上,局部变量已经是静态确定的。)
编辑 4
真正的答案
这种看似混乱的行为是由 Python 的 PEP 227 中定义的静态嵌套作用域引起的。实际上与 PEP 3104 无关。
来自 PEP 227:
名称解析规则对于静态作用域语言是典型的 [...] [除了] 变量没有声明。如果在函数的任何地方发生名称绑定操作,则该名称被视为局部于该函数,所有引用都指向局部绑定。如果在名称绑定之前发生引用,将引发 NameError。
[...]
Tim Peters 的一个例子展示了在没有声明的情况下嵌套作用域的潜在陷阱:
i = 6 def f(x): def g(): print i # ... # skip to the next page # ... for i in x: # ah, i *is* local to f, so this is what g sees pass g()
对 g() 的调用将引用在 f() 中通过 for 循环绑定的变量 i。如果在循环执行之前调用 g(),将引发 NameError。
让我们运行 Tim 的例子的两个更简单的版本:
>>> i = 6
>>> def f(x):
... def g():
... print i
... # ...
... # later
... # ...
... i = x
... g()
...
>>> f(3)
3
当 g()
在其内部作用域中找不到 i
时,它会动态向外搜索,找到在 f
的作用域中绑定的 i
,这个 i
通过 i = x
赋值为 3
。
但改变 f
中最后两个语句的顺序会导致错误:
>>> i = 6
>>> def f(x):
... def g():
... print i
... # ...
... # later
... # ...
... g()
... i = x # Note: I've swapped places
...
>>> f(3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 7, in f
File "<stdin>", line 3, in g
NameError: free variable 'i' referenced before assignment in enclosing scope
记住 PEP 227 说过“名称解析规则对于静态作用域语言是典型的”,让我们看看相应的 C 版本:
// nested.c
#include <stdio.h>
int i = 6;
void f(int x){
int i; // <--- implicit in the python code above
void g(){
printf("%d\n",i);
}
g();
i = x;
g();
}
int main(void){
f(3);
}
编译并运行:
$ gcc nested.c -o nested
$ ./nested
134520820
3
所以虽然 C 会乐于使用一个未绑定的变量(使用之前存储的任何内容:在这个例子中是 134520820),但 Python(值得庆幸的是)拒绝这样做。
有趣的是,静态嵌套作用域使得 Alex Martelli 所称的 “Python 编译器做的最重要的优化:函数的局部变量不保存在字典中,而是在一个紧凑的值向量中,每次访问局部变量时使用该向量中的索引,而不是名称查找。”
2 个回答
这篇文章虽然有几年了,但它讨论了Python中作用域和静态绑定这个重要问题,算是比较少见的。不过,作者在例子3中有一个重要的误解,可能会让读者感到困惑。(不要认为其他例子都是正确的,我只是详细看了例子3中提到的问题。)
让我来解释一下发生了什么。
在例子3中
def myfunc():
x = 3
class MyClass(object):
x = x
return MyClass
>>> myfunc().x
应该会返回一个错误,这和作者所说的不一样。我认为他之所以没发现这个错误,是因为在例子1中,x
是在全局作用域中被赋值为3
的。所以对发生的事情理解错了。
这个解释在这篇文章中有详细描述:Python中变量引用是如何解析的
这是因为Python在查找变量名字时的一些规则:你只能访问全局和局部的范围,但不能访问中间的范围,比如说不能访问你直接外面的范围。
编辑:上面的说法不太准确,其实你是可以访问外部范围定义的变量,但如果你在非全局的地方用 x = x
或 mymethod = mymethod
,你实际上是用你自己定义的变量覆盖了外部的那个变量。
在例子2中,你直接外面的范围是全局范围,所以 MyClass
可以看到 mymethod
。但是在例子4中,你直接外面的范围是 my_defining_func()
,所以它看不到,因为外部定义的 mymethod
已经被你在内部定义的那个覆盖了。
想了解更多关于非局部名字解析的内容,可以查看 PEP 3104。
另外,基于上面的原因,我在Python 2.6.5和3.1.2下都无法运行例子3:
>>> def myfunc():
... x = 3
... class MyClass(object):
... x = x
... return MyClass
...
>>> myfunc().x
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in myfunc
File "<stdin>", line 4, in MyClass
NameError: name 'x' is not defined
但下面的代码是可以运行的:
>>> def myfunc():
... x = 3
... class MyClass(object):
... y = x
... return MyClass
...
>>> myfunc().y
3