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

这篇具有很好参考价值的文章主要介绍了Python 既是解释型语言,也是编译型语言。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

哈喽大家好,我是咸鱼

不知道有没有小伙伴跟我一样,刚开始学习 Python 的时候都听说过 Python 是一种解释型语言,因为它在运行的时候会逐行解释并执行,而 C++ 这种是编译型语言
Python 既是解释型语言,也是编译型语言
不过我今天看到了一篇文章,作者提出 Python 其实也有编译的过程,解释器会先编译再执行

不但如此,作者还认为【解释】与【编译】是错误的二分法、限制了编程语言的可能性。Python 既是解释型语言,也是编译型语言!

本文文字干货较多,耐心看完相信你会有不小的收获

原文:https://eddieantonio.ca/blog/2023/10/25/python-is-a-compiled-language/

前言

本文所说的 Python ,不是指 PyPy、Mypyc、Numba、Cinder 等 Python 的替代版本,也不是像 Cython、Codon、mojo1这样的类 Python 编程语言

我指的是常规的 Python——CPython

目前,我正在编写一份教材,教学生如何阅读和理解程序报错信息(programming error messages)。我们正在为三种编程语言(C、Python、Java)开设课程

程序报错信息的本质的关键点之一在于程序报错是在不同阶段生成的,有些是在编译时生成,有些是在运行时生成

第一门课是针对 C 语言的,具体来说是如何使用 GCC 编译器,以及演示 GCC 如何将代码转换成可执行程序

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

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

当我将这门课调整成针对 Java 和 Python 时,我发现 Python 和 Java 都没有预处理器(preprocessor),并且 Python 和 Java 的链接(linking)不是同一个概念

我忽略了上面这些变化,但是我偶然发现了一个有趣的现象:

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

所以让我们玩一个小游戏来发现 Python 解释器的各个阶段

Which Is The First Error

我们将创建一个包含多个 bug 的 Python 程序,每个 bug 都试图引发不同类型的报错信息

我们知道常规的 Python 每次运行只会报告一个错误,所以这个游戏就是——哪条报错会被首先触发

# 下面是有 bug 的程序
1 / 0
print() = None
if False
    ñ = "hello

每行代码都会产生不同的报错:

  • 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 版本很重要(比我想象的要重要),所以如果你看到不同的结果,请记住这一点

PS:下面运行代码所使用的 Python 版本为 Python 3.12

在开始执行代码之前,先想想【解释】语言和【编译】语言对你来说意味着什么?

下面我将给出一段苏格拉底式的对话,希望你能反思一下其中的区别

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

柏拉图:等等,Java不是一种编译语言吗?

苏格拉底:是的,Java是一种编译语言。

柏拉图:但是常规 Java编译器的输出不是一个 .class 文件。那是字节码,不是吗?

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

柏拉图:解释型语言呢?

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

柏拉图:如果 Python不是一种编译语言,那么为什么标准库包含名为 py_compile and compileall 的模块?

苏格拉底:嗯,这些模块只是将 Python转换为字节码。他们不会将 Python 转换为机器代码,因此 Python 仍然是一种解释型语言。

柏拉图:那么,Python和 Java都转换为字节码了吗?

苏格拉底:对。

柏拉图:那么,为什么Python是一种解释型语言,而 Java却是一种编译型语言呢?

苏格拉底:因为 Python 中的所有错误都是在运行时捕获的。 (ps:请注意这句话)

  • 回合一

当我们执行上面那段有 bug 的程序时,将会收到下面的错误

  File "/private/tmp/round_1.py", line 4
    ñ = "hello  # SyntaxError: EOL while scanning string literal
        ^
SyntaxError: unterminated string literal (detected at line 4)

检测到的第一个报错位于源码的最后一行。可以看到:在运行第一行代码之前,Python 必须读取整个源码文件

如果你脑子里有一个关于【解释型语言】的定义,其中包括”解释型语言按顺序读取代码,一次运行一行”,我希望你忘掉它

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

扫描器将整个文件转换为一系列标记(token),然后继续进行下一阶段。

扫描器扫描到源码最后一行的字符串字面值末尾少了个引号,它希望把整个字符串字面值转换成一个 token ,但是没有结束引号它就转换不了

在 Python 3.12 中,扫描器首先运行,所以这也是为什么第一个报错是unterminated string literal

  • 回合二

我们把第四行的代码的 bug 修复好,第 1 2 3 行仍有 bug

1 / 0
print() = None
if False
    ñ = "hello"

我们现在来执行代码,看下哪个是第一个报错

File "/private/tmp/round_2.py", line 2
    print() = None
    ^^^^^^^
SyntaxError: cannot assign to function call here. Maybe you meant '==' instead of '='?"

这次是第二行报错!同样,我没有去查看 CPython 的源码,但是我有理由确定扫描的下一阶段是解析(parsing),也称为语法分析

在运行代码之前会先解析源码,这意味着 Python 不会看到第一行的错误,而是在第二行报错

