当前位置:首页 > 科技  > 软件

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

来源: 责编: 时间:2023-11-08 17:03:27 195观看
导读哈喽大家好,我是咸鱼不知道有没有小伙伴跟我一样,刚开始学习 Python 的时候都听说过 Python 是一种解释型语言,因为它在运行的时候会逐行解释并执行,而 C++ 这种是编译型语言图片不过我今天看到了一篇文章,作者提出 Python

哈喽大家好,我是咸鱼LcS28资讯网——每日最新资讯28at.com

不知道有没有小伙伴跟我一样,刚开始学习 Python 的时候都听说过 Python 是一种解释型语言,因为它在运行的时候会逐行解释并执行,而 C++ 这种是编译型语言LcS28资讯网——每日最新资讯28at.com

图片图片LcS28资讯网——每日最新资讯28at.com

不过我今天看到了一篇文章,作者提出 Python 其实也有编译的过程,解释器会先编译再执行LcS28资讯网——每日最新资讯28at.com

不但如此,作者还认为【解释】与【编译】是错误的二分法、限制了编程语言的可能性。Python 既是解释型语言,也是编译型语言!LcS28资讯网——每日最新资讯28at.com

本文文字较多,干货满满,耐心看完相信你会有不小的收获LcS28资讯网——每日最新资讯28at.com

原文:https://eddieantonio.ca/blog/2023/10/25/python-is-a-compiled-language/LcS28资讯网——每日最新资讯28at.com

前 言

本文所说的 Python ,不是指 PyPy、Mypyc、Numba、Cinder 等 Python 的替代版本,也不是像 Cython、Codon、mojo1这样的类 Python 编程语言LcS28资讯网——每日最新资讯28at.com

我指的是常规的 Python——CPythonLcS28资讯网——每日最新资讯28at.com

目前,我正在编写一份教材,教学生如何阅读和理解程序报错信息(programming error messages)。我们正在为三种编程语言(C、Python、Java)开设课程LcS28资讯网——每日最新资讯28at.com

程序报错信息的本质的关键点之一在于程序报错是在不同阶段生成的,有些是在编译时生成,有些是在运行时生成LcS28资讯网——每日最新资讯28at.com

第一门课是针对 C 语言的,具体来说是如何使用 GCC 编译器,以及演示 GCC 如何将代码转换成可执行程序LcS28资讯网——每日最新资讯28at.com

  • 预处理(preprocessing)
  • 词汇分析(lexical analysis)
  • 语法分析(syntactic analysis)
  • 语义分析(semantic analysis)
  • 链接(linking)

除此之外,这节课还讨论了在上述阶段可能出现的程序报错,以及这些报错将如何影响所呈现的错误消息。重要的是:早期阶段的错误将阻止在后期阶段检测到错误(也就是说 A 阶段的报错出现之后,B 阶段就算有错误也不会检测出来)LcS28资讯网——每日最新资讯28at.com

当我将这门课调整成针对 Java 和 Python 时,我发现 Python 和 Java 都没有预处理器(preprocessor),并且 Python 和 Java 的链接(linking)不是同一个概念LcS28资讯网——每日最新资讯28at.com

我忽略了上面这些变化,但是我偶然发现了一个有趣的现象:LcS28资讯网——每日最新资讯28at.com

编译器在各个阶段会生成报错信息,而且编译器通常会在继续执行之前把前面阶段的报错显示出来,这就意味着我们可以通过在程序中故意创建错误来发现编译器的各个阶段LcS28资讯网——每日最新资讯28at.com

所以让我们玩一个小游戏来发现 Python 解释器的各个阶段LcS28资讯网——每日最新资讯28at.com

Which is the first error ?

我们将创建一个包含多个 bug 的 Python 程序,每个 bug 都试图引发不同类型的报错信息LcS28资讯网——每日最新资讯28at.com

我们知道常规的 Python 每次运行只会报告一个错误,所以这个游戏就是——哪条报错会被首先触发LcS28资讯网——每日最新资讯28at.com

图片图片LcS28资讯网——每日最新资讯28at.com

每行代码都会产生不同的报错:LcS28资讯网——每日最新资讯28at.com

  • 1 / 0将生成 ZeroDivisionError: division by zero
  • print() = None 将生成 SyntaxError: cannot assign to function call
  • if False 将生成 SyntaxError: expected ':' .
  • ñ = "hello 将生成 SyntaxError: EOL while scanning string literal .

