譯者 | 朱先忠
審校 | 千山
本文將通過對變量聲明、模塊歸屬、依賴沖突、包管理、全局解釋器鎖以及并發和并行計算等方面的分析,向你解釋為什么不推薦在開發大型項目時使用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