關於 Python,一些不得不吐槽的「迷惑行為」

摘要:隨著大資料與人工智慧時代的到來,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 8add_five(3,4) # returns 12

      之所以會出現這種現象,是因為表達式b=0定義了b是一個整數,而整數是不可變的。再看看下面這個例子:

        def add_element(list=[]):list.append("foo")return listadd_element() # returns ["foo"], as expected

        發現問題了嗎?再執行一次會怎麼樣?

          add_element() # returns ["foo", "foo"]! wtf!

          因為這裡的list已經存在,即[“foo”],而Python會繼續向這個列表添加新東西。這是因為列表與整數不同,是可變類型。

          我不禁想起一句話:「瘋子就是不斷重複同一件事,卻期待不同的結果。」(經考證,這句話不是愛因斯坦說的)。我想說,Python + 可選參數 + 可變對象 = 瘋子。

          類變數並不安全

          類變數並不安全

          如果你認為上述問題僅限於可變對象作為可選參數的時候,那你就大錯特錯了。

          相信你也使用Python編寫物件導向的程式碼,在Python程式碼中類無處不在。而類最實用的特性之一便是:繼承。

          簡單來說,如果父類具有某些屬性,子類就可以繼承這些屬性。如下所示:

            class parent(object):x = 1class firstchild(parent):passclass secondchild(parent):passprint(parent.x, firstchild.x, secondchild.x) # returns 1 1 1

            注意,這段程式碼寫得並不好,不要複製到實際的項目中。關鍵在於,子類繼承了 x = 1,因此我們可以獲取子類的這個屬性,得到的結果與父類相同。

            如果我們修改某個子類的x屬性,那麼理應說變化的只有這個子類。就好像孩子染髮不可能改變父母親或兄弟姐妹的髮色。程式碼如下:

              firstchild.x = 2print(parent.x, firstchild.x, secondchild.x) # returns 1 2 1

              如果這時父母染髮,孩子的髮色會變嗎?不會變,對不對?

                parent.x = 3print(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 = 2return basenumber*numberbasenumber## Oh no! This is the error:# Traceback (most recent call last):# File "", line 1, in # NameError: name 'basenumber' is not defined

                  這部分完全符合直覺,我栽跟頭也不是因為這部分程式碼。

                  但是反過來呢?我的意思是,如果我在函數外部定義一個變數,然後在函數內部引用它呢?

                    x = 2def add_5():x = x + 5print(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 = 2def add_5():global xx = x + 5print(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語言的學習並沒有那麼簡單,請多加小心。

                          相關文章

                          Python 雖已登峰,但尚未造極!

                          Python 雖已登峰,但尚未造極!

                          本文來自 CSDN 策劃的《2022 年技術年度盤點》欄目。本欄目將圍繞程式語言、開源、雲端運算、人工智慧、架構服務、資料庫、晶片、開發工具...

                          Python 與 JavaScript 做比較公平嗎?

                          Python 與 JavaScript 做比較公平嗎?

                          在討論應該使用 Python 還是 JavaScript 構建項目時,一般我們都不會說只使用一種程式語言來構建所有的元件。 在現代軟體開發中...