问题在于,哪个错误会先被显示出来?需要注意的是:Python 版本很重要(比我想象的要重要),所以如果你看到不同的结果,请记住这一点LcS28资讯网——每日最新资讯28at.com

PS:下面运行代码所使用的 Python 版本为 Python 3.12LcS28资讯网——每日最新资讯28at.com

在开始执行代码之前,先想想【解释】语言和【编译】语言对你来说意味着什么?LcS28资讯网——每日最新资讯28at.com

下面我将给出一段苏格拉底式的对话,希望你能反思一下其中的区别LcS28资讯网——每日最新资讯28at.com

苏格拉底:编译语言是指代码在运行之前首先通过编译器的语言。一个例子是 C 编程语言。要运行 C 代码,首先必须运行像  or clang 这样的 gcc 编译器,然后才能运行代码。编译后的语言被转换为机器代码,即 CPU 可以理解的 1 和 0。LcS28资讯网——每日最新资讯28at.com

柏拉图:等等,Java不是一种编译语言吗?LcS28资讯网——每日最新资讯28at.com

苏格拉底:是的,Java是一种编译语言。LcS28资讯网——每日最新资讯28at.com

柏拉图:但是常规 Java编译器的输出不是一个 .class 文件。那是字节码,不是吗?LcS28资讯网——每日最新资讯28at.com

苏格拉底:没错。字节码不是机器码,但 Java 仍然是一种编译语言。这是因为编译器可以捕获许多问题,因此您需要在程序开始运行之前更正错误。LcS28资讯网——每日最新资讯28at.com

柏拉图:解释型语言呢?LcS28资讯网——每日最新资讯28at.com

苏格拉底:解释型语言是依赖于一个单独的程序(恰如其分地称为解释器)来实际运行代码的语言。解释型语言不需要程序员先运行编译器。因此,在程序运行时,您犯的任何错误都会被捕获。Python 是一种解释型语言,没有单独的编译器,您犯的所有错误都会在运行时捕获。LcS28资讯网——每日最新资讯28at.com

柏拉图:如果 Python不是一种编译语言,那么为什么标准库包含名为 py_compile and compileall 的模块? LcS28资讯网——每日最新资讯28at.com

苏格拉底:嗯,这些模块只是将 Python转换为字节码。他们不会将  Python 转换为机器代码,因此 Python 仍然是一种解释型语言。LcS28资讯网——每日最新资讯28at.com

柏拉图:那么,Python和 Java都转换为字节码了吗?LcS28资讯网——每日最新资讯28at.com

苏格拉底:对。LcS28资讯网——每日最新资讯28at.com

柏拉图:那么,为什么Python是一种解释型语言,而 Java却是一种编译型语言呢?LcS28资讯网——每日最新资讯28at.com

苏格拉底:因为 Python 中的所有错误都是在运行时捕获的。 (ps:请注意这句话)LcS28资讯网——每日最新资讯28at.com

  • 回合一

当我们执行上面那段有 bug 的程序时,将会收到下面的错误LcS28资讯网——每日最新资讯28at.com

图片图片LcS28资讯网——每日最新资讯28at.com

检测到的第一个报错位于源码的最后一行。可以看到:在运行第一行代码之前,Python 必须读取整个源码文件LcS28资讯网——每日最新资讯28at.com

如果你脑子里有一个关于【解释型语言】的定义,其中包括”解释型语言按顺序读取代码,一次运行一行”,我希望你忘掉它LcS28资讯网——每日最新资讯28at.com

我还没有深入研究 CPython 解释器的源码来验证这一点,但我认为这是第一个检测到的报错的原因是 Python 3.12 所做的第一个步骤是扫描(scanning ),也称为词法分析LcS28资讯网——每日最新资讯28at.com

扫描器将整个文件转换为一系列标记(token),然后继续进行下一阶段。LcS28资讯网——每日最新资讯28at.com

扫描器扫描到源码最后一行的字符串字面值末尾少了个引号,它希望把整个字符串字面值转换成一个 token ,但是没有结束引号它就转换不了LcS28资讯网——每日最新资讯28at.com

在 Python 3.12 中,扫描器首先运行,所以这也是为什么第一个报错是unterminated string literalLcS28资讯网——每日最新资讯28at.com

  • 回合二

我们把第四行的代码的 bug 修复好,第 1 2 3 行仍有 bugLcS28资讯网——每日最新资讯28at.com

图片图片LcS28资讯网——每日最新资讯28at.com

