攜手創作,共同成長!這是我參與「掘金日新計劃 · 8 月更文挑戰」的第5天,點擊查看活動詳情
直到出現混亂前,它對初學者都是友好的
無論是行業領袖還是學術研究人員,都吹捧 Python 是編程新手最好的語言之一.他們沒有錯,但這並不意味著 Python 不會讓編程新手們感到困惑.
以動態類型為例,看起來令人驚訝,Python 可以自己計算出變量可能獲得的值類型,而且不需要浪費一行代碼來聲明類型,這樣更快.
一開始是這樣的,然後你在某一行搞砸了,繼而導致你的整個項目在運行之前就崩潰了.
公平的說,其它語言許多都使用動態類型,但對於 Python 來說,這僅僅是一個糟糕清單的開始.
幾年前,當我開始攻讀博士學位時,我想進一步開發一個由同事編寫的現有軟件,我了解它的基本原理,甚至我的同事寫了一篇關於它的文檔.
但我仍然需要閱讀成千上萬行的 Python 代碼,以確保我知道每部分代碼做了什麼,從而可以可以把我想到的新功能放在那裡,這就是問題所在......
整個代碼中到處都是未被聲明的變量,為了理解每個變量的用途,我必須在整個文件中搜索它,更常見的是在整個項目中搜索它.
還有一個復雜的情況,變量通常在函數內部被調用,但是當函數被調用時,又會有其他的東西被調用……還有一個情況,一個變量可以與一個類交織在一起,這個類與另一個類的另一個變量相關聯,而另一個類又影響著一個完全不同的類……你明白了吧.
有這種經歷的不止我一個,Zen of Python 明確表示,顯式要比隱式好,但是在 Python 中做隱式變量太容易了,特別是在大型項目中,shit*t 很快就受到了歡迎.
在 Python 中,你可以通過提供默認值來定義具有可選參數的函數,不需要在之後顯式聲明的參數,像這樣:
def add_five(a, b=0):
return a + b + 5
復制代碼
我知道這是個鬧著玩的例子,但是你現在可以用一個或者兩個參數來調用這個函數,它還是可以工作的:
add_five(3) # 返回 8
add_five(3,4) # 返回 12
復制代碼
它能運行,是因為表達式 b = 0 將 b 定義為一個整數,而整數是不可變的:
def add_element(list=[]):
list.append("foo")
return list
add_element() # 返回 ["foo"],符合預期
復制代碼
到目前為止,一切正常,但是如果再次執行它會發生什麼?
add_element() # returns ["foo", "foo"]! wtf!
復制代碼
因為參數是一個列表,即列表 ["foo"] 已經存在,Python 只是把它的東西附加到那個列表中,這樣做是因為列表與整數不同,列表是可變的類型.
常言道: “瘋狂就是一再重復相同的事情,卻期望得到不同的結果”(這句話常常被誤認為是阿爾伯特 · 愛因斯坦說的).也可以說,Python 加上可選參數,加上可變對象簡直是瘋了.
如果你認為這些問題僅限於可變對象作為可選參數的情況,那就錯了.
如果你進行面向對象編程(幾乎所有人都是這樣),那麼類在 Python 代碼中無處不在,有史以來,類最有用的特性之一是……(噔噔蹬蹬)繼承.
這只是一個花哨的說法,如果你有一個具有某些屬性的父類,你可以創建一個子類繼承其屬性,像這樣:
class parent(object):
x = 1
class firstchild(parent):
pass
class secondchild(parent):
pass
print(parent.x, firstchild.x, secondchild.x) # 返回 1 1 1
復制代碼
這不是一個特別好的例子,所以不要將其復制到你的代碼項目中.關鍵是,子類繼承了 x=1,因此我們可以調用它,並得到與父類相同的結果.
而且,如果我們改變了一個子類的 x 屬性,它應該只改變那個子類.就像你在青少年時期染了頭發,它不會改變你父母或你兄弟姐妹的頭發,這樣就可以了.
firstchild.x = 2
print(parent.x, firstchild.x, secondchild.x) # 返回 1 2 1
復制代碼
你小時候媽媽染頭發的時候發生了什麼? 你的頭發沒變,對吧?
parent.x = 3
print(parent.x, firstchild.x, secondchild.x) # 返回3 2 3
復制代碼
呃.
這是因為 Python 的方法解析順序,只要沒有特殊的說明,子類繼承了父類的一切,所以,在 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 = 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 相關的問題時,編碼並不是唯一的痛苦.Python 的執行速度也曾經慢得令人難以置信,比大多數語言都慢 2 到 10 倍.現在這種情況已經好了很多,例如,Numpy 包在處理列表、矩陣等等方面非常快.
使用 Python,多進程也變得更加容易.這可以讓你使用所有的 2 個、16 個或多個核心的計算機,而不是只有一個.我已經在 20 個核心上運行過,它已經為我節省了數周的計算時間.
此外,隨著機器學習在過去幾年中取得進展,Python 已經表明,它還有很長的路要走.像 Pytorch 和 Tensorflow 這樣的軟件包使得機器學習變得非常容易,而其他語言正在努力跟上這一步.
這些年來 Python 已經變得更好了,然而,這一事實並不能保證一個美好的未來,Python 仍然不是傻瓜式的,請謹慎地使用它.