引言
当你在程序中使用一个变量名时,Python在一个称为命名空间(namespace)地方创建、改变、查找。命名空间是变量名存在的地方。Python会根据变量名第一次赋值的位置决定将变量名放到不同的命名空间。换句话说,在源代码中给变量名赋值的位置决定了这个名字会存在于哪个命名空间和这个名字的作用域。例如,在函数内部赋值的变量名会被放到函数的命名空间,也就是说这个变量只在函数内有效。
进阶
命名空间是可以嵌套的,函数定义了一个局部(local
)作用域而模块定义了一个全局(global
)作用域,并有以下特性:
- 封闭模块是全局作用域
每个模块都是全局作用域——变量在顶层模块文件中创建并存在以一个命名空间中。当模块被引入之后,全局变量变成了模块对象的属性,但在模块内部仍旧只是一个变量。 - 全局作用域的范围只是每个单独文件
Python中全局作用域是只和每个模块相联系的,而模块为单独文件,如果想使用某个文件内的变量,必须先引入模块 - 变量名默认是局部作用域除非声明
global
或nonlocal
所有在函数内部赋值的变量默认为局部变量,如果需要在函数内部创建全局变量,需要特殊声明。 - 每次函数调用都会创建一个新的局部作用域
每次调用函数就会创建一个新的局部作用域——一个函数内部定义的变量存在的命名空间。可以认为每个def
或lambda
定义一个新的局部作用域,但是局部作用域是和函数调用相对应的。因为函数允许循环调用自己——递归函数。
有一点要铭记,在交互式命令行中输入的代码也是存在于一个模块中的。
也要注意,在函数内部所有类型的赋值都会定义一个一个变量为局部变量。这包括=
、import
中的模块名、def
中的函数名、函数参数名等等,如果你在def
内赋值了一个变量,他都会默认为是局部变量。相反的,就地改变一个对象并不会定义一个名字为局域变量,只有实际上的赋值语句是。
LEGB规则
Python中处理变量名的解决方案称之为LEGB规则,不过这个规则只适用于变量名。
- 当你在函数内部使用一个无限制的变量时,Python会在4个作用域中搜索——局部作用域(Local),然后是局部作用域的任何封闭(Enclosing)
def
或lambda
,然后是全局作用域(Global),然后是内置(Built-in)作用域。 - 当在函数内部赋值一个名字的时候,Python总是只在局部作用域中创建或者改变,除非在函数内声明
global
或者nonlocal
- 当在任何函数外赋值一个变量的时候,局部作用域就是全局作用域——模块的命名空间
图示:
其他Python作用域
确切的来说,Python中还有其他三种作用域——一些推导式(comprehension)中的循环变量、一些try
中的异常引用变量(exception reference)、class
语句中的局部作用域。前两个属于特殊情况,而第三个遵循LEGB规则。
推导式变量(comprehension)
推导式中的变量X用来指向当前的迭代元素,如[ x for x in I]。因为他们可能会与其它变量名冲突而影响生成器的内在状态,在3.X中,这样的变量是表达式的局部变量,在所有的推导式格式中都是这样:生成器、列表、集合和字典。在2.X中它们对于生成器表达式、集合和字典来说是局部的,但不适用于列表生成式。作为对比,for循环从来不局部化它们的变量。
异常变量
在try模块中变量X用来指向抛出的错误例如 except E as X。因为在3.X它们可能推迟垃圾回收机制的内存回收,这样的变量属于except块的局域变量,当块退出后它们就被删除。在2.x中在try之后一直存在
内置(built-in)作用域
内置作用域就是一个内置模块称作builtins
,但是当查询这个模块的时候必须引入,因为builtins
这个名字并没有内置到这个模块中,builtins
只是一个标准库文件,可以用dir
函数查看:
>>> import builtins
>>> dir(builtins)
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException',
'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning',
...many more names omitted...
'ord', 'pow', 'print', 'property', 'quit', 'range', 'repr', 'reversed',
'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum',
'super', 'tuple', 'type', 'vars', 'zip']
很明显能够看出,前面的是内置异常名,后面的是内置函数名。因为Python会根据LEGB规则自动搜索这个模块,所以你可以自由的使用这些函数而不需要引入任何模块。例如有两种方式调用内部函数,效果是一样的:
>>> zip
<class 'zip'>
>>> import builtins
>>> builtins.zip
<class 'zip'>
>>> zip is builtins.zip
True
并且铭记,尽管Python中有保留字,但是当你重定义一个内置函数名的时候Python是不警告你的,也不会抛出错误。所以尽量不要重定义名内置函数名,或者至少不要重定义你需要用到的函数。
global语句
Python中的global
语句和nonlocal
语句更像是声明语句,但他们不是类型或大小的声明,而是命名空间的声明:
>>> x = 1
>>> y = 2
>>> def func():
... global x
... x = 0
... y = 0
...
>>> print(x, y)
1 2
程序设计:最小化全局变量
函数应该依赖于参数和返回值,而不是全局变量。默认情况下函数内的赋值是局部变量,如果你想改变函数外的变量你必须写额外的代码(如global
),但你必须十分谨慎,避免日后潜在的麻烦和危险,程序会更难debug
和理解,所以应该尽量避免使用global。
程序设计:最小化跨文件改变
这是另一个作用域相关的设计问题——尽管我们可以在另一个文件中直接改变变量,但是应该尽量避免这么做:
# first.py
X = 99 # This code doesn't know about second.py
# second.py
import first
print(first.X)
first.X = 88
第一个文件定义了一个变量X,第二个文件打印了X,并在之后通过赋值语句改变了X。注意到我们必须在第二个人间中引入第一个文件才能访问他的变量——正如我们之前了解到的,每一个模块都有自己的命名空间,我们必须引入才能在另一个文件中访问他的变量。这也是重点——通过分开变量在不同的人间中来避免命名冲突,同样的方法也避免了不同函数内的变量冲突。
事实上,一旦被引入之后,模块的全局作用域就变成了模块对象的属性命名空间——引入者能够自动访问文件的所有全局变量,因为在引入之后文件的全局作用域转变成了对象的属性空间。
引入第一个模块之后,第二个文件打印变量并赋了一个新值,引用并打印变量这没什么问题——这正是在更大的系统中模块是如何联系到一起的。问题的关键在于重新赋值,这有隐含性的危害——维护或重用第一个文件的人可能并不知道某个文件内在运行时会改变X的值。或许第二个文件在完全不同的目录,这很难注意到。
尽管这种跨文件的变量改变是可能的,但这要比你想象的更微妙,这在两个文件间建立了太强的联系——两个文件都依赖于变量X,这使其很困难去理解或重用其中单独一个文件。这样的跨文件依赖顽固的代码和严重的bug。
跨文件通信的最好方法是调用函数,传参和返回值,在下列这个例子中我们定义了一个函数去管理这种改变:
# first.py
X = 99
def setX(new):
global X
X = new
# second.py
import first
first.setX(88)
这需要更多的代码和和一些看似细小的改变。但在可读性和可维护性上产生了巨大的不同——当一个人读第一个模块的时候会看到这个函数,就会知道这是一个改变X值的接口。尽管我们并不能避免跨文件改变的生,但我们也要最小化的使用。
其他访问全局变量的方法
# thismod.py
var = 99
def local():
var = 0
def glob1():
global var
var += 1
def glob2():
var = 0
import thismod
thismod.var += 1
def glob3():
var = 0
import sys
glob = sys.modules['thismod']
glob.var += 1
def test():
print(var)
local(); glob1(); glob2(); glob3()
print(var)