我要指出我为这个小游戏而编写的代码是完全没有意义的,并且对于如何修复 bug 也没有正确的答案。我的目的纯粹是估计编写错误然后发现 python 解释器现在处在哪个阶段

我不知道 print() = None可能是什么意思,所以我将通过将其替换为print(None)来解决这个问题,这也没有意义,但至少它在语法上是正确的。

  • 回合三

我们把第二行的语法错误也修复了,但源码还有另外两个错误,其中一个也是语法错误

1 / 0
print(None)
if False
    ñ = "hello"

回想一下,语法错误在回合二的时候优先显示了出来,在回合三还会一样吗

  File "/private/tmp/round_3.py", line 3
    if False
            ^
SyntaxError: expected ':'

没错!第三行的语法错误优先于第一行的错误

正如回合二一样,Python 解释器在运行代码之前会先解析源码,对其进行语法分析

这意味着 Python 不会看到第一行的错误,而是在第三行报错

你可能想知道为什么我在一个文件中插入了两个 SyntaxError,难道一个还不够表明我的观点吗?

这是因为 Python 版本的不同会导致结果的不同,如果你在 Python3.8 或者更早的版本去运行代码,那么结果如下

在 Python 3.8 中,第 2 轮报告的第一个错误消息位于第 3 行:

1 / 0
print() = None
if False
    ñ = "hello"
  
# 报错
File "round_2.py", line 3
    if False
            ^
SyntaxError: invalid syntax

修复第三行的错误之后,Python 3.8 在第 2 行报告以下错误消息:

1 / 0
print() = None
if False:
    ñ = "hello"

# 报错
  File "round_3.py", line 2
    print() = None
    ^
SyntaxError: cannot assign to function call

为什么 Python 3.8 和 3.12 报错顺序不一样?是因为 Python 3.9 引入了一个新的解析器。这个解析器比以前的 naïve 解析器功能更强大

旧的解析器无法提前查看多个 token,这意味着旧解析器在技术上可以接受语法无效的 Python 程序

尤其是这种限制导致解析器无法识别赋值语句的左边是否为有效的赋值目标,好比下面这段代码,旧解析器能够接收下面的代码

[x for x in y] = [1,2,3]

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

Python会检查所有的赋值语句,并确保赋值号左边实际上是可以被赋值的东西

而这个阶段是发生在解析之后,这也就是为什么旧版本 Python 中会先把第二行的报错先显示出来

  • 回合四

现在还剩最后一个错误了

1 / 0
print(None)
if False:
    ñ = "hello"

我们来运行一下

Traceback (most recent call last):
  File "/private/tmp/round_4.py", line 1, in <module>
    1 / 0
    ~~^~~
ZeroDivisionError: division by zero

需要注意的是,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 代码:

$ python3 -m compileall

这会将当前目录中所有 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 章的一些引述:

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

事实证明,这就像问水果和蔬菜之间的区别一样。这似乎是一个二元的非此即彼的选择,但实际上“水果”是一个植物学术语,而“蔬菜”是烹饪学术语。

严格来说,一个并不意味着对另一个的否定。有些水果不是蔬菜(苹果),有些蔬菜不是水果(胡萝卜),但也有既是水果又是蔬菜的可食用植物,如西红柿

当你使用 CPython 来运行 Python 程序时,源码会被解析并转换成内部字节码格式,然后在虚拟机中执行

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

答案是:CPython 是一个解释器,它有一个编译器

那么为什么这很重要呢?为什么在【编译】和【解释】语言之间做出严格的区分会适得其反?

【编译】与【解释】限制了我们认为编程语言的可能性

编程语言不必由它是编译还是解释来定义的!以这种僵化的方式思考限制了我们认为给定的编程语言可以做的事情

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

出于这个原因,我真的厌倦了那些说解释型语言必然慢的论点——性能是多方面的,并且不仅仅取决于"默认"编程语言的实现

JavaScript 现在很快了、Ruby 现在很快了、Lua 已经快了一段时间了

那对于通常被标记为编译型语言的编程语言呢?(例如 C)你是不会去想着解释 C 语言程序的

语言之间真正的区别

语言之间真正的区别:【静态】还是【动态】

我们应该教给学生的真正区别是语言特性的区别,前者可以静态地确定,即只盯着代码而不运行代码,后者只能在运行时动态地知道

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

静态与动态是一个范围,Python 位于范围中更动态的一端。像 Java 这样的语言比 Python 有更多的静态特性,但即使是 Java 也包括反射之类的东西,这无疑是一种动态特性

我发现动态与静态经常被混为一谈,编译与解释混为一谈,这是可以理解的

因为通常使用解释器的语言具有更多的动态特性,如 Python、Ruby 和 JavaScript

具有更多静态特性的语言往往在没有解释器的情况下实现,例如 C++ 和 Rust

然后是介于两者之间的 Java

Python 中的静态类型注释已经逐渐(呵呵)在代码库中得到采用,其中一个期望是:由于更多静态的东西,这可以解锁 Python 代码中的性能优势

不幸的是,事实证明,Python 中的类型(是的,只是一般类型,考虑元类)和注释本身都是Python 的动态特性,这使得静态类型不是大伙所期望的性能优势

