Python 可执行文件如何解析和执行脚本?
假设我有一个脚本,叫做 test.py
:
import my_library
bar = 12
def foo():
nested_bar = 21
my_library.do_things()
def nested_foo():
nested_bar += 11
not_a_variable += 1
{$ invalid_syntax
bar = 13
foo()
bar = 14
我很好奇,当我运行 python test.py
时,究竟发生了什么。显然,Python 不是逐行读取程序的——否则它就不会在实际执行程序之前就发现语法错误。这让解释器的工作原理显得有些模糊。我想知道有没有人能帮我理清这些问题。特别是,我想了解:
Python 在什么时刻意识到第 13 行有语法错误?
Python 在什么时刻读取嵌套的函数,并把它们加入到
foo
的作用域中?同样,Python 是如何在遇到函数
foo
时将它添加到命名空间中,而不去执行它的?假设
my_library
是一个无效的导入。Python 是否一定会在执行其他命令之前就抛出ImportError
?假设
my_library
是一个有效的模块,但里面没有函数do_things
。Python 会在执行foo()
时才意识到这一点,还是在此之前就知道了?
如果有人能给我指个方向,告诉我关于 Python 如何解析和执行脚本的文档,我将非常感激。
2 个回答
一般来说,Python会先读取文件内容,然后把这些内容转换成一种叫做字节码的东西,最后按顺序执行这些代码。这意味着所有的语句都是一行一行执行的。所以,这里有几个要点:
- 语法错误会在解析阶段被发现,也就是在任何代码执行之前。如果你在脚本中添加了一些会产生副作用的操作,比如创建一个文件,你会发现这些操作根本不会被执行。
- 一个函数在定义之后才会被识别。如果你在
def nested_foo()
之前就尝试调用nested_foo
,你会看到失败,因为在那个时候nested_foo
还没有被定义。 - 和第二点一样。
- 如果Python无法导入一个库,导入的意思是它尝试执行这个模块,那么就会出现
ImportError
错误。 - 因为你在导入时没有尝试访问
do_things
(也就是说,你没有写from my_library import do_things
),所以只有在你尝试调用foo()
的时候才会出现错误。
在教程的模块部分有一些信息,但我觉得文档没有提供完整的参考资料。所以,下面是发生的事情。
当你第一次运行一个脚本或者导入一个模块时,Python会把代码的语法解析成一个抽象语法树(AST),然后将其编译成字节码。这个时候还没有执行任何代码;它只是把你的代码编译成一个小机器可以理解的指令。这就是语法错误被捕捉的地方。(你可以在ast
模块、token
模块、compile
内置函数、语法参考以及其他一些地方看到这些内容的详细信息。)
实际上,你可以独立于运行生成的代码来编译一个模块;这就是内置的compileall
方法的作用。
所以这是第一阶段:编译。Python只有一个其他阶段,那就是实际运行代码。在你的模块中,每个语句,除了在def
或lambda
里面的语句,都是按顺序执行的。这意味着import
语句在运行时执行,无论你把它放在模块的哪个位置。这也是为什么把所有import
放在顶部是个好习惯。同样的道理适用于def
和class
:这些只是创建特定类型对象的语句,它们在遇到时就会被执行,就像其他任何语句一样。
这里唯一复杂的地方是,这些阶段可能会发生多次——例如,import
语句只在运行时执行,但如果你之前从未导入过那个模块,那么它就必须被编译,这样你又回到了编译阶段。但是在import
之外的地方仍然是运行时,这就是为什么你可以捕捉到import
引发的SyntaxError
。
无论如何,来回答你的具体问题:
在编译时。当你把它作为脚本运行,或者当你把它作为模块导入,或者当你用
compileall
编译它,或者以其他方式让Python理解它时。在实际操作中,这可以在任何时候发生:如果你试图在一个函数内导入这个模块,你只有在调用那个函数时才会得到SyntaxError
,这可能是在你的程序进行到一半的时候。在执行
foo
的时候,因为def
和class
只是创建一个新对象并将其赋值给一个名字。但Python仍然知道如何创建嵌套函数,因为它已经编译了其中的所有代码。就像把
foo = lambda: 1 + 2
添加到命名空间一样,而没有执行它。一个函数只是一个包含“代码”属性的对象——字面上就是一块Python字节码。你可以把code
类型当作数据来操作,因为它就是数据,和执行它无关。试着查看一个函数的.__code__
,阅读数据模型中的“代码对象”部分,或者甚至玩玩反汇编器。(你甚至可以使用exec
直接执行一个代码对象,使用自定义的局部和全局变量,或者更改一个函数使用的代码对象!)是的,因为
import
是一个普通的语句,像其他任何语句一样,按顺序执行。但如果在import
之前有其他代码,那些代码会先执行。如果它在一个函数中,你在那个函数运行之前不会得到错误。请注意,import
就像def
和class
一样,只是一种特殊的赋值形式。只有在执行
foo()
的时候。Python无法知道在那之前是否会有其他代码向你的模块添加do_things
,甚至将my_library
更改为其他对象。属性查找总是在你请求时即时进行,而不是提前进行。