我们现在来执行代码,看下哪个会首先报错LcS28资讯网——每日最新资讯28at.com

图片图片LcS28资讯网——每日最新资讯28at.com

这次是第二行报错!同样,我没有去查看 CPython 的源码,但是我有理由确定扫描的下一阶段是解析(parsing),也称为语法分析LcS28资讯网——每日最新资讯28at.com

在运行代码之前会先解析源码,这意味着 Python 不会看到第一行的错误,而是在第二行报错LcS28资讯网——每日最新资讯28at.com

我要指出我为这个小游戏而编写的代码是完全没有意义的,并且对于如何修复 bug 也没有正确的答案。我的目的纯粹是编写错误然后发现 python 解释器现在处在哪个阶段LcS28资讯网——每日最新资讯28at.com

我不知道 print() = None可能是什么意思,所以我将通过将其替换为print(None)来解决这个问题,这也没有意义,但至少它在语法上是正确的。LcS28资讯网——每日最新资讯28at.com

  • 回合三

我们把第二行的语法错误也修复了,但源码还有另外两个错误,其中一个也是语法错误LcS28资讯网——每日最新资讯28at.com

图片图片LcS28资讯网——每日最新资讯28at.com

回想一下,语法错误在回合二的时候优先显示了出来,在回合三还会一样吗LcS28资讯网——每日最新资讯28at.com

图片图片LcS28资讯网——每日最新资讯28at.com

没错!第三行的语法错误优先于第一行的错误LcS28资讯网——每日最新资讯28at.com

正如回合二一样,Python 解释器在运行代码之前会先解析源码,对其进行语法分析LcS28资讯网——每日最新资讯28at.com

这意味着 Python 不会看到第一行的错误,而是在第三行报错LcS28资讯网——每日最新资讯28at.com

你可能想知道为什么我在一个文件中插入了两个 SyntaxError,难道一个还不够表明我的观点吗?LcS28资讯网——每日最新资讯28at.com

这是因为 Python 版本的不同会导致结果的不同,如果你在 Python3.8 或者更早的版本去运行代码,那么结果如下LcS28资讯网——每日最新资讯28at.com

在 Python 3.8 中,第 2 轮报告的第一个错误消息位于第 3 行:LcS28资讯网——每日最新资讯28at.com

图片图片LcS28资讯网——每日最新资讯28at.com

修复第三行的错误之后,Python 3.8 在第 2 行报告以下错误消息:LcS28资讯网——每日最新资讯28at.com

图片图片LcS28资讯网——每日最新资讯28at.com

为什么 Python 3.8 和 3.12 报错顺序不一样?是因为 Python 3.9 引入了一个新的解析器。这个解析器比以前的 naïve 解析器功能更强大LcS28资讯网——每日最新资讯28at.com

旧的解析器无法提前查看多个 token,这意味着旧解析器在技术上可以接受语法无效的 Python 程序LcS28资讯网——每日最新资讯28at.com

尤其是这种限制导致解析器无法识别赋值语句的左边是否为有效的赋值目标,好比下面这段代码,旧解析器能够接收下面的代码LcS28资讯网——每日最新资讯28at.com

图片图片LcS28资讯网——每日最新资讯28at.com

上面这段代码没有任何意义,甚至 Python 语法是不允许这么使用的。为了解决这个问题,Python 曾经存在过一个独立的,hacky 的阶段(这个 hacky 我不知道用什么翻译比较好)LcS28资讯网——每日最新资讯28at.com

即 Python会检查所有的赋值语句,并确保赋值号左边实际上是可以被赋值的东西LcS28资讯网——每日最新资讯28at.com

而这个阶段是发生在解析之后,这也就是为什么旧版本 Python 中会先把第二行的报错先显示出来LcS28资讯网——每日最新资讯28at.com

  • 回合四

现在还剩最后一个错误了LcS28资讯网——每日最新资讯28at.com

图片图片LcS28资讯网——每日最新资讯28at.com

我们来运行一下LcS28资讯网——每日最新资讯28at.com

图片图片LcS28资讯网——每日最新资讯28at.com

需要注意的是,Traceback (most recent call last)表示 Python 运行时报错的主要内容,这里在回合四才出现LcS28资讯网——每日最新资讯28at.com

经过前面的扫描、解析阶段,Python 终于能够运行代码了。但是当 Python 开始运行解释第一行的时候,引发一个名为 ZeroDivisionError 的报错LcS28资讯网——每日最新资讯28at.com

