摘要:隨著大資料與人工智慧時代的到來,Python 近年來頗受程式設計師喜愛,在 TIOBE 程式語言排行榜中也穩居第一。但這並不說明 Python 毫無缺點,本文作者就將盤點一些 Python 的「迷惑性為」。
原文連結:https://medium.com/geekculture/why-python-still-is-a-mess-1f7bf5bca281
作者 | Ari Joury
譯者 | 彎月
長期以來,Python一直自詡是最適合新手程式設計師的語言之一。話雖沒錯,但這並不意味著程式設計新手不會對Python的一些行為感到困惑。
舉個例子,動態類型。你無需單獨編寫一行程式碼來定義變數的類型,Python能夠自行分辨,乍一看之下,這似乎很神奇。感覺這樣程式設計速度更快。
然而,就因為少了一行變數定義,整個項目在運行結束之前就有可能崩潰。
說句公道話,許多其他程式語言也使用動態類型。但對於Python而言,這只是一系列噩夢的開始。


隱式的變數聲明會影響閱讀程式碼
幾年前,我想在同事編寫的一個軟體的基礎之上,進行二次開發。我知道該軟體的基本思想,我的同事甚至寫了一篇論文作為該軟體的文件。
但是,我仍然需要閱讀數千行 Python 程式碼,才能搞清楚各個部分在幹什麼,以及我可以將新功能放到哪裡。然而,就在這個過程中,我遇到了很大的問題……
縱觀整個程式碼庫,變數聲明到處都是。為了搞清楚每個變數的用途,我不得不搜尋整個檔案,甚至是整個項目。
此外,還有各種各樣的複雜情況,比如函數的某個參數的名字和調用該函數時使用的變數完全不同,或者一個變數與某個類緊密結合,而該類又和另一個類中的某個變數交織在一起……諸如此類的事情層出不窮。
很多人都有類似的感覺,有人就曾表示顯式變數聲明優於隱式(參考連結:https://peps.python.org/pep-0020/)。但是,在Python中隱式變數聲明比比皆是,尤其是在大型項目中。

無處不在的可變類型,甚至在函數中
在 Python 中,定義函數的時候可以指定可選參數,即不需要明確指定的參數。如下所示:
def add_five(a, b=0):
return a + b + 5
通過這個簡單的示例可以看出,在調用函數時,無論指定一個參數還是兩個參數都可以:
add_five(3) # returns 8
add_five(3,4) # returns 12
之所以會出現這種現象,是因為表達式b=0定義了b是一個整數,而整數是不可變的。再看看下面這個例子:
def add_element(list=[]):
list.append("foo")
return list
add_element() # returns ["foo"], as expected
發現問題了嗎?再執行一次會怎麼樣?
add_element() # returns ["foo", "foo"]! wtf!
因為這裡的list已經存在,即[“foo”],而Python會繼續向這個列表添加新東西。這是因為列表與整數不同,是可變類型。
我不禁想起一句話:「瘋子就是不斷重複同一件事,卻期待不同的結果。」(經考證,這句話不是愛因斯坦說的)。我想說,Python + 可選參數 + 可變對象 = 瘋子。

類變數並不安全
如果你認為上述問題僅限於可變對象作為可選參數的時候,那你就大錯特錯了。
相信你也使用Python編寫物件導向的程式碼,在Python程式碼中類無處不在。而類最實用的特性之一便是:繼承。
簡單來說,如果父類具有某些屬性,子類就可以繼承這些屬性。如下所示:
class parent(object):
x = 1
class firstchild(parent):
pass
class secondchild(parent):
pass
print(parent.x, firstchild.x, secondchild.x) # returns 1 1 1
注意,這段程式碼寫得並不好,不要複製到實際的項目中。關鍵在於,子類繼承了 x = 1,因此我們可以獲取子類的這個屬性,得到的結果與父類相同。
如果我們修改某個子類的x屬性,那麼理應說變化的只有這個子類。就好像孩子染髮不可能改變父母親或兄弟姐妹的髮色。程式碼如下:
firstchild.x = 2
print(parent.x, firstchild.x, secondchild.x) # returns 1 2 1
如果這時父母染髮,孩子的髮色會變嗎?不會變,對不對?
parent.x = 3
print(parent.x, firstchild.x, secondchild.x) # returns 3 2 3
出現這個結果是因為Python的方法解析順序(http://python-history.blogspot.com/2010/06/method-resolution-order.html)。簡單來說,只要沒有另行說明,子類就會繼承父類擁有的一切。也就是說,在Python的世界裡,如果你不提前抗議,那麼你媽媽在染頭髮的時候,會順帶連你的頭髮一起染了。

反方向的作用域
我個人已經因為這個問題多次栽跟頭。
在Python中,函數內部定義的變數無法在函數外部使用,這是因為超出了作用域:
def myfunction(number):
basenumber = 2
return basenumber*number
basenumber
## Oh no! This is the error:
# Traceback (most recent call last):
# File "", line 1, in
# NameError: name 'basenumber' is not defined
這部分完全符合直覺,我栽跟頭也不是因為這部分程式碼。
但是反過來呢?我的意思是,如果我在函數外部定義一個變數,然後在函數內部引用它呢?
x = 2
def add_5():
x = x + 5
print(x)
add_5()
## Oh dear...
# Traceback (most recent call last):
# File "", line 1, in
# File "", line 2, in add_y
# UnboundLocalError: local variable 'x' referenced before assignment
這就很奇怪了,不是嗎?我們生活在一個有樹的世界裡,雖然平時我們住在房子裡,但肯定也知道樹長什麼樣子,對不對?(樹是 x,房子是 add_5(),我們是 5……)
有好多次,我在某個類中調用另一個類中定義的函數,就遇到了錯誤。我花了很長一段時間才找到問題的根源。
其背後的基本思想是,函數內部的 x 與外部的 x 是不同的,所以你不能在外部調用它。
幸運的是,這個問題有一個簡單的解決方案,即在 x 之前加一個global,讓x變成全局變數!
x = 2
def add_5():
global x
x = x + 5
print(x)
add_5() # works!
所以說,如果你認為作用域的目的僅僅是保護函數內部的變數不被外部干擾,那就大錯特錯了。在Python中,局部作用域也無法訪問外部。

在迭代的過程中修改列表
請看如下程式碼:
mynumbers = [x for x in range(10)]
# this is [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
for x in range(len(mynumbers)):
if mynumbers[x]%3 == 0:
mynumbers.remove(mynumbers[x])
## Ew!
# Traceback (most recent call last):
# File "", line 2, in
# IndexError: list index out of range
這個循環出錯,是因為循環在迭代的過程中不斷刪除列表中的元素。因此,列表不斷縮短,循環不可能到達第10個元素,因為它不存在了!
有一種解決方法是,為你想刪除的所有元素統一分配一個值,然後在循環結束後刪除它們。
此外,似乎還有一種更好的解決方式:
mynumbers = [x for x in range(10) if x%3 != 0]
# that's what we wanted! [1, 2, 4, 5, 7, 8]
只需要一行程式碼!
請注意,在上面的示例中,我們使用了 Python 的列表推導式來調用列表。
列表推導式指的是方括號([])中的表達式,一般都是循環的縮寫形式。列表推導式通常比常規循環更快,因此非常適合處理大型資料集。
在這個示例中,我們添加了一個 if 子句來告訴列表推導式:不應包含可被 3 整除的數字。
這個問題與前面的幾個不同,我不認為這是Python的迷惑行為,相反我認為這種處理很聰明,儘管初學者理解起來會有些困難。

總結
實際上,我們對Python的不滿不止是編寫程式碼的痛苦,別忘了,以前Python的執行速度非常慢,比大多數其他語言慢 2~10 倍。
現在情況已經好很多了。例如,現在Numpy 包能夠非常快速地處理列表、矩陣等。
Python的多執行緒處理也變得更加容易了。你可以使用計算機上的多個核心,我曾在 20 個核心上運行進程,為我節省了數週的計算時間。
此外,在過去幾年中,隨著機器學習的蓬勃發展,Python 也表現出了進一步的發展空間。Pytorch 和 Tensorflow 等包的出現推動了Python的採用,而其他語言也正在努力中。
雖然,多年來Python在不斷進步,但這並不能保證Python未來的發展會一帆風順。Python語言的學習並沒有那麼簡單,請多加小心。