這個話題已經在 Hacker News 上引發了熱烈的討論(評論 400+),感興趣的讀者可以去圍觀或參與一下。
Hacker News 討論:https://news.ycombinator.com/item?id=18706174
1. 版本
如果要安裝一個預設的 Linux 作業系統,那你很有可能需要安裝多個版本的 Python:Python2、Python3 甚至是 3.5、3.7。原因在於:Python3 無法與 Python2 完全相容。甚至一些用小數表示的版本(如 3.5、3.7)也明顯缺乏向後的相容性。
我完全贊成往程式語言中新增新的功能,我甚至不介意淘汰一些舊的版本。但 Python 卻要分開安裝。我的 Python 3.5 程式碼不適用於 Python 3.7 安裝版本,除非我特意將其匯入 3.7。很多 Linux 開發者都覺得匯出太麻煩,因此安裝 Ubuntu 的時候會一併安裝 Python2 和 Python3——因為有的核心功能需要前者,而有的需要後者。
向後相容性的缺乏和各自為政的版本通常會為其敲響喪鐘。Commodore 創造了第一批家用電腦(比 IBM PC 和蘋果都要早很多)。但 Commodore PET 不能與後續的 Commodore CBM 相容。CBM 又不與 VIC-20、Commodore-64、Amiga 等相容。因此,你要麼選擇花很多時間將程式碼從一個平臺導到另一個,要麼選擇放棄這個平臺。(Commodore 今天何在?早被使用者拋棄涼涼了……)
類似地,Perl 也火過一陣。但 Perl3 與 Perl2 的很多程式碼也不相容。社群罵聲一片,於是一些好的程式碼導了出來,其他的則被拋棄了。Perl4 也是如此。等 Perl5 出來的時候,人們乾脆改用另一種更穩定的程式語言。如今,只有一小部分人還在頻繁使用 Perl 來維持之前的專案。但已經沒有人用 Perl 建立新的大專案了。
同理,Python 的每個版本也都存在穀倉效應。之前的版本還要留著,最終造成手裡有一堆舊的無用 Python 程式碼,因為大家都不想花時間將其移到最新版上。據我所知,沒有人為 Python2 建立新程式碼了,但我們還留著它,因為沒有人想將所需程式碼移到 Python3.x 中。Python 2.7、3.5、3.6、3.7 的文件都還在 Python 官網上積極維護著,因為他們無法下決心棄用之前的程式碼。Python 就像一種殭屍程式語言——已經死掉的部分還在以行屍走肉的方式存在著。
2. 安裝
很多軟體包都可以幫你輕鬆地執行 apt、yum、rpm 或其他一些安裝庫,並獲得最新版本的程式碼。但 Python 並非如此。如果用「apt-get install python」安裝,你都不知道自己安的是哪個版本,它可能也無法與你所需的所有程式碼相容。
因此,你要安裝你需要的那版 Python。我的其中一個專案用到 Python,但必須用 Python3.5。所以最後,我的電腦安裝了 Python2、Python2.6、Python3 及 Python3.5。其中兩個來自作業系統,一個用於專案,另外一個服務於出於其他原因安裝的無關軟體。雖然都是 Python,但此 Python 非彼 Python。
如果你想安裝 Python 包,你應該使用「pip」(Pip Installs Packages)。但由於系統上有一堆 Python,你要注意使用正確版本的 pip。否則,「pip」可能執行「pip2」,而不是你需要的「pip3.7」。(如果名稱不存在,你需要為 pip3.7 指定明確的真實路徑)
一位隊友建議我配置自己的環境,這樣的話每種軟體都可以使用 Python3.5 的 base 環境。在我需要用 Python3.6 開展另一個專案之前,這種做法是非常行得通的,但是需要 Python 3.6 就得建立另外一個環境。兩個專案,兩版 Python,一點都不會混,真的(用生命在假笑)。
pip 安裝程式將檔案放置在使用者的本地目錄。安裝系統級的庫時不用 pip。Gawd 不允許你在執行「sudo pip」時出錯,因為那會毀了你的整個電腦!執行 sudo 可能會使一些軟體包在系統級別安裝,有些是為錯誤版本的 Python 安裝的,而你的主目錄中的一些檔案可能最終歸 root 所有,因此未來的非 sudo pip 安裝可能會因許可權問題而失敗。不要這樣做。
這些 pip 模組由誰來維護呢?當然是社群。也就是說,沒有明確的所有者,也沒有強制性的來源鏈或責任鏈。今年早些時候,PyPI 的一個版本中發現了一個竊取 SSH 憑證的後門。這也是意料之中。(出於同樣的原因,我不用 Node.js 和 npm;我不信任他們的社群專案。)
3. 句法
我非常主張程式碼的可讀性要強。乍一看,Python 的可讀性似乎不錯。但當你開始建立大型程式碼庫的時候你就不會這麼想了。
大多數程式語言使用某種符號來標識範圍——函式的開始和結束位置、條件語句中包含的操作、變數的定義範圍等。C 語言、Java、JavaScript、Perl 和 PHP 都用 {...} 來定義範圍,Lisp 使用 (...)。Python 呢?它用空格!如果你要定義複雜程式碼的範圍,你可以縮排接下來的幾行程式碼,縮排結束時,該範圍也截止。
Python 手冊說,你可以用任意數量的空格或製表符來定義範圍。但是,每次縮排最好使用四個空格!如果你想縮排兩次進行巢狀,使用八個空格!Python 社群已經對此進行標準化,即使 Python 手冊中並沒有明文規定。這個社群就喜歡用四個空格。所以,除非你不打算將自己的程式碼向任何人展示,否則的話每次縮排最好用四個空格。
我第一次看到 Python 程式碼時,覺得用縮排來定義範圍還挺好的,但這麼做有一個巨大的缺陷。你可以進行深度巢狀,但這麼做使得每一行都會很長,導致不得不在文字編輯器中換行。較長的函式和條件語句可能會使開始和結束範圍很難匹配。而且當你不小心把三個空格當成四個空格,還容易出現計算錯誤,進而花幾個小時來除錯和追蹤。
對於其它語言,我已經養成了除錯程式碼不帶任何縮排的習慣。這樣,我可以快速瀏覽程式碼,然後輕鬆地識別和刪除除錯程式碼。但 Python 呢?任何沒有適當縮排的程式碼都會產生縮排錯誤。
4. includes
大部分程式語言都有辦法匯入其它程式碼塊。比如,C 語言用「#include」,PHP 語言可以用「include、include_once、require、require」。而 Python 用的是「import」。
Python 可以匯入整個模組、模組的一部分或模組中的特定函式。C 語言?你可以檢視「/usr/include/」。Python 的話,最好用「python -v」列出所有路徑,然後從列表中搜尋每個目錄和子目錄中的每個檔案。我有些朋友很喜歡 Python,但我看到他們想匯入東西時,總得瀏覽標準模組。
匯入功能還允許使用者重新命名匯入的程式碼。它們基本上定義了一個自定義的名稱空間。乍一看,你會覺得挺不錯的,但這最終會影響可讀性和長期支援。重新命名對於較小的指令碼來說還是不錯的,但對於長期專案來說真的不適用。那些使用 1-2 個字母作為名稱空間(比如「import numpy as n」),而且還不按約定俗成的方式來命名的,簡直應該拉出去槍斃!
這還不是最糟糕的。大部分程式語言 include 程式碼的時候就只是匯入程式碼而已。如果有一個帶有建構函式的全域性物件,有些語言,如物件導向的 C++可能會執行程式碼。類似地,有些 PHP 程式碼可能會定義全域性變數,所以匯入可以執行程式碼——但這種做法通常被認為很糟糕。相比之下,很多 Python 模組包含在匯入期間執行的初始化函式。你不知道在執行的是什麼,它要幹什麼,你甚至可能不會注意到。除非存在名稱空間衝突,如果這樣就好玩了,你得花很多時間來尋找原因。
5. 命名法
在其它語言中,陣列(array)直接稱之為'arrays',但是在 Python 中,它們被稱為 'lists'。關聯陣列在某些地方被稱為 'hash' (Perl),但是 Python 將其稱為「字典」(dictionary)。Python 似乎完全按照自己的節奏來,不使用電腦科學和資訊科學領域的常見術語。
此外,Python 庫的命名也有問題。PyPy、PyPi、NumPy、SciPy、SymPy、PyGtk、Pyglet、PyGame……(前兩個庫的發音一樣,但是它們的功能完全不同)。我理解「py」表示 Python,但是它們就不能統一出現在前面或後面嗎?
一些常見庫放棄了類似雙關語的「Py」命名約定,包括 matplotlib、nose、Pillow和 SQLAlchemy。雖然有一些命名可能暗示其目的(如 SQLAlchemy 包含 SQL,所以它可能是一個 SQL 介面),但是其它的可能只是隨機的單詞。如果你不知道「BeautifulSoup」這個庫是幹什麼的,那麼你能從命名看出來它是一個 HTML/XML 解析器嗎?不過,BeautifulSoup 有很完善的文件且易於使用,如果每一個 Python 模組都這樣,我也就不抱怨了,但是大多數 Python 庫的文件非常爛。
總的來說,我認為 Python 是一個具有不一致命名約定的函式庫集合。我經常抱怨開源專案的命名非常可怕。除非你知道這些專案在幹什麼,否則你從命名本身中什麼都看不出來。除非你知道在尋找什麼樣的庫,不然只能透過別人偶然提及的名字或偶然的機會發現一些庫。大多數 Python 庫加重了這種現象,也加重了 Python 的負面體驗。
6. 奇怪的操作
每種語言都有自己比較奇特的操作。C 語言中使用 & 和 * 獲取地址空間和值的命名法非常奇怪。C 語言中還有用 ++ 和—實現 increment/decrement 的捷徑。Bash 語言中,在引用特定字元(如用於正規表示式的圓括號和句號)時需要一直考慮「什麼時候使用轉義符 (\)」。JavaScript 相容性有問題(並非每個瀏覽器都支援所有有用的功能)。但 Python 的奇怪操作比我見過的其他語言都多。如:
在 C 語言中,雙引號裡的是字串,單引號裡的是字元。
在 PHP 和 Bash 中,兩種引號都能包含字串。但是,雙引號裡的字串可以嵌入變數。相比之下,單引號的字串是文字;任何嵌入的類似變數的名稱都不可擴充套件。
在 JavaScript 中,單引號和雙引號沒什麼區別。
在 Python 中,單引號和雙引號也沒有什麼區別。但是,如果你想讓字串跨行,就得用三重引號,如"""string""" 或 '''string'''。如果你想用二進位制,那你需要優先選擇帶有 b(b'binary')或 r(r'raw')的字串。有時你要用 str(string) 把字串轉換為字串,或使用 string.encode('utf-8') 將其轉換為 utf8 格式。
如果你一開始認為 PHP 和 JavaScript 中的=、==、===有點奇怪,那等你用 Python 中的引號時可能不會這麼想了。
7. 透過物件 Reference 傳遞
大多數程式語言的函式引數傳遞是傳值。如果函式改變了值,結果不會傳遞迴呼叫程式碼。但正如我解釋過的,Python 偏偏要有所不同。Python 預設使用 pass-by-object-reference 來傳遞函式引數。這意味著改變源變數可能最終會改變值。
這是面向程式、函式和物件的程式語言之間的最大區別。如果每個變數都由物件引用來傳遞,並且變數的任何變化都會改變所有的引用,那你可能使用的都是全域性物件。透過不同的命名呼叫相同的物件不會改變物件,所以實際上它就是全域性的。此外,正如 C 的程式設計師早就學到的,全域性變數太噁心了,別用。
在 Python 中,你必須透過值來傳遞變數,例如「a=b」只是給相同的物件空間分配了另一個命名,但並沒有複製 b 的值給 a。如果你真的想要複製 b 的值,你需要使用一個 copy 函式,通常是「a=b.copy()"的形式。然而,注意我說的是「通常」。不是所有資料型別都有一個「copy」原型,或者 copy 函式可能是不完整的。在這種情況下,你可以使用單獨的「copy」庫:"a=copy.deepcopy(b)"。
8. 本地命名
用所用的庫或函式的名字來命名程式是常見的程式設計技巧。例如,如果我用一個叫做「libscreencapture.so」的 C 庫來測試一個截圖程式,我會將該程式命名為「screencapture.c」並編譯為「screencapture.exe」。
gcc -o screencapture.exe screencapture.c -lscreencapture
在 C、Java、JavaScript、Perl、PHP 等語言中,這通常很有效,因為這些語言可以輕易地辨別本地程式和資源庫,它們有不同的路徑。但 Python 呢?還是算了吧,千萬別這樣做。為什麼?Python 會假定你首先要匯入原生程式碼。如果我有一個名為「screencapture.py」的程式使用了「import screencapture」,那麼它將匯入自己而不是系統庫。至少,你應該呼叫本地程式「myscreencapture.py」吧。
並非一無是處
Python 是一門非常流行的程式語言,有很多粉絲。甚至我的很多朋友都很喜歡 Python。多年來,我和他們討論過這些問題,每次他們都點頭表示同意。他們並不反對 Python 存在這些問題,只是認為這不足以澆滅他們對這種語言的熱情。
我的朋友經常提到那些非常酷的 Python 庫。我同意一些庫非常有用。例如,BeautifulSoup 是我用過最好的 HTML 解析器之一,NumPy 使多維陣列和複雜的數學更容易實現,而 TensorFlow 對於機器學習非常有用。但是,我不會因為喜歡 TensorFlow 或 SciPy 而用 Python 建立單片程式。我不打算為了這些「蠅頭小利」而放棄可讀性和可維護性,這不值得。
通常當我寫一篇關於某個主題的批評時,我也會嘗試寫一些積極的東西。但我沒辦法列出關於 Python 的好的方面,因為我真的認為 Python 很糟糕。
原文地址:https://www.hackerfactor.com/blog/index.php?/archives/825-8-Reasons-Python-Sucks.html