为什么知道现在处于【运行时】,因为 Python 已经打印出 Traceback (most recent call last),这表示我们有一个堆栈跟踪LcS28资讯网——每日最新资讯28at.com

堆栈跟踪只能在运行时存在,这意味着这个报错必须在运行时捕获。LcS28资讯网——每日最新资讯28at.com

但这意味着在回合1~3 中遇到的报错不是运行时报错,那它们是什么报错?LcS28资讯网——每日最新资讯28at.com

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

没错!CPython 解释器实际上是一个解释器,但它也是一个编译器LcS28资讯网——每日最新资讯28at.com

我希望上面的练习已经说明了 Python 在运行第一行代码之前必须经过几个阶段:LcS28资讯网——每日最新资讯28at.com

  • 扫描(scanning )
  • 解析(parsing )

旧版本的 Python 多了一个额外阶段:LcS28资讯网——每日最新资讯28at.com

  • 扫描(scanning )
  • 解析(parsing )
  • 检查有效的分配目标(checking for valid assignment targets)

让我们将其与前面编译 C 程序的阶段进行比较:LcS28资讯网——每日最新资讯28at.com

  • 预处理
  • 词汇分析(“扫描”的另一个术语)
  • 语法分析(“解析”的另一个术语)
  • 语义分析
  • 链接

Python 在运行任何代码之前仍然执行一些编译阶段,就像 Java一样,它会把源码编译成字节码LcS28资讯网——每日最新资讯28at.com

前面三个报错是 Python 在编译阶段产生的,只有最后一个才是在运行时产生,即ZeroDivisionError: division by zero.LcS28资讯网——每日最新资讯28at.com

实际上,我们可以使用命令行上的 compileall 模块预先编译所有 Python 代码:LcS28资讯网——每日最新资讯28at.com

图片图片LcS28资讯网——每日最新资讯28at.com

这会将当前目录中所有 Python 文件的编译字节码放入其中 __pycache__/ ,并显示任何编译器错误LcS28资讯网——每日最新资讯28at.com

如果你想知道那个 __pycache__/ 文件夹中到底有什么,我为 EdmontonPy 做了一个演讲,你应该看看!LcS28资讯网——每日最新资讯28at.com

演讲地址:https://www.youtube.com/watch?v=5yqUTJuFuUk&t=7m11sLcS28资讯网——每日最新资讯28at.com

只有在 Python 被编译为字节码之后,解释器才会真正启动,我希望前面的练习已经证明 Python 确实可以在运行时之前报错LcS28资讯网——每日最新资讯28at.com

编译语言和解释语言是错误的二分法

每当一种编程语言被归类为【编译】或【解释】语言时,我都会感到很讨厌。一种语言本身不是编译或解释的LcS28资讯网——每日最新资讯28at.com

一种语言是编译还是解释(或两者兼而有之!)是一个实现细节LcS28资讯网——每日最新资讯28at.com

我不是唯一一个有这种想法的人。Laurie Tratt 有一篇精彩的文章,通过编写一个逐渐成为优化编译器的解释器来论证这一点LcS28资讯网——每日最新资讯28at.com

文章地址:https://tratt/laurie/blog/2023/compiled_and_interpreted_languages_two_ways_of_saying_tomato.htmlLcS28资讯网——每日最新资讯28at.com

还有一篇文章就是 Bob Nystrom 的 Crafting Interpreters。以下是第 2 章的一些引述:LcS28资讯网——每日最新资讯28at.com

编译器和解释器有什么区别?

事实证明,这就像问水果和蔬菜之间的区别一样。这似乎是一个二元的非此即彼的选择,但实际上“水果”是一个植物学术语,而“蔬菜”是烹饪学术语。LcS28资讯网——每日最新资讯28at.com

严格来说,一个并不意味着对另一个的否定。有些水果不是蔬菜(苹果),有些蔬菜不是水果(胡萝卜),但也有既是水果又是蔬菜的可食用植物,如西红柿LcS28资讯网——每日最新资讯28at.com

当你使用 CPython 来运行 Python 程序时,源码会被解析并转换成内部字节码格式,然后在虚拟机中执行LcS28资讯网——每日最新资讯28at.com

从用户的角度来看,这显然是一个解释器(因为它们从源码运行程序),但如果你仔细观察 CPython(Python 也可译作蟒蛇)的鳞状表皮(scaly skin),你会发现它肯定在进行编译LcS28资讯网——每日最新资讯28at.com

