這是「AI 學習之路」的第 6 篇,「Python 學習」的第 6 篇
小之的公眾號 : WeaponZhi
題外話
這周工作日 5 天,我並沒有更新文章,但大家並不要以為小之懶惰了。正好相反,自從上篇的 AI 入門文章後,我自己便開始進行機器學習的系統學習了,這週一到週五,只要有空閒時間,我就開始看吳恩達 Coursera 的視訊,可以說是非常痴迷了。
吳教授的課程非常通俗易懂,而且他本人的教學風格也是不緊不慢,循序漸進,甚至有關微積分和線代甚至 Octave 這些知識點都花了比較多的篇幅進行展開講解,親身體會後,再次推薦給大家。
目前,機器學習篇我已經學到一半了,實際上本來可以更快一些,但中間的一些微積分和線代的知識點,我又回爐複習了一下。非常慶幸我在大四的時候把高數重新複習了一遍,現在雖說不能完全回想起來,但回爐和記憶的串聯算是比較快的,節省了很多的時間。
同時,我現在每天保持一到兩題的 LeetCoded 的刷題量,實際上我不太追求說要刷的多快,刷題的目的一來是鞏固基礎,二來是每天刷一兩道,活動下腦子。我每天早上上班前,先開啟一道題,然後把題目閱讀一下,過個腦子,上班途中就想想思路,如果思路比較清晰,到公司在別人吃早飯的時間,我就把程式碼提交了,如果思路不太順,我就工作空閒或者中午的時候在桌上用紙筆畫一畫,然後晚上下班之後開始碼程式碼。
如果這樣的流程一天時間還是想不起來思路,那我就會直接看一下 discuss 或者 Solution,不追求必須靠自己解答出來,只要學到方法,過個腦子,然後把程式碼碼出來上傳到 GitHub 留個記錄,對我來說就夠了。
好了,迴歸正題,今天我們來較為細緻的講解下 Python 中的函式。
函式的定義
我們之前已經看了很多函式的使用例子了,比如我們要定義一個函式可以這樣寫
>>> def testFun(a):
... print(a)
...
>>> testFun
<function testFun at 0x1060f28c8>
>>> testFun('這是一個方法輸出')
這是一個方法輸出
複製程式碼
我們來看看這段程式碼。通過 def 語句,依次寫出函式名、括號、括號中的引數,還有最後的冒號,千萬不要忘記了。
冒號後面,就是具體的函式體了,函式體第一行和函式定義要有一個 Tab 的縮排距離。我們知道 Python 是沒有分號和大括號來區分程式碼的結束和開始的,所以縮排的問題一定要注意,如果你的縮排不正確,可能會報 indented 錯誤。
在互動式環境下定義函式的時候,冒號輸入完畢回車後,會有...
的提示,每行結束回車到下一行,連續兩次回車定義該函式完畢,重新回到>>>
提示符下。
Python 是一門物件導向語言,一切都是物件,甚至函式本身也是物件,我們稱這種特性為「函數語言程式設計」,上面的例子中,我們直接列印testFun
是可以列印出它的函式型別的。
像我們熟悉的 Kotlin、Groovy 還有 Go 語言,都有這樣的特性,函數語言程式設計可以有非常強大的擴充性,同時可以很輕易的解決很多不支援函數語言程式設計的語言下的一些寫法問題。這個我會在後面專門寫一篇文章來介紹 Python 的函數語言程式設計。
使用函式很簡單, 函式名、括號,加上引數即可呼叫函式。因為 Python 是動態型別語言,所以我們不需要像 Java 那樣,對每一個變數和方法引數都提前在編譯期設定好型別,我們定義testFun(a)
的時候,並沒有指示 a 到底是字串型別還是別的型別。這樣的自由肯定是有一定代價的,當函式體內部對引數的使用有較嚴格的要求的時候,如果傳參型別錯誤,就會報錯。
>>> def func_abs(x):
... if x >= 0:
... return x
... else:
... return -x
...
>>> func_abs(-1)
1
>>> func_abs('string')
TypeError: '>=' not supported between instances of 'str' and 'int'
複製程式碼
函式的引數
函式的定義不是很複雜,但搭配引數,就會非常靈活,Python 的引數五花八門,除了我們常用的位置引數外,還有預設引數、可變引數和關鍵字引數。
位置引數
我們之前定義的 testFun(a) 中的 a 就是一個位置引數,當然你可以設定很多位置引數,顧名思義,引數的意義是和位置一一對應的,比如我們現在改寫下 testFun 讓它具備輸出兩個引數的能力
>>> testFun(a,b):
... print(a,b)
...
>>> testFun('測試','引數')
測試 引數
複製程式碼
剛剛說的位置一致性的意思就是我們的輸入引數的順序是和函式定義時候的引數順序是一致的,第一個引數是'測試'
,第二個引數是'引數'
,只能代表a='測試'
,b='引數'
,順序不能錯亂。
預設引數
Java 中,如果需要使用一個函式的多種引數形式,是通過過載的形式的,這種方式是比較麻煩的,比如,上面的例子中,我們想讓testFun
既可以使用一個引數,也可以使用兩個引數
public void testFun(String a){
System.out.println(a);
}
public void testFun(String a,String b){
System.out.println(a + " " + b);
}
複製程式碼
在 Python 中,我們可以使用預設引數來一次性完成這樣的函式定義
>>> def testFun(a,b='函式'):
... print(a,b)
...
>>> testFun('測試')
測試 函式
>>> testFun('測試',"引數")
測試 引數
複製程式碼
使用 testFun(a) 的時候,會自動把 b 賦值為 '函式',只有當你需要改變 b 的時候,才需要自己輸入引數值。需要注意的是,預設引數定義要放在位置引數的後面。
不僅如此,預設引數的預設值一定要指向的是不可變物件,否則就會出現一些難以預料的問題,這裡舉一個例子
>>> def app_end(L=[]):
... L.append('END')
... return L
...
>>> app_end()
['END']
>>> app_end()
['END','END']
複製程式碼
注意到了嗎,雖然我們的預設引數 L 是一個空 list,但在呼叫的過程中,每次新增元素,都會被新增到 L 這個 list 中去,而不是我們預料的那樣,每次都是一個新的單元素 list,所以我們的預設引數,儘量使用不可變物件,除非你的設計邏輯就是如此。
可變引數
預設引數雖然可以擴充函式的引數數量,但畢竟數量還是固定的,如果我們想讓引數數量是任意的,可以使用可變引數,可變引數很簡單,在引數前加 * 號即可
>>> def calcSum(*nums):
... sum = 0
... for n in nums:
... sum = sum + n
... return sum
...
>>> calcSum(1,2,3)
6
>>> nums = [1,2,3]
>>> calcSum(*nums)
6
>>> nums = (1,2,3)
>>> calcSum(*nums)
6
複製程式碼
我們也可以把 list 和 tuple 加星號傳入可變引數中
關鍵字引數
關鍵字引數和可變引數一樣,可以允許你傳入 0 到任意個引數,不過這些引數都是含有引數名的,在函式內部是以一個 dict 的形式組裝
>>> def testKW(a,b,**kw):
... print(a,b,kw)
...
>>> testKW('測試','關鍵字引數',name='小之')
測試 關鍵字引數 {'name':'小之'}
>>> kw = {'name':'小之','age':23}
>>> testKW('測試','關鍵字引數',**kw)
測試 關鍵字引數 {'name':'小之','age':23}
複製程式碼
當然,你可以像可變引數那種形式一樣,通過提前定義好 dict,再把變數以**
開頭傳入。
關鍵字引數比較隨意,你可以傳入不受限制的引數,如果你需要判斷傳了哪些引數,你還需要在函式體內部進行判斷,這個時候,我們就可以用命名關鍵字引數來作一定的限制
>>> def testKW(a,b,*,name,age):
... print(a,b,name,age)
...
>>> testKW('測試','命名關鍵字引數',name='小之',age=23)
測試 命名關鍵字引數 小之 23
複製程式碼
命名關鍵字引數需要一個分隔符*
,*
號後面就會被看作是命名關鍵字引數。如果定義中有一個可變引數,那麼後面的命名關鍵字引數就不需要*
了,就像這樣
>>> def testKW(a,b,*args,name,age):
... print(a,b,args,name,age)
複製程式碼
命名關鍵字引數是可以有預設值的,有預設值的情況下,可以不傳入該引數,這點和預設引數有點類似
>>> def testKW(a,b,*,name='小之',age):
... print(a,b,name,age)
...
>>> testKW('測試','命名關鍵字引數',23)
測試 命名關鍵字引數 小之 23
複製程式碼
引數的組合
Python 中,上述引數可以組合使用,但需要注意一定的順序:必選位置引數、預設引數、可變引數、命名關鍵字引數、關鍵字引數,這部分我將用廖大的例子來簡單介紹下,我們先定義兩個有若干引數的函式:
def f1(a, b, c=0, *args, **kw):
print('a =', a, 'b =', b, 'c =', c, 'args =', args, 'kw =', kw)
def f2(a, b, c=0, *, d, **kw):
print('a =', a, 'b =', b, 'c =', c, 'd =', d, 'kw =', kw)
複製程式碼
呼叫的時候,Python 直譯器會自動按照引數位置和引數名把對應引數穿進去
>>> f1(1, 2)
a = 1 b = 2 c = 0 args = () kw = {}
>>> f1(1, 2, c=3)
a = 1 b = 2 c = 3 args = () kw = {}
>>> f1(1, 2, 3, 'a', 'b')
a = 1 b = 2 c = 3 args = ('a', 'b') kw = {}
>>> f1(1, 2, 3, 'a', 'b', x=99)
a = 1 b = 2 c = 3 args = ('a', 'b') kw = {'x': 99}
>>> f2(1, 2, d=99, ext=None)
a = 1 b = 2 c = 0 d = 99 kw = {'ext': None}
複製程式碼
最神奇的是可以通過一個 tuple 和 dict 完成上述函式的呼叫
>>> args = (1, 2, 3, 4)
>>> kw = {'d': 99, 'x': '#'}
>>> f1(*args, **kw)
a = 1 b = 2 c = 3 args = (4,) kw = {'d': 99, 'x': '#'}
>>> args = (1, 2, 3)
>>> kw = {'d': 88, 'x': '#'}
>>> f2(*args, **kw)
a = 1 b = 2 c = 3 d = 88 kw = {'x': '#'}
複製程式碼
所以我們可以通過類似func(*args, **kw)
的形式來呼叫任意函式,無論它的引數是如何定義的。你去翻看原始碼,可以看到很多內建的函式都是用這種形式定義函式的。
歡迎關注我的公眾號