Python 可执行文件如何解析和执行脚本?

3 投票
2 回答
1620 浏览
提问于 2025-04-18 10:43

假设我有一个脚本,叫做 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 不是逐行读取程序的——否则它就不会在实际执行程序之前就发现语法错误。这让解释器的工作原理显得有些模糊。我想知道有没有人能帮我理清这些问题。特别是,我想了解:

  1. Python 在什么时刻意识到第 13 行有语法错误?

  2. Python 在什么时刻读取嵌套的函数,并把它们加入到 foo 的作用域中?

  3. 同样,Python 是如何在遇到函数 foo 时将它添加到命名空间中,而不去执行它的?

  4. 假设 my_library 是一个无效的导入。Python 是否一定会在执行其他命令之前就抛出 ImportError

  5. 假设 my_library 是一个有效的模块,但里面没有函数 do_things。Python 会在执行 foo() 时才意识到这一点,还是在此之前就知道了?

如果有人能给我指个方向,告诉我关于 Python 如何解析和执行脚本的文档,我将非常感激。

2 个回答

1

一般来说,Python会先读取文件内容,然后把这些内容转换成一种叫做字节码的东西,最后按顺序执行这些代码。这意味着所有的语句都是一行一行执行的。所以,这里有几个要点:

  1. 语法错误会在解析阶段被发现,也就是在任何代码执行之前。如果你在脚本中添加了一些会产生副作用的操作,比如创建一个文件,你会发现这些操作根本不会被执行。
  2. 一个函数在定义之后才会被识别。如果你在def nested_foo()之前就尝试调用nested_foo,你会看到失败,因为在那个时候nested_foo还没有被定义。
  3. 和第二点一样。
  4. 如果Python无法导入一个库,导入的意思是它尝试执行这个模块,那么就会出现ImportError错误。
  5. 因为你在导入时没有尝试访问do_things(也就是说,你没有写from my_library import do_things),所以只有在你尝试调用foo()的时候才会出现错误。
5

在教程的模块部分有一些信息,但我觉得文档没有提供完整的参考资料。所以,下面是发生的事情。

当你第一次运行一个脚本或者导入一个模块时,Python会把代码的语法解析成一个抽象语法树(AST),然后将其编译成字节码。这个时候还没有执行任何代码;它只是把你的代码编译成一个小机器可以理解的指令。这就是语法错误被捕捉的地方。(你可以在ast模块、token模块、compile内置函数、语法参考以及其他一些地方看到这些内容的详细信息。)

实际上,你可以独立于运行生成的代码来编译一个模块;这就是内置的compileall方法的作用。

所以这是第一阶段:编译。Python只有一个其他阶段,那就是实际运行代码。在你的模块中,每个语句,除了在deflambda里面的语句,都是按顺序执行的。这意味着import语句在运行时执行,无论你把它放在模块的哪个位置。这也是为什么把所有import放在顶部是个好习惯。同样的道理适用于defclass:这些只是创建特定类型对象的语句,它们在遇到时就会被执行,就像其他任何语句一样。

这里唯一复杂的地方是,这些阶段可能会发生多次——例如,import语句只在运行时执行,但如果你之前从未导入过那个模块,那么它就必须被编译,这样你又回到了编译阶段。但是在import之外的地方仍然是运行时,这就是为什么你可以捕捉到import引发的SyntaxError

无论如何,来回答你的具体问题:

  1. 在编译时。当你把它作为脚本运行,或者当你把它作为模块导入,或者当你用compileall编译它,或者以其他方式让Python理解它时。在实际操作中,这可以在任何时候发生:如果你试图在一个函数内导入这个模块,你只有在调用那个函数时才会得到SyntaxError,这可能是在你的程序进行到一半的时候。

  2. 在执行foo的时候,因为defclass只是创建一个新对象并将其赋值给一个名字。但Python仍然知道如何创建嵌套函数,因为它已经编译了其中的所有代码。

  3. 就像把foo = lambda: 1 + 2添加到命名空间一样,而没有执行它。一个函数只是一个包含“代码”属性的对象——字面上就是一块Python字节码。你可以把code类型当作数据来操作,因为它就是数据,和执行它无关。试着查看一个函数的.__code__,阅读数据模型中的“代码对象”部分,或者甚至玩玩反汇编器。(你甚至可以使用exec直接执行一个代码对象,使用自定义的局部和全局变量,或者更改一个函数使用的代码对象!)

  4. 是的,因为import是一个普通的语句,像其他任何语句一样,按顺序执行。但如果在import之前有其他代码,那些代码会先执行。如果它在一个函数中,你在那个函数运行之前不会得到错误。请注意,import就像defclass一样,只是一种特殊的赋值形式。

  5. 只有在执行foo()的时候。Python无法知道在那之前是否会有其他代码向你的模块添加do_things,甚至将my_library更改为其他对象。属性查找总是在你请求时即时进行,而不是提前进行。

撰写回答