Python 既是解释型语言,也是编译型语言( 三 )


旧的解析器无法提前查看多个 token,这意味着旧解析器在技术上可以接受语法无效的 Python 程序
尤其是这种限制导致解析器无法识别赋值语句的左边是否为有效的赋值目标 , 好比下面这段代码,旧解析器能够接收下面的代码

Python 既是解释型语言,也是编译型语言

文章插图
图片
上面这段代码没有任何意义,甚至 Python 语法是不允许这么使用的 。为了解决这个问题,Python 曾经存在过一个独立的,hacky 的阶段(这个 hacky 我不知道用什么翻译比较好)
即 Python会检查所有的赋值语句,并确保赋值号左边实际上是可以被赋值的东西
而这个阶段是发生在解析之后,这也就是为什么旧版本 Python 中会先把第二行的报错先显示出来
  • 回合四
现在还剩最后一个错误了
Python 既是解释型语言,也是编译型语言

文章插图
图片
我们来运行一下
Python 既是解释型语言,也是编译型语言

文章插图
图片
需要注意的是,Traceback (most recent call last)表示 Python 运行时报错的主要内容 , 这里在回合四才出现
经过前面的扫描、解析阶段,Python 终于能够运行代码了 。但是当 Python 开始运行解释第一行的时候,引发一个名为 ZeroDivisionError 的报错
为什么知道现在处于【运行时】,因为 Python 已经打印出 Traceback (most recent call last),这表示我们有一个堆栈跟踪
堆栈跟踪只能在运行时存在,这意味着这个报错必须在运行时捕获 。
但这意味着在回合1~3 中遇到的报错不是运行时报错,那它们是什么报错?
Python 既是解释型语言 , 也是编译型语言没错!CPython 解释器实际上是一个解释器,但它也是一个编译器
我希望上面的练习已经说明了 Python 在运行第一行代码之前必须经过几个阶段:
  • 扫描(scanning )
  • 解析(parsing )
旧版本的 Python 多了一个额外阶段:
  • 扫描(scanning )
  • 解析(parsing )
  • 检查有效的分配目标(checking for valid assignment targets)
让我们将其与前面编译 C 程序的阶段进行比较:
  • 预处理
  • 词汇分析(“扫描”的另一个术语)
  • 语法分析(“解析”的另一个术语)
  • 语义分析
  • 链接
Python 在运行任何代码之前仍然执行一些编译阶段,就像 Java一样,它会把源码编译成字节码
前面三个报错是 Python 在编译阶段产生的,只有最后一个才是在运行时产生,即ZeroDivisionError: division by zero.
实际上,我们可以使用命令行上的 compileall 模块预先编译所有 Python 代码:
Python 既是解释型语言,也是编译型语言

文章插图
图片
这会将当前目录中所有 Python 文件的编译字节码放入其中 __pycache__/,并显示任何编译器错误
如果你想知道那个 __pycache__/ 文件夹中到底有什么 , 我为 EdmontonPy 做了一个演讲,你应该看看!
演讲地址:https://www.YouTube.com/watch?v=5yqUTJuFuUk&t=7m11s
只有在 Python 被编译为字节码之后,解释器才会真正启动,我希望前面的练习已经证明 Python 确实可以在运行时之前报错
编译语言和解释语言是错误的二分法每当一种编程语言被归类为【编译】或【解释】语言时,我都会感到很讨厌 。一种语言本身不是编译或解释的
一种语言是编译还是解释(或两者兼而有之?。┦且桓鍪迪窒附?
我不是唯一一个有这种想法的人 。Laurie Tratt 有一篇精彩的文章,通过编写一个逐渐成为优化编译器的解释器来论证这一点
文章地址:https://tratt.NET/laurie/blog/2023/compiled_and_interpreted_languages_two_ways_of_saying_tomato.html
还有一篇文章就是 Bob Nystrom 的 Crafting Interpreters 。以下是第 2 章的一些引述:
编译器和解释器有什么区别?事实证明,这就像问水果和蔬菜之间的区别一样 。这似乎是一个二元的非此即彼的选择,但实际上“水果”是一个植物学术语,而“蔬菜”是烹饪学术语 。


推荐阅读