本文将通过对变量声明、模块归属、依赖冲突、包管理、全局解释器锁以及并发和并行计算等方面的分析,向你解释为什么不推荐在开发大型项目时使用Python。
译者 | 朱先忠
审校 | 千山
本文将通过对变量声明、模块归属、依赖冲突、包管理、全局解释器锁以及并发和并行计算等方面的分析,向你解释为什么不推荐在开发大型项目时使用Python。
Python并非你想象中那么棒!
在开发人员的职业生涯中,有一个特定的阶段,即从为项目做出贡献到发明自己的技术。对一些人来说这个阶段会来得更早一些,对另一些人来说则可能更晚,而有些人根本就没有这个阶段。大多数职业生涯较长的开发人员都经历过这一点,我称之为“自己动手建造”阶段。
如果你是一位职业生涯较长的开发人员,那么你应该知道本小节标题的真正含义:它是如何工作的?用户体验如何?应用框架是什么?数据是如何流动的?还有更多类似的问题。
我不会在这里为你回答所有这些问题。针对你要开始的任何项目,它们都是一些非常具体的问题。这些问题中的每一个都值得至少使用一篇文章来加以解释。
不过,我还是乐意回答一个问题:哪种语言最适合项目开发?
你可能会认为这也是因项目而异的非常具体的问题。的确,你并没有完全错。但每种编程语言都有一些缺陷。事实证明,Python也存在很多陷阱。尤其是当你试图用它来构建一个大型程序时。
变量不声明导致的问题
Python禅宗(Zen of Python)的建议是:显式优于隐式。然而,在实际Python开发中,隐式声明比显式声明更常见。
与此相反,大家不妨考虑下面这样一小段C代码:
char notpython[50] = "This isn't Python.";
在我们回到Python话题前,先让我们深入了解一下上面这段C代码。
在此,char是一个类型标识符,它告诉你后面的所有内容都与字符串有关。“notpython”是我给这个字符串起的名字。[50]告诉你,C将为此保留50个字符的内存空间。不过,在本例中,我可以得到19个字符——每个字符对应一个空间位置,最后加上一个空字符\0。最后,用分号简洁地结束。
这种显式声明在C语言中是必须的。如果省略显式声明,编译器将出现相应错误提示!这种做事方式一开始似乎既愚蠢又乏味,但却是非常值得的。
当你在两周或两年后阅读C代码时,例如你偶然发现了一个你不知道的变量,你只需检查一下它的声明。当然,如果你给它起了一个有意义的名字,那么这会提供给你一个强有力的线索:有关它是什么,它在做什么,它需要在哪里,等等。
现在,让我们将其与Python进行比较。
在Python中,你几乎可以一边编写代码一边声明新的变量。不过,如果你没有给它起一个有意义的名字或者至少留下一条注释的话,那么过些时候再阅读此代码时你会觉得问题变得一团糟。
在Python中,除了深入分析有关源代码之外,你根本无法理解变量在做什么。
但是如果你在一个变量中有一个输入错误,那么就可能破坏你的整个代码,因为不存在像C中那样的保护声明。
如果你在做更小的项目,比如说几千行的代码,那就没关系;或者如果你的项目不是很复杂时,也会存在什么问题。但是当有了更大的项目时,就会遇到极大麻烦。
是的,你可以在Python中进行显式变量声明。但事实上,只有最勤奋的程序员才能做到这一点。更实际的情形是:当编译器没有“抱怨”时,许多人会完全忘记这些额外的代码行。
编写Python代码是很快速的事情,而且对于小型和简单的项目来说,阅读Python也很容易。
然而,在阅读和维护大型Python项目时,例如当寻找描述性变量名和注释所有代码方面,你最好是世界级程序员,否则你就完蛋了。
模块归属问题
如果你认为事情不会变得更糟,那你就错了。
变量在代码中从何处开始“存在”的问题不仅仅源于隐式声明。
变量也可能来自其他模块。它们的形式通常像my_module.my_variable()。如果你对这样一个变量感到困惑,那么当你检查了它在主文件中的其他位置时,你还没有彻底解决问题。
你还需要检查是否存在一行代码,像下面其中之一:
import my_module
from another_module import my_module
在第二行代码中,你告诉编译器你需要从一个包含更多内容的模块中得到哪个函数或变量。
这是很烦人的事情,因为你在PyPI(https://pypi.org/)上能找到更多的模块,而且你还可以在计算机上导入任何其他Python文件。所以,快速搜索函数或变量并不总是有效。
但有些情况下,问题可能会变得更加糟糕。例如,有的模块可能依赖于其他模块。如果你运气不好,例如你导入了模块A、B和C,但它们又依赖于模块E、F、G和H,而模块E、F、G和H又依赖于I、J和K。突然之间,你需要管理的模块不是三个,而变成了十个!
更糟糕的是,有时这种依赖并不是一棵简单的依赖树。假设B和C也依赖于M和N,J也依赖于M,C和H也依赖于Q……不必再强调,你应该明白了一切。
这就像一个迷宫一样。Python程序员把此称为“依赖地狱”——的确是真实存在的。
而循环依赖算是迷宫中“最丑陋的野兽”。例如,如果模块A依赖于模块B,但模块B也使用模块A的一部分时——你的麻烦大了!
对于小项目来说,这没什么大不了的。但对于大的项目……你可能会一头扎进“原始丛林”。
大规模的依赖冲突
我还没发泄完我对模块的咆哮:不仅是模块本身,还有它们的版本方面也存在问题。
原则上,Python拥有如此活跃的用户群,并且许多模块都定期更新,这是非常棒的。然而,只有一个问题:并非所有版本的模块都与其他模块兼容。
例如,假设你正在使用模块A和B。这两个模块都依赖于模块C。但是A在3.2或更高版本中需要C,而B在2.9或更低版本中需要C。你不在乎C,你只想要A和B。
世界上没有任何工具能帮你解决这场冲突。如果幸运的话,你会发现一个补丁,它是由与你遇到同样问题的人编写的。如果你没那么幸运,你就得自己写补丁了。
或者使用不同的软件包。或者完全重写其中一个包A或B,然后在需要错误版本的C的任何地方找到解决方法。
总之,在任何情况下,你都需要花费额外的时间。
这是一片“代码丛林”,你需要耐心和一些工具来驾驭它才行。
撇开依赖冲突不谈,还有一些不错的工具。例如,有一个名为pip的工具,可以使安装软件包变得容易。借助一个简单的文本文件“requirements.txt”,你可以指定要使用的软件包和版本,而不是弄乱文件头部。虚拟环境将所有软件包放在一个地方,并与主要的Python安装分开。
对于更大、更混乱的项目,还可以借助conda、YAML文件等途径来解决上述问题。
但无论如何,你都需要学习如何使用每种工具。而且,你需要花最少的时间来处理这些问题。
不同机器上的Python问题
与上述“依赖地狱”世界联系在一起的是另一个令人不安的话题。
即使你已经解决了机器上的所有依赖性问题,并且Python像一匹新生的马一样平稳运行,也不能保证它会在其他人的机器上正常运行。
新生的马会跑吗?我不知道,但我似乎在努力让自己在生物学方面比以往任何时候都更有学识。闲言少叙,让我们回到Python话题。
像pip、文本文件requirements.txt和虚拟环境这样的工具将有助于你在一定程度上克服依赖地狱问题。但是,这种便利仅限于本地情形。
在每台新机器上,你都需要检查并可能重新安装每个需求及其版本。
唯一真正便携的解决方案是借助Jupyter notebooks。在这里你可以用任何你喜欢的版本写东西。在Jupyter中,所有内容都通过在线服务器运行,因此你可以将这些文件发送给任何人,他们的确能够“开箱即用”。
不过,这种方法也存在一个明显的缺点:Jupyter notebooks只有图形界面,而有时候仅仅使用图形界面,很难处理包含许多相互关联文件的大型项目。
也许这就是为什么我从未在Jupyter notebooks上看到过大型项目,尽管它们确实存在。
相比之下,其他一些计算机语言只使用虚拟机,这类问题即可迎刃而解。
除Pip之外的工具
假设你已经通过使用Jython或PyPy或类似的解决方案将项目移植到不同的机器上。所有这些操作都比虚拟机稍显笨拙。但是,至少它们能够正常工作了!
如果你正在组建一个大项目,你可能会集成C包、Fortran包等等。这样做有很多好处:Python中可能不存在C包,而且通常速度更快。出于历史原因,科学计算类软件包通常只存在于Fortran中。
实际上,您将不得不使用诸如gcc、gfortran之类的编译器,或许还有其他更多的编译器。
这太麻烦了!在Python代码中集成C模块的文档超过4500字——是本文的两倍!Fortran相关的文档也没有那么短。
用C语言构建整个项目最初可能会耗费更长时间;但是,你可以防止出现必须处理多个编译器和接口的情况。
C是如此古老的语言,以致几乎任何东西都有对应的C语言包装版本,甚至包括用户友好的机器学习软件包。
使用全局解释器锁锁定性能
全局解释器锁(GIL)从Python诞生的第一天起就一直存在,它能够使终端用户的内存管理变得非常简单。
至少在较小的项目中,开发人员在使用Python时根本不需要考虑计算机内存问题。我们不妨将其与C语言比较一下:在C语言中,需要为每个变量保留一些内存。
基本上,GIL能够自动计算一个变量在代码的每个部分中被引用的次数。如果不再需要该变量,GIL就会释放它所占用的内存空间。
在小型项目中,GIL有助于提高性能,因为不必要的内存空间会被及时清除掉。
但在更大的项目中有一个问题:GIL不喜欢多线程!
当多个指令线程在同一进程资源上独立运行时,这是一种高性能执行程序的方法。机器学习模型很适合这样训练。
然而,只有一个小问题:GIL一次只能在一个线程上工作。
于是,会出现这样的情况:例如变量A在线程1上执行,而线程2已经用完了变量A,那么它的内存可能最终会被删除。这种情况取决于GIL当时在哪里运行。
因此,这可能会导致非常奇怪的错误,正如你所想象的……
这方面有一些变通办法,但都不是很漂亮。另一种选择方案是借助于多进程来完成计算。但在没有GIL的语言中,它通常不会像多线程那样快。
并发和并行计算依然让人头疼
我们已经看到了并发的一个缺点。在进行多线程处理时,全局解释器锁会降低速度,或者导致奇怪的错误。
同样的缺点也存在于Python的协程中。
线程和协程之间有一些细微的区别,但最重要的一点是:协程一次执行一个任务,而线程可以同时执行多个任务,二者都是并发实现的。
当你有需要大量等待的任务时,协程非常有用,例如当你正在阅读网站数据并等待服务器响应时的情形。协程不会让计算机无所事事,而是给它分配另一项任务。
另一方面,当你有几个任务很耗时但不需要太多CPU消耗,也不需要太多等待时,线程是很有用的。流式数据就是一个例子。
如果你有一个CPU密集型任务,并且你想充分利用你的硬件,你可能想尝试一下使用并行计算。
多进程会成为你最好的朋友——它基本上是告诉计算机使用多个内核进行计算,从而节省大量时间。
不过,线程、协同程序和多进程这三种技术都面临类似的问题。它们在Python中实现起来并不难。但是代码看起来很笨重,很难阅读,尤其是对于初学者来说。
相比来说,Clojure、Go和Haskell等语言在并发性和并行性方面性能要好得多。但是,如果你不是在处理缓慢或密集的进程,那么你根本不必考虑这样的方案。但如果当你遇到这样的问题时,那么你可能需要考虑选择语言的问题了。
用哪一种语言代替Python?
Python并不全是邪恶的,但它的确也存在缺点。
因此,如果你想要明确说明的变量和开发良好的包,而这些包不会让你轻易陷入依赖地狱,那么你可以选择C语言。此外,如果你想要任何机器都可以移植的东西,那么Java、Clojure或Scala都是不错的选择。因为它们在虚拟机上运行,所以你不会遇到与Python相同的问题。
还有,如果你想执行大而慢的任务,你可能想试试Go或Haskell。尽管一开始,它们比Python更难学习,但你投入的时间是有回报的。特别是,你总可以把多种语言组合起来使用。
Python非常适合快速编写脚本、初稿,甚至适合中等规模的项目。我认识的许多开发人员都用Python编写初稿和测试运行,然后再使用C、Go或Clojure重写重要的部分。这使代码执行更快,而且你仍然可以享受Python提供的优势。
在大型项目中,Python并不是被禁止使用,只不过这可能不是唯一使用的语言而已。你可以通过Python像胶水一样将C、Go或Clojure中的代码拼接在一起。此外,如果你已经达到了自己构建的阶段,那么请记住:没有任何一种语言是绝对优秀的!
总之,尽管Python有缺点,但它的确很酷,而且也很方便。通过使用Python集成其他语言中的代码,你总是可以绕过有关难点并最终解决问题。
译者介绍
朱先忠,51CTO社区编辑,51CTO专家博客、讲师,潍坊一所高校计算机教师,自由编程界老兵一枚。早期专注各种微软技术(编著成ASP.NET AJX、Cocos 2d-X相关三本技术图书),近十多年投身于开源世界(熟悉流行全栈Web开发技术),了解基于OneNet/AliOS+Arduino/ESP32/树莓派等物联网开发技术Scala+Hadoop+Spark+Flink等大数据开发技术。
原文标题:Don’t use Python… if you’re starting a big project,作者:Ari Joury
链接:https://thenextweb.com/news/dont-use-python-for-big-projects