Java教程

深度对比 Python 与 Java 的区别(一)

本文主要是介绍深度对比 Python 与 Java 的区别(一),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

引入

高中有一好友,在大学期间苦练 Java,各类八股文烂熟于心,最终进入某大厂却在维护 Python 项目。

而本人不思进取,不想背八股文,于是大学期间只是苟着写 Python,然而却最终进入某互联网小厂维护 Java 项目。

世事无常,就像老话说得:

人生就像蛋炒饭,你永远不知道自己下一口吃得是蛋还是饭。

感慨于此,就写下了这篇文章,总结下自己在使用这两种语言的过程中的心得,加深自己对于这两种语言底层原理的理解,分享自己的心得体会。
玩蛇即指写 Python,喝咖啡即指写 Java,至于为什么,看完这篇文章就会明白的。

因为内容较长,可能又得分篇进行描述了。
这一篇会主要记录两种语言的发展历程、Hello World、语法特点、执行效率以及 Python 的执行机制。
下一篇会记录 Java 的执行机制与 Java 虚拟机、两种语言的并发、内存回收机制。

其中,Python 主要以 Python 3.8.2 为例,Java 以 Java SE 11 为主,同时穿插 JVM 语言 Kotlin 与 Groovy。

发展历程

Python

最早的 Python 由 Guido van Rossum 于 1980 年代后期开始研究,1991 年发布了 0.9.0 版本。
121px-Python-logo-notext.svg

Python 的命名主要来自于英国喜剧团体 Monty Python,而 Python 的一词的原意是蟒蛇,这也是为啥 Python 的 Logo 是一条蛇。

我们比较熟悉的版本为 Python 2 和 Python 3,分别于 2000 年和 2008 年发布。其中 Python 3 并不向后兼容,即 Python 3 并不兼容 Python 2 的代码,这一点与 Java 有很大的区别。

Java

最早的 Java 由 James Gosling、Mike Sheridan 和 Patrick Naughton 于 1991 年 6 月 开始研究,设计之初的语法便参考了 C/C++ 的语法。

Java 的命名历经 oak(橡树),Green 和现在的 Java(Java 是一种印度尼西亚生产的咖啡豆,这也是为什么 Java 的 logo 是一杯咖啡)。
121px-Java_programming_language_logo.svg

Java 1.0 于 1996 年发布。目前常用的版本为 Java SE 8,Java SE 11 和 Java SE 17。

与 Python 不同的是,Java 的向后兼容性非常好,多数的 JRE 都具备向后兼容的能力,但也因此,Java 背上了沉重的历史包袱。

Hello World

对于 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 就能实现,这样也使得程序员不会更多的考虑如何对这个过程进行抽象和设计,而是单纯从效率出发,尽快的实现功能。但却使得后续的维护和扩展成本变得更高。

因此对比两种语言,我个人觉得使用他们的编程成本的曲线可能是这样的:

image-20211212135011281

在功能复杂度较低的时候,Python 的开发成本更低,可以更快的实现所需功能。但当功能复杂度较高时,Java 的优势边开始显现。在一个设计规划良好的 Java 项目中,功能复杂度的增加对于 Java 开发成本的增加会比 Python 低得多。

执行效率

在Python、Java 与 C 的性能对比一文中, Java、Python 与 C 在相同规模的矩阵运算任务的耗时如下:

1N4ns2d0oFaoN3EalxVd-CQ

显然我们能够看出,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 的执行机制与 PVM(Python 虚拟机)

CPython 与其他 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 的编译

接下来针对 CPython 的编译过程进行详细讲解。

Python 源代码是以 .py 结尾的文本文件,在 Python 3 中默认是以 utf-8 格式编码存储的。

在 Python 执行前,Python 解释器会将 .py 文件编译为 .pyc 格式的字节码文件,然后交由 Python 虚拟机执行。

(有没有感觉这个过程和 JVM 的执行很类似?)
Process

为了能够更加方便的理解 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 的编译过程是怎么回事了:
compile

Python 在编译某段源码时,并不会直接返回字节码,而是返回一个 Code Object 对象,字节码则保存在该对象的 co_code 字段中。由于字节码是一个二进制字节序列,无法直接进行阅读,所以需要通过”反汇编器”(dis 模块)将字节码转换成人类可读的助记符。助记符的形式和汇编语言非常类似,均由操作指令+操作数所组成。

Python 字节码的执行

指令执行的源码均位于 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 其实也是存在虚拟机的,同时也存在编译的过程。Python 的虚拟机是由 C 语言实现的,但相对于 Java 虚拟机,Python 的虚拟机比较简单,编译过程中对性能的优化也比较弱,这一点在后续 Java 执行机制的分析中可以更清楚的感受到。


如有帮助,欢迎点赞/转载~
(听说给文章点赞的人代码bug特别少

这篇关于深度对比 Python 与 Java 的区别(一)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!