Python模块初始化顺序?
我是一名刚接触Python的新手,之前学的是C++。虽然我知道用我以前的C++知识去找Python中的对应概念并不是很“Pythonic”,但我觉得这个问题还是值得问的:
在C++中,有一个大家都知道的问题叫做全局/静态变量初始化顺序混乱。这是因为C++无法决定在不同的编译单元中,哪个全局或静态变量会先被初始化。因此,如果一个全局或静态变量依赖于另一个在不同编译单元中的变量,可能会出现先初始化依赖的变量,而后初始化被依赖的变量的情况。这样,当依赖的变量开始使用另一个变量提供的服务时,就会出现未定义的行为。我在这里不想深入讨论C++是如何解决这个问题的。:)
在Python中,我确实看到有使用全局变量的情况,甚至跨不同的.py文件。我看到的一个典型用法是:在一个.py文件中初始化一个全局对象,而在其他.py文件中,代码就毫不犹豫地开始使用这个全局对象,假设它一定在别的地方被初始化了。对于我来说,这在C++中是绝对不可接受的,因为我刚才提到的问题。
我不确定这种用法在Python中是否是常见的做法(是否“Pythonic”),Python一般是如何解决这种全局变量初始化顺序的问题的?
2 个回答
在Python中,当你导入一个模块时,它会从头到尾执行这个模块的代码。之后再导入同一个模块时,其实只是拿到了之前已经存在的引用,即使这个模块还在导入过程中(比如因为循环导入的情况)。在循环导入之前已经初始化的模块属性(其实就是模块里的“全局变量”)仍然会存在。
main.py
:
import a
a.py
:
var1 = 'foo'
import b
var2 = 'bar'
b.py
:
import a
print a.var1 # works
print a.var2 # fails
在C++中,有一个著名的问题叫做全局/静态变量初始化顺序混乱。这是因为C++无法决定在不同的编译单元中,哪个全局或静态变量会先被初始化。
我觉得这个说法突出了Python和C++之间的一个关键区别:在Python中,没有不同的编译单元。我的意思是,在C++中(你应该知道),两个不同的源文件可能会完全独立地编译,因此如果你比较文件A中的一行和文件B中的一行,就没有任何东西能告诉你哪个会先放到程序中。这有点像多个线程的情况:你无法确定线程1中的某个语句会在线程2中的某个语句之前还是之后执行。可以说,C++程序是并行编译的。
相反,在Python中,执行是从一个文件的顶部开始的,并且按照文件中每个语句的明确顺序进行,在导入其他文件的地方分支出去。实际上,你几乎可以把import
指令看作是#include
,这样你就可以识别程序中所有源文件中所有代码行的执行顺序。(嗯,这比这要复杂一点,因为模块实际上只有在第一次被导入时才会真正执行,还有其他原因。)如果说C++程序是并行编译的,那么Python程序则是串行解释的。
你的问题还涉及到Python中模块的更深层含义。Python模块——也就是单个.py
文件中的所有内容——实际上是一个对象。在单个源文件中声明的所有“全局”范围的内容,实际上都是该模块对象的属性。在Python中没有真正的全局范围。(Python程序员常常说“全局”,实际上语言中确实有一个global
关键字,但它总是指当前模块的顶层。)我能理解从C++背景过来的人可能会觉得这个概念有点奇怪。对我来说,从Java过来也需要一些适应,而在这方面,Java和Python比C++更相似。(Java中也没有全局范围)
我想提到的是,在Python中,使用一个变量而不知道它是否已经被初始化/定义是完全正常的。嗯,可能不算正常,但在适当的情况下至少是可以接受的。在Python中,尝试使用一个未定义的变量会引发NameError
;你不会像在C或C++中那样得到任意的行为,因此你可以轻松处理这种情况。你可能会看到这样的模式:
try:
duck.quack()
except NameError:
pass
如果duck
不存在,它什么都不做。实际上,你更常见到的是
try:
duck.quack()
except AttributeError:
pass
如果duck
没有名为quack
的方法,它什么都不做。(AttributeError
是当你尝试访问一个对象的属性,但该对象没有这个属性时会出现的错误。)这就是Python中的类型检查:我们认为如果我们只需要鸭子叫,我们可以直接让它叫,如果它叫了,我们就不在乎它是否真的是真正的鸭子。(这被称为鸭子类型;-)