在函数内创建类并访问包含函数作用域内的函数

94 投票
2 回答
105051 浏览
提问于 2025-04-16 07:43

编辑:

请查看我在这个问题底部的完整回答。

简而言之: 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 个回答

11

这篇文章虽然有几年了,但它讨论了Python中作用域和静态绑定这个重要问题,算是比较少见的。不过,作者在例子3中有一个重要的误解,可能会让读者感到困惑。(不要认为其他例子都是正确的,我只是详细看了例子3中提到的问题。)

让我来解释一下发生了什么。

在例子3中

def myfunc():
    x = 3
    class MyClass(object):
        x = x
    return MyClass

>>> myfunc().x

应该会返回一个错误,这和作者所说的不一样。我认为他之所以没发现这个错误,是因为在例子1中,x是在全局作用域中被赋值为3的。所以对发生的事情理解错了。

这个解释在这篇文章中有详细描述:Python中变量引用是如何解析的

26

这是因为Python在查找变量名字时的一些规则:你只能访问全局和局部的范围,但不能访问中间的范围,比如说不能访问你直接外面的范围。

编辑:上面的说法不太准确,其实你可以访问外部范围定义的变量,但如果你在非全局的地方用 x = xmymethod = 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

撰写回答