最后总结一下:文章来源地址https://www.toymoban.com/news/detail-746008.html

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

到了这里,关于Python 既是解释型语言,也是编译型语言的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【Python知识】11 个最佳的 Python 编译器和解释器,码住了,万一哪天就用上了!

    Python 是一门对初学者友好的编程语言,是一种多用途的、解释性的和面向对象的高级语言。 它拥有非常小的程序集,非常易于学习、阅读和维护。其解释器可在Windows、Linux 和 Mac OS 等多种操作系统上使用。它的可移植性和可伸缩性等特性使得它更加容易被运用。 Python 库可用

    2024年02月06日
    浏览(52)
  • 大语言模型也是知识库:基于知识的对话大模型综述

    ©PaperWeekly 原创 · 作者 | 缥缈孤鸿影 引言 ChatGPT 的横空出世,在整个自然语言处理乃至人工智能领域均掀起波澜。不同于普通的闲聊式机器人和任务型智能客服仅局限于固定场景,ChatGPT 具有相当丰富的知识储备,对于很多冷门的知识,它亦能对答如流,堪称当代“百晓生”

    2024年02月09日
    浏览(49)
  • 【解释器设计模式详解】C/Java/Go/JS/TS/Python不同语言实现

    解释器模式(Interpreter Pattern)是一种行为型设计模式。这种模式实现了一个表达式接口,该接口解释一个特定的上下文。这种模式常被用在 SQL 解析、符号处理引擎等。 解释器模式常用于对简单语言的编译或分析实例中,为了掌握好它的结构与实现,必须先了解编译原理中的

    2023年04月12日
    浏览(188)
  • python实现刮刮乐(遮罩层也是图片)

    python实现刮刮乐(遮罩层也是图片) python实现刮刮乐效果,与普通刮刮乐有些不同,被刮掉的遮罩层是图片,当其被刮掉后,露下面的图片。在游戏循环中,监听鼠标移动事件,来模拟刮开效果。 效果图  Pygame是一组用来开发游戏软件的 Python 程序模块,基于 SDL 库的基础上

    2024年02月06日
    浏览(77)
  • 【2023,学点儿新Java-14】携程面试题:如何看待Java是一门半编译半解释型的语言?| 咨询互联网行业 资深前辈的一些问题 | 附:为什么说ChatGPT的核心算法是...?| GPT-3.5

    前情回顾: 【2023,学点儿新Java-13】阶段练习之Java面试企业真题(阿里巴巴拼多多 等) | 常用的Java命令行操作都有哪些 | 如何解决Java的内存泄漏和内存溢出问题? 【2023,学点儿新Java-12】小结:阶段性复习 | Java学习书籍推荐(小白该读哪类Java书籍?有一定基础后,再去读

    2024年02月09日
    浏览(46)
  • Python制作爱心跳动代码,你也是天才程序员

    前端时间电视剧《点燃我,温暖你》正在热播中,里面的天才程序员李峋制作的爱心跳动代码是不是震撼了你的心,今天我们用Python来尝试一下制作爱心跳动代码吧! 怎么说呢,用这个表白也可以的,万一她也看这个剧呢,万一就成了呢 哈哈 冲啊,兄弟们 okok 话不多说,现

    2024年02月09日
    浏览(61)
  • python语言在线编译器,python 在线编程工具

    大家好,小编来为大家解答以下问题,python语言在线编译器,python 在线编程工具,今天让我们一起来看看吧! 1.python在线编译器的解决方案 方案一:vscode web版(vscode online) 大名鼎鼎的vscode 推出了web版,也就是说可以在网页上进行编程了。 github地址:https://github.com/microsoft/v

    2024年04月26日
    浏览(40)
  • OPENCV 编译选项以及解释

    cmake编译以及模块介绍,后面遇到没有的会继续再补充 CMAKE_BUILD_TYPE:指定构建类型,如Debug、Release等。 CMAKE_INSTALL_PREFIX:指定安装目录。 BUILD_SHARED_LIBS:设置为ON时,构建共享库;设置为OFF时,构建静态库。 BUILD_WITH_STATIC_CTR:当为ON是,构建静态运行时库(MT/MTd),设置为

    2024年02月06日
    浏览(35)
  • 编译器、链接器和解释器

    编译器的作用就是将高级编程语言翻译为机器代码。 编译器工作过程一般分为: 词法分析:将高级语言解析成 Token 集合; 语法分析:将 Token 集合构建成语法树,在这个过程可以判断出语法是否有误,比如 while 后面是否 { 等等; 语义分析:判断语法树是否有明显的语义错处

    2024年02月14日
    浏览(38)
  • JVM执行引擎——解释器与编译器JIT

            执行引擎是JVM核心的组成部分之一,因为字节码文件不能直接运行在操作系统上,所以执行引擎就充当了将字节码文件翻译为机器码,是将高级语言转化为机器语言的桥梁。         执行引擎有两种行为方式:解释执行和编译执行。         解释器:当J

    2024年02月15日
    浏览(51)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包