答案是:CPython 是一个解释器,它有一个编译器LcS28资讯网——每日最新资讯28at.com

那么为什么这很重要呢?为什么在【编译】和【解释】语言之间做出严格的区分会适得其反?LcS28资讯网——每日最新资讯28at.com

【编译】与【解释】限制了我们认为编程语言的可能性LcS28资讯网——每日最新资讯28at.com

编程语言不必由它是编译还是解释来定义的!以这种僵化的方式思考限制了我们认为给定的编程语言可以做的事情LcS28资讯网——每日最新资讯28at.com

例如,JavaScript 通常被归入“解释型语言”类别。但有一段时间,在 Google Chrome 中运行的 JavaScript 永远不会被解释——相反,JavaScript 被直接编译为机器代码!因此,JavaScript 可以跟上 C++ 的步伐LcS28资讯网——每日最新资讯28at.com

出于这个原因,我真的厌倦了那些说解释型语言必然慢的论点——性能是多方面的,并且不仅仅取决于"默认"编程语言的实现LcS28资讯网——每日最新资讯28at.com

JavaScript 现在很快了、Ruby 现在很快了、Lua 已经快了一段时间了LcS28资讯网——每日最新资讯28at.com

那对于通常被标记为编译型语言的编程语言呢?(例如 C)你是不会去想着解释 C 语言程序的LcS28资讯网——每日最新资讯28at.com

语言之间真正的区别LcS28资讯网——每日最新资讯28at.com

语言之间真正的区别:【静态】还是【动态】LcS28资讯网——每日最新资讯28at.com

我们应该教给学生的真正区别是语言特性的区别,前者可以静态地确定,即只盯着代码而不运行代码,后者只能在运行时动态地知道LcS28资讯网——每日最新资讯28at.com

需要注意的是,我说的是【语言特性】而不是【语言】,每种编程语言都选择自己的一组属性,这些属性可以静态地或动态地确定,并结合在一起,这使得语言更“动态”或更“静态”LcS28资讯网——每日最新资讯28at.com

静态与动态是一个范围,Python 位于范围中更动态的一端。像 Java 这样的语言比 Python 有更多的静态特性,但即使是 Java 也包括反射之类的东西,这无疑是一种动态特性LcS28资讯网——每日最新资讯28at.com

我发现动态与静态经常被混为一谈,编译与解释混为一谈,这是可以理解的LcS28资讯网——每日最新资讯28at.com

因为通常使用解释器的语言具有更多的动态特性,如 Python、Ruby 和 JavaScriptLcS28资讯网——每日最新资讯28at.com

具有更多静态特性的语言往往在没有解释器的情况下实现,例如 C++ 和 RustLcS28资讯网——每日最新资讯28at.com

然后是介于两者之间的 JavaLcS28资讯网——每日最新资讯28at.com

Python 中的静态类型注释已经逐渐(呵呵)在代码库中得到采用,其中一个期望是:由于更多静态的东西,这可以解锁 Python 代码中的性能优势LcS28资讯网——每日最新资讯28at.com

不幸的是,事实证明,Python 中的类型(是的,只是一般类型,考虑元类)和注释本身都是Python 的动态特性,这使得静态类型不是大伙所期望的性能优势LcS28资讯网——每日最新资讯28at.com

最后总结一下:LcS28资讯网——每日最新资讯28at.com

  • CPython 是一个解释器,它有一个编译器(或者说 Python 既是解释型语言,也是编译型语言)
  • Python 是编译的还是解释的并不重要。重要的是,相对于那些具有更多静态属性(在编译或解释阶段可以在运行前确定的属性)的编程语言,Python 中可以在运行前确定的属性相对较少,这意味着在 Python 中,许多属性是在运行时动态确定的,而不是在编译或解释时静态确定的
  • 由于 Python 具有较少的静态属性,这意味着在运行时,某些错误可能只能在运行时才会显现,而不是在编译或解释时就能被发现
  • 这是真正重要的区别,这是一个比【编译】和【解释】更细致、更微妙的区别。出于这个原因,我认为强调特定的静态和动态特性是很重要的,而不是一昧的局限于“解释型”和“编译型”语言之间的繁琐的区别。

本文链接:http://www.28at.com/showinfo-26-17797-0.htmlPython 既是解释型语言,也是编译型语言

声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com

上一篇: 从入门到精通:Python中SQLite数据库的实战指南!

下一篇: 聊聊如何在Java应用中发送短信

标签:
  • 热门焦点
Top