多年以来,Python 语言一直受到性能、应用程序打包以及项目管理三大问题的困扰。好在,解决方案即将到来。
虽然 Python 诞生距今已经有 30 年左右,但就在过去几年当中,其受欢迎程度开始快速提升并达到旷古烁今的地步。当下,唯有 Java 及 C 等顶尖高手能够与之匹敌。另外,Python 的普及程度超越了传统编程语言,目前在教学与学术研究当中成为最优编程方法、理想的软件开发起点以及几乎一切技术堆栈的重要组成部分。
遗憾的是,旺盛的人气也放大了 Python 语言的固有缺陷。与其优点一样,Python 也有着不少天生顽疾——性能、应用程序打包与交付,以及项目管理困难,一直是支持者心中永远的痛。虽然这些并不算什么致命缺陷,只能说是 Python 普及道路上的一点障碍;但随着 Julia、Nim、Rust 以及 Go 等其他竞争语言的崛起,解决这些问题已经成为 Python 的燃眉之急。
在本文中,我们将探讨 Python 程序员面对的三大主要挑战,以及 Python 自身、第三方 Python 工具以及库开发者们尝试解决这些挑战的具体方法。
Python 多线程与速度
问题:Python 的总体性能较慢,有限的线程与孱弱的多处理能力成为其未来发展的主要障碍。
Python 长期以来一直更重视编程速度,而非运行速度。考虑到很多开发者习惯于利用 C 或 C++编写高速外部库(例如 NumPy 或者 Numba)以执行 Python 下的性能密集型任务,这样的权衡似乎也没什么大不了。但问题在于,Python 的开箱性能仍然落后于其它语法同样简单、但能够编译为机器码的语言,例如 Nim 或者 Julia。
Python 当中历史最悠久的性能问题之一,在于其对多核心或处理器的资源使用能力不佳。虽然 Python 确实具有线程功能,但却仅限于单一核心。此外,Python 也会尝试通过启动其运行时的子实例以支持多处理,但是针对这些子进程结果的调度与同步往往效率不高。
解决方案:目前,还没有某一种自上而下的整体性解决方案,能够直接搞定 Python 的性能问题。不过,现在已经出现了一系列用于加速 Python 的尝试,其各自都在特定领域做出了一定改进。
下面来看例子:
改善 CPython 的内部行为。 CPython 改进带来了幅度有限但却覆盖面广泛的加速效果。例如,Python 3.8 的 Vectorcall 协议为 Python 对象带来了更快的调用约定。虽然改进效果不算显著,但足以带来具有可测量且可预测的性能提升,而且完全不会破坏向下兼容性;此外,现有 Python 应用程序可直接受益,无需任何代码重写。
改进 CPython 的子解释器功能。 Python 解释器实例的新编程接口现在可以时在各解释器之间实现优雅的数据共享,从而实现多核处理。现在,这项提案已经确定将在 Python 3.9 中面世,相信其还将在后续版本中继续发挥重要作用。
改进多个进程之间的对象共享。Python 当中的多处理机制会为每个核心启动一个新的解释器实例,用以获取最佳性能;但当多个解释器尝试对同一内存对象进行操作时,大部分性能提升都会瞬间作废。目前,以 SahredMemory 类以及新的 pickle 协议为代表的新功能,可以减少解释器之间数据传递所需要的复制或者序列化过程,从根本上消除相关性能问题。
在 Python 之外,也有不少外部项目带来了新的性能提升方法——但同样仅限于特定问题:
PyPy。另一种 Python 解释器,PyPy 能够将 Python 即时编译为本机机器码。它在纯 Python 项目当中发挥出色,现在也能很好地兼容比较流行的二进制相关库——例如 NumPy。但其一般更适合长期运行的服务,而非一次性应用程序。
Cython。 Cython允许用户逐步将 Python 代码转换为 C 代码。该项目最初是专为科学与数值计算所设计的,但却能够在大多数场景下起效。Cython 最大的缺点在于语法,其使用了独有的语法设置,且转换只能单向进行。Cython 最适合处理“热点”部分代码,这种有针对性的优化方式往往比应用程序整体优化要更合理、也更可行。
Numba。 Numba 的即时编译功能可以面向选定功能将 Python 代码编译为机器码。与 Cython 类似,Numba 同样主要用于科学计算,其比较适合就地运行而非对代码进行重新发布。
Mypyc。Mypyc 项目目前仍在开发当中,其希望将带有 mypy 类型注释的 Python 代码转换为 C 代码。Mypyc 很有前途,因为其使用到 Python 中的众多原生类型,但目前距离生产应用还有很长的路要走。
经过优化的 Python 发行版。某些第三方 Python 版本(例如英特尔的 Python 发行版)拥有可充分发挥英特尔处理器扩展(例如 AVX512)优势的数学与统计库。需要注意的是,尽管其能够显著加快特定数学函数的执行速度,但却无法实现全面的速度提升。
有经验的 Python 程序员一定还会提到全局解释器锁(GIL)的问题,其负责对指向对象的访问进行序列化,以确保不同线程不会彼此影响到对方的工作负载。从理论上讲,放弃 GIL 可以提高性能。然而,无 GIL Python 基本上丧失了向下兼容能力(特别是在 Python C 扩展方面)。因此到目前为止,所有移除 GIL 的尝试要么已经走进死胡同,要么反而降低了 Python 的性能。
目前另一个正在推进的 Python 计划有望解决不少速度方面的问题,即重构 Python 内部的 C API 实现。众长远来看,提升 API 集的有序程度可以带来诸多性能改进:重新设计或者剔除 GIL、提供可实现强大即时编译的 hook、在解释器实例之间使用更好的数据联合方法等等。
Python 打包与独立可执行文件
问题:即使是在 30 年之后,Python 仍然没能拿到理想的方法,用以将程序或脚本转换为自打包可执行文件,并轻松部署在多种平台之上。
虽然这一目标已经有办法实现,但依靠的主要是第三方工具,而非 Python 的原生功能——此外,其使用难度也不太友好。
最具知名且最受支持的当数PyInstaller,它能够打包多种广受喜爱的高人气第三方扩展,例如 NumPy。不过PyInstaller必须与这些第三方扩展手动保持同步,而这在 Python 的庞大生态系统中无疑是一项艰巨的任务。此外,PyInstaller会生成超大的应用程序包,因为其中捆绑有程序 import 语句中所要求的全部内容,而且并不确定在运行时究竟会用到哪些组件。此外,PyInstaller也无法实现跨平台应用程序打包;我们必须在目标部署平台上进行包创建。
解决方案: 我对 PyInstaller 抱有充分的尊重,但我们可能还需要一套 Python 原生解决方案——无论是内置形式,还是通过标准库提供。它应当允许开发人员以独立二进制文件的形式将 Python 应用程序打包并交付至各类常见的平台。理想情况下,这款内置的打包器应该利用运行时代码覆盖率信息确保仅打包所需的库(而非所有内容),并能够自动与其余 Python 库保持同步。
目前还没有这样的解决方案存在,但关于如何构建此类工具的提示倒是所在多有。PyOxidizer 项目使用 Rust 语言生成能够嵌入 Python 的二进制文件,并将此作为创建独立 Python 应用程序的一种方法。虽然该项目尚处于起步阶段,距离成为完整的应用程序交付方案还有很长的路要走,但这足以证明 Python 生态系统之外的成果也许会成为解决挑战的关键。
Python 安装、软件包管理与项目管理
问题: 有些使用体验太过复杂——对,说的就是为专业级 Python 项目设置工作区、目录结构与基本架构;管理与项目相关的环境、软件包及依赖项;以可重复方式重新分配项目来源;并一次又一次不断进行这个过程。
Rust 与 Go 这两种语言,在初始设计阶段就强调提供一种单一且规范的项目设置方式,并允许开发者在整个生命周期之内对其进行轻松管理。Rust 与 Go 开发人员虽然因此牺牲掉了一定程度的灵活性,但却换来了良好的一致性、可预测性以及可管理性。
Python 提供的安装、软件包以及项目管理工具与方法,随着时间推移而不断积压,并成为 30 年发展周期中的一笔重大负担。其中有用于软件包管理的 pip、用于创建虚拟环境的 venv/virtualenv、用于元管理的 virtualenvwrapper 与 Pipenv、用于生成项目依赖性的 pip-tools、以及用于创建 Python 代码发行版的 distutils 与 setuptools 等。此外,还有负责定义项目其它部分的 setup.py、requirements.txt、setup.cfg、MANIFEST.in 以及 Pipfile 等等。
解决方案: 同样的,我们需要取代这如同一团乱麻的工具与流程大杂烩,由 Python 核心开发团队拿出一套规范性的解决方案,同时确保其能够优雅地迁移一切利用现有方法开发出的项目。当然,这是个很难解决的挑战,但随着 Python 语言变得越来越重要,我们必须努力让它成为一款易于上手、维护简单、一致性强且友好舒适的编程工具。
目前这方面工作已经有所进展。根据 PEP 518 提案,Python 的 build 依赖项被合并为 pyproject.toml 文件格式。而像poetry 这样的第三方工具虽然只能打包现有工具,但已经体现出一体化管理产品的样貌。随着时间的推移,其中一种解决方案也许会脱颖而出、受到整个社区的关注,并成为客观层面甚至是范式性的处理标准。
原文链接:
评论 1 条评论