高中有一好友,在大学期间苦练 Java,各类八股文烂熟于心,最终进入某大厂却在维护 Python 项目。
而本人不思进取,不想背八股文,于是大学期间只是苟着写 Python,然而却最终进入某互联网小厂维护 Java 项目。
世事无常,就像老话说得:
人生就像蛋炒饭,你永远不知道自己下一口吃得是蛋还是饭。
感慨于此,就写下了这篇文章,总结下自己在使用这两种语言的过程中的心得,加深自己对于这两种语言底层原理的理解,分享自己的心得体会。
玩蛇即指写 Python,喝咖啡即指写 Java,至于为什么,看完这篇文章就会明白的。
因为内容较长,可能又得分篇进行描述了。
这一篇会主要记录两种语言的发展历程、Hello World、语法特点、执行效率以及 Python 的执行机制。
下一篇会记录 Java 的执行机制与 Java 虚拟机、两种语言的并发、内存回收机制。
其中,Python 主要以 Python 3.8.2 为例,Java 以 Java SE 11 为主,同时穿插 JVM 语言 Kotlin 与 Groovy。
最早的 Python 由 Guido van Rossum 于 1980 年代后期开始研究,1991 年发布了 0.9.0 版本。
Python 的命名主要来自于英国喜剧团体 Monty Python,而 Python 的一词的原意是蟒蛇,这也是为啥 Python 的 Logo 是一条蛇。
我们比较熟悉的版本为 Python 2 和 Python 3,分别于 2000 年和 2008 年发布。其中 Python 3 并不向后兼容,即 Python 3 并不兼容 Python 2 的代码,这一点与 Java 有很大的区别。
最早的 Java 由 James Gosling、Mike Sheridan 和 Patrick Naughton 于 1991 年 6 月 开始研究,设计之初的语法便参考了 C/C++ 的语法。
Java 的命名历经 oak(橡树),Green 和现在的 Java(Java 是一种印度尼西亚生产的咖啡豆,这也是为什么 Java 的 logo 是一杯咖啡)。
Java 1.0 于 1996 年发布。目前常用的版本为 Java SE 8,Java SE 11 和 Java SE 17。
与 Python 不同的是,Java 的向后兼容性非常好,多数的 JRE 都具备向后兼容的能力,但也因此,Java 背上了沉重的历史包袱。
对于 Hello World 而言,Python 几乎是所有编程语言中最简洁的,仅需要一行代码:
print("hello world!")
而相对而言,Java 就显得略显繁琐:
public class demo{ public static void main(String[] args){ System.out.println("hello world!"); } }
当然,考虑到 JVM 语言并不是只有 Java,有不少 JVM 语言都对 Java 进行了优化。
例如 Kotlin:
fun main(args: Array<String>){ println("hello world!") }
相对于 Java,Kotlin 的语法显得格外简洁。Kotlin 可以省略行尾的分号,同时 main 函数可以单独出现,而不必在某个类中。
例如 Groovy:
println("Hello World")
Groovy 的 Hello world 相比前两者的简洁程度则更进一步,不仅不需要存在于某个类中,连 main 函数都可省略。
说编程语言的语法特点就不得不说编程范式。
Robert Floyd 在 1979 年图灵奖的颁奖演说中使用了编程范式一词。简单来说,**编程范式是程序员看待程序应该具有的观点,代表了程序设计者认为程序应该如何被构建和执行的看法。**常见的编程范式有:命令式、声明式、过程式、面向对象、函数式、泛型编程等。
命令式:用语句更改程序的状态的编程范式。
声明式:它指定程序应该做什么,而不具体说明怎么做。例如 SQL 和正则。
面向对象:关键词为 封装 抽象 继承 多态
一些编程语言是专门为某种特定范式设计的,例如 C 语言是过程式编程语言,Java 是较纯粹的面向对象编程语言。但编程语言和编程范式的关系并不一一对应,Python,Groovy 都支持面向对象和一定程度上的函数式编程。
前面说到,Java 在设计之初,其语法很大程度上收到 C/C++ 的影响。但与 C++ 不同的是,Java 中除了 8 种基本类型之外,全部是基于对象的。Java 代码都是写在类里面的。因此说 Java 是比较纯粹的面向对象编程语言。然而 Java 8 开始引入了 Stream,提供了函数式编程的能力。
而对于 Python,Python 既支持过程式编程,也支持面向对象。因此相对而言 Python 的编程语法较为自由。
由于 Java 与 Python 的语法区别,两者在真正完成编程工作过程中的感觉也是不一样。
有一种说法是,写 Java 是自顶向下的,而写 Python 是自底向上的。
这是可能是因为 Java 的面向对象特性,使得在编程过程中会不自觉的先定义父类,然后继承父类,根据多态来定义子类应有的属性和方法。或者是先定义接口,再根据接口去实现对应的函数。这个过程会使得程序员在编程过程中不自觉的自顶向下的进行思考,会在拿到需求后先进行抽象和设计,再逐步实现。
而 Python 则有所不同。由于 Python 是一门多范式的编程语言,对过程式编程的支持使得用 Python 实现一个功能变得很方便。
同时 Python 本身内置了大量方便简洁的 API,如序列化用的 json,文件目录处理 os 等,这些 API 让程序员面对一些用 Java 来做可能需要创建好几个类才能实现的功能简单调用一下 API 就能实现,这样也使得程序员不会更多的考虑如何对这个过程进行抽象和设计,而是单纯从效率出发,尽快的实现功能。但却使得后续的维护和扩展成本变得更高。
因此对比两种语言,我个人觉得使用他们的编程成本的曲线可能是这样的:
在功能复杂度较低的时候,Python 的开发成本更低,可以更快的实现所需功能。但当功能复杂度较高时,Java 的优势边开始显现。在一个设计规划良好的 Java 项目中,功能复杂度的增加对于 Java 开发成本的增加会比 Python 低得多。
在Python、Java 与 C 的性能对比一文中, Java、Python 与 C 在相同规模的矩阵运算任务的耗时如下:
显然我们能够看出,Python 的运行效率可以说是完败于 Java 和 C 的。
这是因为 Python 的执行过程是需要逐行解释的,同时作为动态语言,Python 需要在运行过程中确定对象的类型。这也大大降低了 Python 的运行效率。
然而,观察上图更有意思的是 Java 的执行效率竟然比未经 GCC 优化的运行效率要高得多!这就牵扯到 Java 的执行机制了。
与 C 相同,Java 也会有编译的过程,不过该过程只是将源文件(后缀为.java
)通过 Java 编译器编译成二进制的字节码文件(后缀为.class
),将字节码文件放入 JVM 中执行。真正使得 Java 运行效率大增的是 JIT(Just-in-time compilation,即时编译)技术,该技术通过在运行过程中将字节码编译成本地机器代码来提高 Java 程序的性能。JVM 可以直接调用编译后的代码,而不需要耗费处理器时间和内存来解释代码。
刚才已经说过,Python 与 Java 的性能差异来自其执行机制的不同,那么两者的执行机制有什么区别呢?
Python 的执行机制比较简单,先来看看 Python 的。
在解释 Python 的执行机制之前,首先得定义清楚我们说的 Python 是啥。
我们经常会说,Python 是一门解释性语言,仿佛是说运行 Python 代码的过程中是 Python 解释器在逐步解释每一行 Python 源码,而不存在编译的过程。
然而事实是这样的吗?
并不是这样的。
Python 是一种编程语言,但这只是说明了其语法规定。但对于 Python 的实现,是有很多类型的。
我们一般用的 Python,默认都是指 CPython,即用 C 语言写成的 Python。
但是除了 CPython 以外,还有 Jython、IronPython 等等。
Jyhton 会将 Python 代码编译成 Java 字节码,运行在 JVM 上;
IronPython 会编译成 CLR 代码,运行在 .NET 上。
由于要运行在特定的虚拟机中,这些非 CPython 的实现都存在编译过程。
而回到广泛使用的 Python 版本 CPython,它同样存在一个编译过程,只不过由于 CPython 在设计时主要考虑的就是编译过程尽可能的快,因此这个过程常常短到被忽略。(但也同样由于这个编译过程较短,带来的优化较少,成为 Python 执行效率较低的原因之一)
所以说 Python 是一门解释性语言 这个说法本身就不对,准确的说法是,Python 是一门动态语言。(动态语言是指存在运行过程中的类型推断,而不是在运行前就已经确定对象的类型)
接下来针对 CPython 的编译过程进行详细讲解。
Python 源代码是以 .py
结尾的文本文件,在 Python 3 中默认是以 utf-8
格式编码存储的。
在 Python 执行前,Python 解释器会将 .py
文件编译为 .pyc
格式的字节码文件,然后交由 Python 虚拟机执行。
(有没有感觉这个过程和 JVM 的执行很类似?)
为了能够更加方便的理解 Python 的执行过程,我参考了博主 smartkeyerror 写的 Pyton 虚拟机一文,通过一段代码详细的展示这个过程:
首先我们定义一个简单的函数:
def func(): for i in range(3): print(f"output: {i}")
如果将这段代码以文本形式存储,然后调用 Python 的内建函数 compile
进行编译,会得到这段代码对应的 Code Object 对象:
snippet = """ def func(): for i in range(3): print(f"output: {i}") """ result = compile(snippet, "", "exec") print(type(result)) # 输出为 <class 'code'> print(result) # 输出为 <code object <module> at 0x000001D95B1EFC90, file "", line 2>
这里我们得到的 reslut 就是 Code Object 对象,在该对象中保存着源代码所对应的字节。
查看 CPython 的源代码可以在 cpython/Include/code.h
中找到 PyCodeObject
结构体,即 Code Object 对象:
/* Bytecode object */ typedef struct { PyObject_HEAD /* Python定长对象头 */ PyObject *co_code; /* 指令操作码,即字节码 */ PyObject *co_consts; /* 常量列表 */ PyObject *co_names; /* 名称列表(不一定是变量,也可能是函数名称、类名称等) */ PyObject *co_filename; /* 源码文件名称 */ ... /* 省略若干字段 */ } PyCodeObject;
字段 co_code
即为 Python 编译后字节码,其它字段在此处可暂时忽略。字节码的格式为人类不可阅读格式,其形式通常是这样的:
print(result.co_code) # 输出为 b'd\x00d\x01\x84\x00Z\x00d\x02S\x00'
通过调用 Python 字节码反编译器 dis
,可以反编译 result.co_code
字节码得到助记符:
import dis dis.dis(result.co_code)
这就是定义函数 fun
对应的 Python 字节码。可以看出来这已经非常类似于汇编语言的指令了,均由操作指令和操作数所组成。
0 LOAD_CONST 0 (0) 2 LOAD_CONST 1 (1) 4 MAKE_FUNCTION 0 6 STORE_NAME 0 (0) 8 LOAD_CONST 2 (2) 10 RETURN_VALUE
以上的助记符不难看出是关于如何定义函数的。如果想看函数功能代码对应的助记符,可以直接反编译函数本身:
dis.dis(func)
得到结果如下:
2 0 LOAD_GLOBAL 0 (range) 2 LOAD_CONST 1 (3) 4 CALL_FUNCTION 1 6 GET_ITER >> 8 FOR_ITER 18 (to 28) 10 STORE_FAST 0 (i) 3 12 LOAD_GLOBAL 1 (print) 14 LOAD_CONST 2 ('output: ') 16 LOAD_FAST 0 (i) 18 FORMAT_VALUE 0 20 BUILD_STRING 2 22 CALL_FUNCTION 1 24 POP_TOP 26 JUMP_ABSOLUTE 8 >> 28 LOAD_CONST 0 (None) 30 RETURN_VALUE
看到这里我们就能明白 Python 的编译过程是怎么回事了:
Python 在编译某段源码时,并不会直接返回字节码,而是返回一个 Code Object 对象,字节码则保存在该对象的 co_code
字段中。由于字节码是一个二进制字节序列,无法直接进行阅读,所以需要通过”反汇编器”(dis
模块)将字节码转换成人类可读的助记符。助记符的形式和汇编语言非常类似,均由操作指令+操作数所组成。
指令执行的源码均位于 cpython/Python/ceval.c
中,入口函数有两个,一个是 PyEval_EvalCode
,另一个则是 PyEval_EvalCodeEx
,最终的实际调用函数为 _PyEval_EvalCodeWithName
,所以我们只需要关注该函数即可。
_PyEval_EvalCodeWithName
函数的主要作用为进行函数调用的例常检查,例如校验函数参数的个数、类型,校验关键字参数等。除此之外,该函数将会初始化栈帧对象并将其交给 PyEval_EvalFrame
函数进行处理,最终由 _PyEval_EvalFrameDefault
函数真正的运行指令。
_PyEval_EvalFrameDefault
函数定义超过了 3K 行,绝大部分的逻辑其实都是 switch-case
,即根据指令类型执行相应的逻辑。
for (;;) { switch (opcode) { case TARGET(LOAD_CONST): { /* 加载常量 */ ... } case TARGET(ROT_TWO): { /* 交换两个变量 */ ... } case TARGET(FORMAT_VALUE):{ /* 格式化字符串 */ ... }
可以看到 TARGET()
调用中的参数其实就是 dis
方法返回的助记符,当我们在分析助记符的具体实现逻辑时,可以在该文件中找到对应的 C 实现方法。
如果对 Python 字节码指令感兴趣,可以在 Python 官方文档中关于虚拟机字节码指令的部分 查阅。
根据上述分析可以看出,Python 其实也是存在虚拟机的,同时也存在编译的过程。Python 的虚拟机是由 C 语言实现的,但相对于 Java 虚拟机,Python 的虚拟机比较简单,编译过程中对性能的优化也比较弱,这一点在后续 Java 执行机制的分析中可以更清楚的感受到。
如有帮助,欢迎点赞/转载~
(听说给文章点赞的人代码bug特别少