Python:为什么只在赋值时需要使用全局变量声明,而在读取时不需要?

53
如果一个函数需要修改在全局作用域中声明的变量,它需要使用全局声明。但是,如果函数只需要读取全局变量,则可以在不使用全局声明的情况下这样做:
X = 10
def foo():
    global X
    X = 20 # Needs global declaration
def bar():
    print( X ) # Does not need global
我的问题是关于 Python 的设计:为什么 Python 允许读取全局变量而不使用 global 声明?也就是说,为什么只强制在赋值时使用 global,而不强制在读取时也使用 global 呢?(这样会使它更加一致和优雅。)
注意:我可以看到在读取时没有歧义,但在赋值时并不清楚是否打算创建一个新的局部变量或者是分配给全局变量。但是,我希望 BDFL 有更好的理由或意图来解释这个不平衡的设计选择。
4个回答

35

有了嵌套作用域,变量查找变得容易。它们按照本地变量、封闭的函数定义、模块全局变量和内置范围的顺序进行链式查找。规则是第一个匹配胜出。因此,在查找时不需要“global”声明。

相比之下,在写入时,您需要指定要写入哪个作用域。否则,无法确定函数中的“x = 10”是意味着“写入本地命名空间”还是“写入全局命名空间”。

简而言之,在写入时,您可以选择命名空间,但在查找时,第一个匹配规则就足够了。希望这有所帮助:-)

编辑:是的,“因为BDFL这样说”,但在其他没有类型声明的语言中,只需针对非本地写入才需要修饰符,而查找则采用第一个匹配规则。仔细想一想,这两个规则会导致非常干净的代码,因为作用域修饰符仅在最不常见的情况下需要使用(非本地写入)。


24

看看这段代码:

from module import function

def foo(x):
    return function(x)

这里的 function 是全局变量。如果我不得不每次都说 global function,那么代码编写起来将会非常繁琐。

在你认为你的 X 和我的 function 不同(因为一个是变量而另一个是导入的函数)之前,请记住Python中所有名称都被视为相同的:在使用时,它们的值是在作用域层次结构中查找的。如果你需要 global X,那么你就需要 global function。糟糕。


7
不过,那并没有回答问题。你可以将这个论点扩展到修改全局变量上。为什么要使用显式的全局变量来修改全局变量呢? - jterrace
5
如果你不包含global X,那么语句X = 10将创建一个绑定到10的本地变量X,而不是重新将全局变量X绑定到10 - Dan D.
5
问题是“为什么只有在赋值时需要使用global”,而不是“为什么需要在赋值时使用global”。 - Ned Batchelder
7
实际上,这是问题的标题。真正的问题是:为什么Python设计允许读取全局变量而不需要使用global声明,但不允许修改呢? - Pedro Werneck
1
Predro: Ned正在回答我的实际问题。如果我用英语让读者感到困惑,我很抱歉。我现在已经纠正了问题的文本。 - Ashwin Nanjappa

24

因为明确胜于含糊。

当你阅读一个变量时,没有任何歧义。在从局部到全局的搜索范围内,总是得到找到的第一个变量。

当你赋值时,解释器可以肯定假设你所赋值的只有两个作用域:局部和全局。由于赋值给局部变量是最常见的情况,而赋值给全局变量实际上是被不鼓励的,默认情况下是赋值给局部变量。要赋值给全局变量,你必须显式地告诉解释器,你在这个作用域中使用该变量时,它应该直接进入全局作用域,并且你知道自己在做什么。在Python 3中,你也可以使用“nonlocal”将变量赋值给最近的封闭作用域。

请记住,在Python中给一个名称赋值时,这个新赋值与之前存在的任何内容都没有关系。想象一下如果没有默认值局部变量并且Python在所有范围内搜索试图找到具有该名称的变量并将其分配为读取时所做的那样,你的函数行为可能会基于参数以及封闭范围而改变。生活将非常痛苦。


8
顺便提一下,并不与答案特别相关,我已经专业地使用Python编程8年,从未在任何情况下使用过全局变量。 - Pedro Werneck
1
Pedro:如果你正在编写一个需要修改全局状态的简单脚本,你会怎么做?如果类对于这个简单脚本来说太重了,该怎么办? - Ashwin Nanjappa

7
你自己说过,读取时没有歧义,但写入时有。因此,你需要一些机制来解决写入的歧义。
一种选择(可能实际上被更早版本的Python使用)是仅将写入始终放在本地作用域中。然后就不需要使用global关键字,也没有歧义。但是这样你就无法写入全局变量了(除非使用像globals()这样的方法以迂回方式访问它们),所以这并不好。
另一种选项,由静态声明变量的语言使用,是为每个作用域与语言实现提前通信,哪些名称是本地的(在该作用域中声明的名称)以及哪些名称是全局的(在模块作用域中声明的名称)。但是Python没有声明变量,所以这种解决方案行不通。
另一种选项是,如果某个外部作用域中已经有一个名为x的名称,则x = 3只会分配给本地变量。这似乎直观地做到了正确的事情?但这会导致一些严重的棘手问题。目前,x = 3将写入的位置是由解析器静态确定的;如果同一作用域中没有global x,则它是一个本地写入,否则它是全局写入。但是如果它将要执行的操作取决于全局模块作用域,那么您必须等到运行时才能确定写入的位置这意味着它会在函数调用之间改变。想象一下。每次在模块中创建一个全局变量,都会改变使用该名称作为本地变量名称的所有函数的行为。对于使用tmp作为临时变量的模块范围计算,并在该模块中分配属性然后调用该模块中的函数,我打个寒颤。恶心
另一种选项是,在每个赋值上向语言实现通信,以确定它应该是本地的还是全局的。这就是Python所采用的方法。鉴于几乎所有情况下都有一个合理的默认值(写入本地变量),我们将本地分配作为默认值,并使用global明确标记全局分配。
赋值存在歧义需要某种机制来解决。 global是其中一种机制。虽然并非唯一可能的机制,但在Python的上下文中,似乎所有可替代的机制都很糟糕。我不知道你在寻找什么样的“更好的理由”。

Ben:我是Python的新手。除了源代码中已经列出的全局变量之外,是否有可能在运行时“注入”一个新的全局变量?你的回答似乎表明这是可能的,我想知道如何实现。 - Ashwin Nanjappa
1
@Ashwin 全局作用域与任何其他作用域并没有什么不同。它没有单一的静态声明来说明它包含了什么;Python只是在模块中执行代码,因此名称被分配到全局作用域中。这与本地作用域定义名称的方式完全相同。除此之外,任何引用该模块的其他代码(在导入后)都可以在其上分配属性,而模块属性只是该模块内的全局变量。此外,模块中的函数可以使用 global。这是你所指的吗? - Ben

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接