三 函數#
調用函數
Python 內置了很多有用的函數,我們可以直接調用。要調用一個函數,需要知道函數的名稱和參數,比如求絕對值的函數 abs,只有一個參數。
可以直接從 Python 的官方網站查看文檔:
http://docs.python.org/3/library/functions.html#abs
也可以在交互式命令行通過 help (abs) 查看 abs 函數的幫助信息。
調用函數的時候,如果傳入的參數數量不對,會報TypeError的錯誤,並且 Python 會明確地告訴你:abs () 有且僅有 1 個參數,但給出了兩個:
>>> abs(1, 2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: abs() takes exactly one argument (2 given)
如果傳入的參數數量是對的,但參數類型不能被函數所接受,也會報 TypeError 的錯誤,並且給出錯誤信息:str 是錯誤的參數類型:
>>> abs('a')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: bad operand type for abs(): 'str'
而 max 函數 max () 可以接收任意多個參數,並返回最大的那個:
>>> max(1, 2)
2
>>> max(2, 3, 1, -5)
3
數據類型轉換
Python 內置的常用函數還包括數據類型轉換函數,比如 int () 函數可以把其他數據類型轉換為整數:
>>> int('123')
123
>>> int(12.34)
12
>>> float('12.34')
12.34
>>> str(1.23)
'1.23'
>>> str(100)
'100'
>>> bool(1)
True
>>> bool('')
False
函數名其實就是指向一個函數對象的引用,完全可以把函數名賦給一個變量,相當於給這個函數起了個 “別名”:
>>> a = abs # 變量a指向abs函數
>>> a(-1) # 所以也可以通過a調用abs函數
1
定義函數
在 Python 中,定義一個函數要使用 def 語句,依次寫出函數名、括號、括號中的參數和冒號:,然後,在縮進塊中編寫函數體,函數的返回值用 return 語句返回。
以自定義一個求絕對值的 my_abs 函數為例:
def my_abs(x):
if x >= 0:
return x
else:
return -x
請注意,函數體內部的語句在執行時,一旦執行到 return 時,函數就執行完畢,並將結果返回。因此,函數內部通過條件判斷和循環可以實現非常複雜的邏輯。
如果沒有 return 語句,函數執行完畢後也會返回結果,只是結果為None。return None 可以簡寫為 return。
在 Python 交互環境中定義函數時,注意 Python 會出現... 的提示。函數定義結束後需要按兩次回車重新回到 >>> 提示符下
>>>def my_abs(x):
... if x >= 0:
... return x
... else:
... return -x
...
>>> my_abs(-9)
9
>>>
如果你已經把 my_abs () 的函數定義保存為 abstest.py 文件了,那麼,可以在該文件的當前目錄下啟動 Python 解釋器,用 from abstest import my_abs 來導入 my_abs () 函數,注意 abstest 是文件名(不含.py 擴展名)
>>> from abstest import my_abs
>>> my_abs(-9)
9
空函數
如果想定義一個什麼事也不做的空函數,可以用 pass 語句:
def nop():
pass
pass 語句什麼都不做,那有什麼用?實際上 pass 可以用來作為佔位符,比如現在還沒想好怎麼寫函數的代碼,就可以先放一個 pass,讓代碼能運行起來。
pass 還可以用在其他語句裡,比如:
if age >= 18:
pass
缺少了 pass,代碼運行就會有語法錯誤。
參數檢查
調用函數時,如果參數個數不對,Python 解釋器會自動檢查出來,並拋出 TypeError:
>>> my_abs(1, 2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: my_abs() takes 1 positional argument but 2 were given
但是如果參數類型不對,Python 解釋器就無法幫我們檢查。試試 my_abs 和內置函數 abs 的差別:
>>> my_abs('A')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in my_abs
TypeError: unorderable types: str() >= int()
>>> abs('A')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: bad operand type for abs(): 'str'
當傳入了不恰當的參數時,內置函數 abs 會檢查出參數錯誤,而我們定義的 my_abs 沒有參數檢查,會導致 if 語句出錯,出錯信息和 abs 不一樣。所以,這個函數定義不夠完善。
讓我們修改一下 my_abs 的定義,對參數類型做檢查,只允許整數和浮點數類型的參數。數據類型檢查可以用內置函數isinstance() 實現:
def my_abs(x):
if not isinstance(x, (int, float)):
raise TypeError('bad operand type')
if x >= 0:
return x
else:
return -x
添加了參數檢查後,如果傳入錯誤的參數類型,函數就可以拋出一個錯誤:
>>> my_abs('A')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in my_abs
TypeError: bad operand type
返回多個值
函數可以返回多個值
比如在遊戲中經常需要從一個點移動到另一個點,給出坐標、位移和角度,就可以計算出新的坐標:
import math
def move(x, y, step, angle=0):
nx = x + step * math.cos(angle)
ny = y - step * math.sin(angle)
return nx, ny
import math 語句表示導入 math 包,並允許後續代碼引用 math 包裡的 sin、cos 等函數。
然後,我們就可以同時獲得返回值:
>>> x, y = move(100, 100, 60, math.pi / 6)
>>> print(x, y)
151.96152422706632 70.0
但其實這只是一種假象,Python 函數返回的仍然是單一值:
>>> r = move(100, 100, 60, math.pi / 6)
>>> print(r)
(151.96152422706632, 70.0)
原來返回值是一個tuple!但是,在語法上,返回一個 tuple 可以省略括號,而多個變量可以同時接收一個 tuple,按位置賦給對應的值,所以,Python 的函數返回多值其實就是返回一個 tuple,但寫起來更方便。
函數的參數
對於函數的調用者來說,只需要知道如何傳遞正確的參數,以及函數將返回什麼樣的值就夠了,函數內部的複雜邏輯被封裝起來,調用者無需了解。
Python 的函數定義非常簡單,但靈活度卻非常大。除了正常定義的必選參數外,還可以使用默認參數、可變參數和關鍵字參數,使得函數定義出來的接口,不但能處理複雜的參數,還可以簡化調用者的代碼。
位置參數
我們先寫一個計算 x2 的函數:
def power(x):
return x * x
對於 power (x) 函數,參數 x 就是一個位置參數。
當我們調用 power 函數時,必須傳入有且僅有的一个參數 x:
>>> power(5)
25
現在,如果我們要計算 x3 怎麼辦?可以再定義一個 power3 函數,但是如果要計算 x4、x5…… 怎麼辦?我們不可能定義無限多個函數。
你也許想到了,可以把 power (x) 修改為 power (x, n),用來計算 xn
def power(x, n):
s = 1
while n > 0:
n = n - 1
s = s * x
return s
對於這個修改後的 power (x, n) 函數,可以計算任意 n 次方:
>>> power(5, 2)
25
修改後的 power (x, n) 函數有兩個參數:x 和 n,這兩個參數都是位置參數,調用函數時,傳入的兩個值按照位置順序依次賦給參數 x 和 n。
位置參數
我們先寫一個計算 x2 的函數:
def power(x):
return x * x
對於 power (x) 函數,參數 x 就是一個位置參數。
當我們調用 power 函數時,必須傳入有且僅有的一个參數 x:
power(5)
25
power(15)
225
現在,如果我們要計算 x3 怎麼辦?可以再定義一個 power3 函數,但是如果要計算 x4、x5…… 怎麼辦?我們不可能定義無限多個函數。
你也許想到了,可以把 power (x) 修改為 power (x, n),用來計算 xn,說幹就幹:
def power(x, n):
s = 1
while n > 0:
n = n - 1
s = s * x
return s
對於這個修改後的 power (x, n) 函數,可以計算任意 n 次方:
>>> power(5, 2)
25
>>> power(5, 3)
125
修改後的 power (x, n) 函數有兩個參數:x 和 n,這兩個參數都是位置參數,調用函數時,傳入的兩個值按照位置順序依次賦給參數 x 和 n。
默認參數
新的 power (x, n) 函數定義沒有問題,但是,舊的調用代碼失敗了,原因是我們增加了一個參數,導致舊的代碼因為缺少一個參數而無法正常調用:
>>> power(5)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: power() missing 1 required positional argument: 'n'
Python 的錯誤信息很明確:調用函數 power () 缺少了一个位置參數 n。
這個時候,默認參數就排上用場了。由於我們經常計算 x2,所以,完全可以把第二個參數 n 的默認值設定為 2:
def power(x, n=2):
s = 1
while n > 0:
n = n - 1
s = s * x
return s
這樣,當我們調用 power (5) 時,相當於調用 power (5, 2):
>>> power(5)
25
>>> power(5, 2)
25
而對於 n > 2 的
其他情況,就必須明確地傳入 n,比如 power (5, 3)。
從上面的例子可以看出,默認參數可以簡化函數的調用。設置默認參數時,有幾點要注意:
一是必選參數在前,默認參數在後,否則 Python 的解釋器會報錯
二是如何設置默認參數。
當函數有多個參數時,把變化大的參數放前面,變化小的參數放後面。變化小的參數就可以作為默認參數。
使用默認參數有什麼好處?最大的好處是能降低調用函數的難度。
舉個例子,我們寫個一年級小學生註冊的函數,需要傳入 name 和 gender 兩個參數:
def enroll(name, gender):
print('name:', name)
print('gender:', gender)
這樣,調用 enroll () 函數只需要傳入兩個參數:
>>> enroll('Sarah', 'F')
name: Sarah
gender: F
如果要繼續傳入年齡、城市等信息怎麼辦?這樣會使得調用函數的複雜度大大增加。
我們可以把年齡和城市設為默認參數:
def enroll(name, gender, age=6, city='Beijing'):
print('name:', name)
print('gender:', gender)
print('age:', age)
print('city:', city)
這樣,大多數學生註冊時不需要提供年齡和城市,只提供必須的兩個參數:
>>> enroll('Sarah', 'F')
name: Sarah
gender: F
age: 6
city: Beijing
只有與默認參數不符的學生才需要提供額外的信息:
enroll('Bob', 'M', 7)
enroll('Adam', 'M', city='Tianjin')
可見,默認參數降低了函數調用的難度,而一旦需要更複雜的調用時,又可以傳遞更多的參數來實現。無論是簡單調用還是複雜調用,函數只需要定義一個。
有多個默認參數時,調用的時候,既可以按順序提供默認參數,比如調用 enroll ('Bob', 'M', 7),意思是,除了 name,gender 這兩個參數外,最後 1 個參數應用在參數 age 上,city 參數由於沒有提供,仍然使用默認值。
也可以不按順序提供部分默認參數。當不按順序提供部分默認參數時,需要把參數名寫上。比如調用 enroll ('Adam', 'M', city='Tianjin'),意思是,city 參數用傳進去的值,其他默認參數繼續使用默認值。
默認參數很有用,但使用不當,也會掉坑裡。默認參數有個最大的坑,演示如下:
先定義一個函數,傳入一個 list,添加一個 END 再返回:
def add_end(L=[]):
L.append('END')
return L
當你正常調用時,結果似乎不錯:
>>> add_end([1, 2, 3])
[1, 2, 3, 'END']
>>> add_end(['x', 'y', 'z'])
['x', 'y', 'z', 'END']
當你使用默認參數調用時,一開始結果也是對的:
>>> add_end()
['END']
但是,再次調用 add_end () 時,結果就不對了:
>>> add_end()
['END', 'END']
>>> add_end()
['END', 'END', 'END']
很多初學者很疑惑,默認參數是 [],但是函數似乎每次都 “記住了” 上次添加了 'END' 後的 list。
原因解釋如下:
Python 函數在定義的時候,默認參數 L 的值就被計算出來了,即 [],因為默認參數 L 也是一個變量,它指向對象 [],每次調用該函數,如果改變了 L 的內容,則下次調用時,默認參數的內容就變了,不再是函數定義時的 [] 了。
定義默認參數要牢記一點:默認參數必須指向不變對象!
要修改上面的例子,我們可以用 None 這個不變對象來實現:
def add_end(L=None):
if L is None:
L = []
L.append('END')
return L
現在,無論調用多少次,都不會有問題:
>>> add_end()
['END']
>>> add_end()
['END']
為什麼要設計 str、None 這樣的不變對象呢?因為不變對象一旦創建,對象內部的數據就不能修改,這樣就減少了由於修改數據導致的錯誤。此外,由於對象不變,多任務環境下同時讀取對象不需要加鎖,同時讀一點問題都沒有。我們在編寫程序時,如果可以設計一個不變對象,那就儘量設計成不變對象。
可變參數
在 Python 函數中,還可以定義可變參數。顧名思義,可變參數就是傳入的參數個數是可變的,可以是 1 個、2 個到任意個,還可以是 0 個。
我們以數學題為例子,給定一組數字 a,b,c……,請計算 a2 + b2 + c2 + ……。
要定義出這個函數,我們必須確定輸入的參數。由於參數個數不確定,我們首先想到可以把 a,b,c…… 作為一個 list 或 tuple 傳進來,這樣,函數可以定義如下:
def calc(numbers):
sum = 0
for n in numbers:
sum = sum + n * n
return sum
但是調用的時候,需要先組裝出一個 list 或 tuple:
>>> calc([1, 2, 3])
14
>>> calc((1, 3, 5, 7))
84
如果利用可變參數,調用函數的方式可以簡化成這樣:
>>> calc(1, 2, 3)
14
>>> calc(1, 3, 5, 7)
84
所以,我們把函數的參數改為可變參數:
def calc(*numbers):
sum = 0
for n in numbers:
sum = sum + n * n
return sum
定義可變參數和定義一個 list 或 tuple 參數相比,僅僅在參數前面加了一個 * 號。在函數內部,參數 numbers 接收到的是一個tuple,因此,函數代碼完全不變。但是,調用該函數時,可以傳入任意個參數,包括 0 個參數:
>>> calc(1, 2)
5
>>> calc()
0
如果已經有一個 list 或者 tuple,要調用一個可變參數怎麼辦?可以這樣做:
>>> nums = [1, 2, 3]
>>> calc(nums[0], nums[1], nums[2])
14
這種寫法當然是可行的,問題是太繁瑣,所以 Python 允許你在 list 或 tuple 前面加一個 * 號,把 list 或 tuple 的元素變成可變參數傳進去:
>>> nums = [1, 2, 3]
>>> calc(*nums)
14
*nums 表示把 nums 這個 list 的所有元素作為可變參數傳進去。這種寫法相當有用,而且很常見。
關鍵字參數
可變參數允許你傳入 0 個或任意個參數,這些可變參數在函數調用時自動組裝為一個tuple。而關鍵字參數允許你傳入 0 個或任意個含參數名的參數,這些關鍵字參數在函數內部自動組裝為一個dict。
def person(name, age, **kw):
print('name:', name, 'age:', age, 'other:', kw)
函數 person 除了必選參數 name 和 age 外,還接受關鍵字參數 kw。在調用該函數時,可以只傳入必選參數:
>>> person('Michael', 30)
name: Michael age: 30 other: {}
也可以傳入任意個數的關鍵字參數:
>>> person('Bob', 35, city='Beijing')
name: Bob age: 35 other: {'city': 'Beijing'}
>>> person('Adam', 45, gender='M', job='Engineer')
name: Adam age: 45 other: {'gender': 'M', 'job': 'Engineer'}
關鍵字參數有什麼用?它可以擴展函數的功能。比如,在 person 函數裡,我們保證能接收到 name 和 age 這兩個參數,但是,如果調用者願意提供更多的參數,我們也能收到。試想你正在做一個用戶註冊的功能,除了用戶名和年齡是必填項外,其他都是可選項,利用關鍵字參數來定義這個函數就能滿足註冊的需求。
和可變參數類似,也可以先組裝出一個 dict,然後,把該 dict 轉換為關鍵字參數傳進去:
>>> extra = {'city': 'Beijing', 'job': 'Engineer'}
>>> person('Jack', 24, city=extra['city'], job=extra['job'])
name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}
當然,上面複雜的調用可以用簡化的寫法:
>>> extra = {'city': 'Beijing', 'job': 'Engineer'}
>>> person('Jack', 24, **extra)
name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}
extra 表示把 extra 這個 dict 的所有 key-value 用關鍵字參數傳入到函數的kw 參數,kw 將獲得一個 dict,注意 kw 獲得的 dict 是 extra 的一份拷貝,對 kw 的改動不會影響到函數外的 extra。
命名關鍵字參數
對於關鍵字參數,函數的調用者可以傳入任意不受限制的關鍵字參數。至於到底傳入了哪些,就需要在函數內部通過 kw 檢查。
仍以 person () 函數為例,我們希望檢查是否有 city 和 job 參數:
def person(name, age, **kw):
if 'city' in kw:
# 有city參數
pass
if 'job' in kw:
# 有job參數
pass
print('name:', name, 'age:', age, 'other:', kw)
但是調用者仍可以傳入不受限制的關鍵字參數:
>>> person('Jack', 24, city='Beijing', addr='Chaoyang', zipcode=123456)
如果要限制關鍵字參數的名字,就可以用命名關鍵字參數,例如,只接收 city 和 job 作為關鍵字參數。這種方式定義的函數如下:
def person(name, age, *, city, job):
print(name, age, city, job)
和關鍵字參數 **kw 不同,命名關鍵字參數需要一個特殊分隔符 *,* 後面的參數被視為命名關鍵字參數。
調用方式如下:
>>> person('Jack', 24, city='Beijing', job='Engineer')
Jack 24 Beijing Engineer
如果函數定義中已經有了一個可變參數,後面跟著的命名關鍵字參數就不再需要一個特殊分隔符 * 了:
def person(name, age, *args, city, job):
print(name, age, args, city, job)
命名關鍵字參數必須傳入參數名,這和位置參數不同。如果沒有傳入參數名,調用將報錯:
>>> person('Jack', 24, 'Beijing', 'Engineer')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: person() missing 2 required keyword-only arguments: 'city' and 'job'
由於調用時缺少參數名 city 和 job,Python 解釋器把前兩個參數視為位置參數,後兩個參數傳給 * args,但缺少命名關鍵字參數導致報錯。
命名關鍵字參數可以有缺省值,從而簡化調用:
def person(name, age, *, city='Beijing', job):
print(name, age, city, job)
由於命名關鍵字參數 city 具有默認值,調用時,可不傳入 city 參數:
>>> person('Jack', 24, job='Engineer')
Jack 24 Beijing Engineer
使用命名關鍵字參數時,要特別注意,如果沒有可變參數,就必須加一個作為特殊分隔符。如果缺少,Python 解釋器將無法識別位置參數和命名關鍵字參數
def person(name, age, city, job):
# 缺少 *,city和job被視為位置參數
pass
參數組合
在 Python 中定義函數,可以用必選參數、默認參數、可變參數、命名關鍵字參數和關鍵字參數這 5 種參數都可以組合使用。但是請注意,參數定義的順序必須是:必選參數、默認參數、可變參數、命名關鍵字參數和關鍵字參數。
比如定義一個函數,包含上述若干種參數:
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) 的形式調用它,無論它的參數是如何定義的。
雖然可以組合多達 5 種參數,但不要同時使用太多的組合,否則函數接口的可理解性很差。
小結
1 Python 的函數具有非常靈活的參數形態,既可以實現簡單的調用,又可以傳入非常複雜的參數。
2 默認參數一定要用不可變對象,如果是可變對象,程序運行時會有邏輯錯誤!
3 要注意定義可變參數和關鍵字參數的語法:
*args 是可變參數,args 接收的是一個 tuple;
**kw 是關鍵字參數,kw 接收的是一個 dict。
4 以及調用函數時如何傳入可變參數和關鍵字參數的語法:
可變參數既可以直接傳入:func (1, 2, 3),又可以先組裝 list 或 tuple,再通過args 傳入:func ((1, 2, 3));
關鍵字參數既可以直接傳入:func (a=1, b=2),又可以先組裝 dict,再通過kw 傳入:func ({'a': 1, 'b': 2})。
使用 * args 和 **kw 是 Python 的習慣寫法,當然也可以用其他參數名,但最好使用習慣用法。
5 命名的關鍵字參數是為了限制調用者可以傳入的參數名,同時可以提供默認值。
6 定義命名的關鍵字參數在沒有可變參數的情況下不要忘了寫分隔符 *,否則定義的將是位置參數。
遞歸函數
在函數內部,可以調用其他函數。如果一個函數在內部調用自身本身,這個函數就是遞歸函數。
舉個例子,我們來計算階乘 n! = 1 x 2 x 3 x ... x n,用函數 fact (n) 表示,可以看出:
fact(n)=n!=1×2×3×⋅⋅⋅×(n−1)×n=(n−1)!×n=fact(n−1)×n
所以,fact (n) 可以表示為 n x fact (n-1),只有 n=1 時需要特殊處理。
於是,fact (n) 用遞歸的方式寫出來就是:
def fact(n):
if n==1:
return 1
return n * fact(n - 1)
上面就是一個遞歸函數。可以試試:
>>> fact(1)
1
>>> fact(5)
120
如果我們計算 fact (5),可以根據函數定義看到計算過程如下:
===> fact(5)
===> 5 * fact(4)
===> 5 * (4 * fact(3))
===> 5 * (4 * (3 * fact(2)))
===> 5 * (4 * (3 * (2 * fact(1))))
===> 5 * (4 * (3 * (2 * 1)))
===> 5 * (4 * (3 * 2))
===> 5 * (4 * 6)
===> 5 * 24
===> 120
遞歸函數的優點是定義簡單,邏輯清晰。理論上,所有的遞歸函數都可以寫成循環的方式,但循環的邏輯不如遞歸清晰。
使用遞歸函數需要注意防止棧溢出。在計算機中,函數調用是通過棧(stack)這種數據結構實現的,每當進入一個函數調用,棧就會加一層棧幀,每當函數返回,棧就會減一層棧幀。由於棧的大小不是無限的,所以,遞歸調用的次數過多,會導致棧溢出。可以試試 fact (1000):
>>> fact(1000)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in fact
...
File "<stdin>", line 4, in fact
RuntimeError: maximum recursion depth exceeded in comparison
解決遞歸調用棧溢出的方法是通過尾遞歸優化,事實上尾遞歸和循環的效果是相同的,所以,把循環看成是一種特殊的尾遞歸函數也是可以的。
尾遞歸: 是指在函數返回的時候,調用自身本身,並且,return 語句不能包含表達式。
這樣,編譯器或者解釋器就可以把尾遞歸做優化,使遞歸本身無論調用多少次,都只佔用一個棧幀,不會出現棧溢出的情況。
上面的 fact (n) 函數由於 return n * fact (n - 1) 引入了乘法表達式,所以就不是尾遞歸了。要改成尾遞歸方式,需要多一點代碼,主要是要把每一步的乘積傳入到遞歸函數中:
def fact(n):
return fact_iter(n, 1)
def fact_iter(num, product):
if num == 1:
return product
return fact_iter(num - 1, num * product)
可以看到,return fact_iter (num - 1, num * product) 僅返回遞歸函數本身,num - 1 和 num * product 在函數調用前就會被計算,不影響函數調用。
fact (5) 對應的 fact_iter (5, 1) 的調用如下:
===> fact_iter(5, 1)
===> fact_iter(4, 5)
===> fact_iter(3, 20)
===> fact_iter(2, 60)
===> fact_iter(1, 120)
===> 120
尾遞歸調用時,如果做了優化,棧不會增長,因此,無論多少次調用也不會導致棧溢出。
遺憾的是,大多數編程語言沒有針對尾遞歸做優化,Python 解釋器也沒有做優化,所以,即使把上面的 fact (n) 函數改成尾遞歸方式,也會導致棧溢出。
小結
1 使用遞歸函數的優點是邏輯簡單清晰,缺點是過深的調用會導致棧溢出。
2 針對尾遞歸優化的語言可以通過尾遞歸防止棧溢出。尾遞歸事實上和循環是等價的,沒有循環語句的編程語言只能通過尾遞歸實現循環。
3 Python 標準的解釋器沒有針對尾遞歸做優化,任何遞歸函數都存在棧溢出的问题。
四 高級特性#
代碼越少,開發效率越高
切片
取一個 list 或 tuple 的部分元素是非常常見的操作。比如,一個 list 如下:
>>> L = ['Michael', 'Sarah', 'Tracy', 'Bob', 'Jack']
取前 3 個元素,應該怎麼做?
笨辦法:
>>> [L[0], L[1], L[2]]
['Michael', 'Sarah', 'Tracy']
之所以是笨辦法是因為擴展一下,取前 N 個元素就沒轍了。
取前 N 個元素,也就是索引為 0-(N-1) 的元素,可以用循環:
>>> r = []
>>> n = 3
>>> for i in range(n):
... r.append(L[i])
...
>>> r
['Michael', 'Sarah', 'Tracy']
對這種經常取指定索引範圍的操作,用循環十分繁瑣,因此,Python 提供了切片(Slice)操作符,能大大簡化這種操作。
對應上面的问题,取前 3 個元素,用一行代碼就可以完成切片:
>>> L[0:3]
['Michael', 'Sarah', 'Tracy']
L [0:3] 表示,從索引 0 開始取,直到索引 3 為止,但不包括索引 3。即索引 0,1,2,正好是 3 個元素。
如果第一個索引是 0,還可以省略:
>>> L[:3]
['Michael', 'Sarah', 'Tracy']
也可以從索引 1 開始,取出 2 個元素出來:
>>> L[1:3]
['Sarah', 'Tracy']
類似的,既然 Python 支持 L [-1] 取倒數第一個元素,那麼它同樣支持倒數切片
>>> L[-2:]
['Bob', 'Jack']
>>> L[-2:-1]
['Bob']
記住倒數第一個元素的索引是 -1
切片操作十分有用。我們先創建一個 0-99 的
>>> L = list(range(100))
>>> L
[0, 1, 2, 3, ..., 99]
可以通過切片輕鬆取出某一段數列。比如前 10 個數:
>>> L[:10]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
後 10 個數:
>>> L[-10:]
[90, 91, 92, 93, 94, 95, 96, 97, 98, 99]
前 11-20 個數:
>>> L[10:20]
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
前 10 個數,每兩個取一個:
>>> L[:10:2]
[0, 2, 4, 6, 8]
所有數,每 5 個取一個:
>>> L[::5]
[0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95]
甚至什麼都不寫,只寫 [:] 就可以原樣複製一個 list:
>>> L[:]
[0, 1, 2, 3, ..., 99]
tuple 也是一種 list,唯一区别是 tuple 不可變。因此,tuple 也可以用切片操作,只是操作的結果仍是 tuple:
>>> (0, 1, 2, 3, 4, 5)[:3]
(0, 1, 2)
字符串 'xxx' 也可以看成是一種 list,每個元素就是一個字符。因此,字符串也可以用切片操作,只是操作結果仍是字符串:
>>> 'ABCDEFG'[:3]
'ABC'
>>> 'ABCDEFG'[::2]
'ACEG'
在很多編程語言中,針對字符串提供了很多各種截取函數(例如,substring),其實目的就是對字符串切片。Python 沒有針對字符串的截取函數,只需要切片一個操作就可以完成,非常簡單。
迭代
如果給定一個 list 或 tuple,我們可以通過 for 循環來遍歷這個 list 或 tuple,這種遍歷我們稱為迭代(Iteration)。
在 Python 中,迭代是通過for ... in來完成的
而很多語言比如 C 語言,迭代 list 是通過下標完成的,比如 C 代碼:
for (i=0; i<length; i++) {
n = list[i];
}
可以看出,Python 的 for 循環抽象程度要高於 C 的 for 循環,因為 Python 的 for 循環不僅可以用在 list 或 tuple 上,還可以作用在其他可迭代對象上。
list 這種數據類型雖然有下標,但很多其他數據類型是沒有下標的,但是,只要是可迭代對象,無論有無下標,都可以迭代,比如 dict 就可以迭代:
>>> d = {'a': 1, 'b': 2, 'c': 3}
>>> for key in d:
... print(key)
...
a
c
b
因為 dict 的存儲不是按照 list 的方式順序排列,所以,迭代出的結果順序很可能不一樣。
默認情況下,dict 迭代的是 key。如果要迭代 value,可以用for value in d.values(),如果要同時迭代 key 和 value,可以用for k, v in d.items()。
由於字符串也是可迭代對象,因此,也可以作用於 for 循環:
>>> for ch in 'ABC':
... print(ch)
...
A
B
C
所以,當我們使用 for 循環時,只要作用於一個可迭代對象,for 循環就可以正常運行,而我們不太關心該對象究竟是 list 還是其他數據類型。
那麼,如何判斷一個對象是可迭代對象呢?方法是通過collections.abc 模塊的 Iterable類型判斷:
>>> from collections.abc import Iterable
>>> isinstance('abc', Iterable) # str是否可迭代
True
>>> isinstance([1,2,3], Iterable) # list是否可迭代
True
>>> isinstance(123, Iterable) # 整數是否可迭代
False
最後一個問題,如果要對 list 實現類似 Java 那樣的下標循環怎麼辦?Python 內置的enumerate 函數可以把一個 list 變成索引 - 元素對,這樣就可以在 for 循環中同時迭代索引和元素本身:
>>> for i, value in enumerate(['A', 'B', 'C']):
... print(i, value)
...
0 A
1 B
2 C
上面的 for 循環裡,同時引用了兩個變量,在 Python 裡是很常見的,比如下面的代碼:
>>> for x, y in [(1, 1), (2, 4), (3, 9)]:
... print(x, y)
...
1 1
2 4
3 9
任何可迭代對象都可以作用於 for 循環,包括我們自定義的數據類型,只要符合迭代條件,就可以使用 for 循環。
列表生成式
列表生成式即 List Comprehensions,是 Python 內置的非常簡單卻強大的可以用來創建 list 的生成式。
舉個例子,要生成 list [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 可以用 list (range (1, 11))
但如果要生成 [1x1, 2x2, 3x3, ..., 10x10] 怎麼做?方法一是循環:
>>> L = []
>>> for x in range(1, 11):
... L.append(x * x)
...
>>> L
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
但是循環太繁瑣,而列表生成式則可以用一行語句代替循環生成上面的 list:
>>> [x * x for x in range(1, 11)]
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
寫列表生成式時,把要生成的元素 x * x 放到前面,後面跟 for 循環,就可以把 list 創建出來
for 循環後面還可以加上 if 判斷,這樣我們就可以篩選出僅偶數的平方:
>>> [x * x for x in range(1, 11) if x % 2 == 0]
[4, 16, 36, 64, 100]
還可以使用兩層循環,可以生成全排列:
>>> [m + n for m in 'ABC' for n in 'XYZ']
['AX', 'AY', 'AZ', 'BX', 'BY', 'BZ', 'CX', 'CY', 'CZ']
三層和三層以上的循環就很少用了。
運用列表生成式,可以寫出非常簡潔的代碼。例如,列出當前目錄下的所有文件和目錄名,可以通過一行代碼實現:
>>> import os # 導入os模塊,模塊的概念後面講到
>>> [d for d in os.listdir('.')] # os.listdir可以列出文件和目錄
['.emacs.d', '.ssh', '.Trash', 'Adlm', 'Applications', 'Desktop', 'Documents', 'Downloads', 'Library', 'Movies', 'Music', 'Pictures', 'Public', 'VirtualBox VMs', 'Workspace', 'XCode']
for 循環其實可以同時使用兩個甚至多個變量,比如 dict 的 items () 可以同時迭代 key 和 value:
>>> d = {'x': 'A', 'y': 'B', 'z': 'C' }
>>> for k, v in d.items():
... print(k, '=', v)
...
y = B
x = A
z = C
因此,列表生成式也可以使用兩個變量來生成 list:
>>> d = {'x': 'A', 'y': 'B', 'z': 'C' }
>>> [k + '=' + v for k, v in d.items()]
['y=B', 'x=A', 'z=C']
最後把一個 list 中所有的字符串變成小寫:
>>> L = ['Hello', 'World', 'IBM', 'Apple']
>>> [s.lower() for s in L]
['hello', 'world', 'ibm', 'apple']
if ... else
使用列表生成式的時候,有些童鞋經常搞不清楚 if...else 的用法。
例如,以下代碼正常輸出偶數:
>>> [x for x in range(1, 11) if x % 2 == 0]
[2, 4, 6, 8, 10]
但是,我們不能在最後的 if 加上 else:
>>> [x for x in range(1, 11) if x % 2 == 0 else 0]
File "<stdin>", line 1
[x for x in range(1, 11) if x % 2 == 0 else 0]
^
SyntaxError: invalid syntax
這是因為跟在 for 後面的 if 是一個篩選條件,不能帶 else,否則如何篩選?
把 if 寫在 for 前面必須加 else,否則報錯:
>>> [x if x % 2 == 0 for x in range(1, 11)]
File "<stdin>", line 1
[x if x % 2 == 0 for x in range(1, 11)]
^
SyntaxError: invalid syntax
這是因為 for 前面的部分是一個表達式,它必須根據 x 計算出一個結果。因此,考察表達式:x if x % 2 == 0,它無法根據 x 計算出結果,因為缺少 else,必須加上 else:
>>> [x if x % 2 == 0 else -x for x in range(1, 11)]
[-1, 2, -3, 4, -5, 6, -7, 8, -9, 10]
上述 for 前面的表達式 x if x % 2 == 0 else -x 才能根據 x 計算出確定的結果。
可見,在一個列表生成式中,for 前面的 if ... else 是表達式,而 for 後面的 if 是過濾條件,不能帶 else。
生成器
通過列表生成式,我們可以直接創建一個列表。但是,受到內存限制,列表容量肯定是有限的。而且,創建一個包含 100 萬個元素的列表,不僅佔用很大的存儲空間,如果我們僅僅需要訪問前面幾個元素,那後面絕大多數元素佔用的空間都白白浪費了。
所以,如果列表元素可以按照某種算法推算出來,那我們是否可以在循環的過程中不斷推算出後續的元素呢?這樣就不必創建完整的 list,從而節省大量的空間。在 Python 中,這種一邊循環一邊計算的機制,稱為生成器:generator。
要創建一個 generator,有很多種方法。第一種方法很簡單,只要把一個列表生成式的 [] 改成 (),就創建了一個 generator:
>>> L = [x * x for x in range(10)]
>>> L
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> g = (x * x for x in range(10))
>>> g
<generator object <genexpr> at 0x1022ef630>
創建 L 和 g 的區別僅在於最外層的 [] 和 (),L 是一個 list,而 g 是一個 generator。
我們可以直接打印出 list 的每一個元素,但我們怎麼打印出 generator 的每一個元素呢?
如果要一個一個打印出來,可以通過next () 函數獲得 generator 的下一個返回值:
>>> next(g)
0
>>> next(g)
1
>>> next(g)
4
>>> next(g)
9
>>> next(g)
16
>>> next(g)
25
>>> next(g)
36
>>> next(g)
49
>>> next(g)
64
>>> next(g)
81
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
generator 保存的是算法,每次調用 next (g),就計算出 g 的下一個元素的值,直到計算到最後一個元素,沒有更多的元素時,拋出 StopIteration 的錯誤。
當然,上面這種不斷調用 next (g) 實在是太变态了,正確的方法是使用 for 循環,因為generator 也是可迭代對象
>>> g = (x * x for x in range(10))
>>> for n in g:
... print(n)
...
0
1
4
9
16
25
36
49
64
81
所以,我們創建了一個 generator 後,基本上永遠不會調用 next (),而是通過for 循環來迭代它,並且不需要關心 StopIteration 的錯誤。
generator 非常強大。如果推算的算法比較複雜,用類似列表生成式的 for 循環無法實現的時候,還可以用函數來實現。
比如,著名的斐波拉契數列(Fibonacci),除第一個和第二個數外,任意一個數都可由前兩個數相加得到:
1, 1, 2, 3, 5, 8, 13, 21, 34, ...
斐波拉契數列用列表生成式寫不出來,但是,用函數把它打印出來卻很容易:
def fib(max):
n, a, b = 0, 0, 1
while n < max:
print(b)
a, b = b, a + b
n = n + 1
return 'done'
注意,賦值語句:
a, b = b, a + b
相當於:
t = (b, a + b) # t是一個tuple
a = t[0]
b = t[1]
但不必顯式寫出臨時變量 t 就可以賦值。
上面的函數可以輸出斐波那契數列的前 N 個數:
>>> fib(6)
1
1
2
3
5
8
'done'
仔細觀察,可以看出,fib 函數實際上是定義了斐波拉契數列的推算規則,可以從第一個元素開始,推算出後續任意的元素,這種邏輯其實非常類似 generator。
也就是說,上面的函數和 generator 僅一步之遙。要把 fib 函數變成 generator 函數,只需要把 print (b) 改為 yield b 就可以了:
def fib(max):
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b
n = n + 1
return 'done'
這就是定義 generator 的另一種方法。如果一個函數定義中包含yield關鍵字,那麼這個函數就不再是一個普通函數,而是一個generator 函數,調用一個 generator 函數將返回一個 generator:
>>> f = fib(6)
>>> f
<generator object fib at 0x104feaaa0>
這裡,最難理解的就是 generator 函數和普通函數的執行流程不一樣。普通函數是順序執行,遇到 return 語句或者最後一行函數語句就返回。而變成 generator 的函數,在每次調用 next () 的時候執行,遇到 yield 語句返回,再次執行時從上次返回的 yield 語句處繼續執行。
舉個簡單的例子,定義一個 generator 函數,依次返回數字 1,3,5:
def odd():
print('step 1')
yield 1
print('step 2')
yield(3)
print('step 3')
yield(5)
調用該 generator 函數時,首先要生成一個 generator 對象,然後用 next () 函數不斷獲得下一个返回值:
>>> o = odd()
>>> next(o)
step 1
1
>>> next(o)
step 2
3
>>> next(o)
step 3
5
可以看到,odd 不是普通函數,而是 generator 函數,在執行過程中,遇到 yield 就中斷,下次又繼續執行。執行 3 次 yield 後,已經沒有 yield 可以執行了,所以,第 4 次調用 next (o) 就報錯。
請務必注意:調用 generator 函數會創建一個 generator 對象,多次調用 generator 函數會創建多個相互獨立的 generator。
有的發現這樣調用 next () 每次都返回 1:
>>> next(odd())
step 1
1
>>> next(odd())
step 1
1
>>> next(odd())
step 1
1
原因在於 odd () 會創建一個新的 generator 對象,上述代碼實際上創建了 3 個完全獨立的 generator,對 3 個 generator 分別調用 next () 當然每個都會返回第一個值。
正確的寫法是創建一個 generator 對象,然後不斷對這一個 generator 對象調用 next ():
>>> g = odd()
>>> next(g)
step 1
1
>>> next(g)
step 2
3
>>> next(g)
step 3
5
回到 fib 的例子,我們在循環過程中不斷調用 yield,就會不斷中斷。當然要給循環設置一個條件來退出循環,不然就會產生一個無限數列出來。
同樣的,把函數改成 generator 函數後,我們基本上從來不會用 next () 來獲取下一個返回值,而是直接使用 for 循環來迭代:
>>> for n in fib(6):
... print(n)
...
1
1
2
3
5
8
但是用 for 循環調用 generator 時,發現拿不到 generator 的 return 語句的返回值。如果想要拿到返回值,必須捕獲 StopIteration 錯誤,返回值包含在 StopIteration 的 value 中:
>>> g = fib(6)
>>> while True:
... try:
... x = next(g)
... print('g:', x)
... except StopIteration as e:
... print('Generator return value:', e.value)
... break
...
g: 1
g: 1
g: 2
g: 3
g: 5
g: 8
Generator return value: done
小結
1 generator 是非常強大的工具,在 Python 中,可以簡單地把列表生成式改成 generator,也可以通過函數實現複雜邏輯的 generator。
要理解 generator 的工作原理,它是在 for 循環的過程中不斷計算出下一個元素,並在適當的條件結束 for 循環。對於函數改成的 generator 來說,遇到 return 語句或者執行到函數體最後一行語句,就是結束 generator 的指令,for 循環隨之結束。
請注意區分普通函數和 generator 函數,普通函數調用直接返回結果:
>>> r = abs(6)
>>> r
6
generator 函數的調用實際返回一個 generator 對象:
>>> g = fib(6)
>>> g
<generator object fib at 0x1022ef948>
迭代器
已經知道,可以直接作用於 for 循環的數據類型有以下幾種:
一類是集合數據類型,如 list、tuple、dict、set、str 等;
一類是generator,包括生成器和帶 yield 的 generator function。
這些可以直接作用於 for 循環的對象統稱為可迭代對象:Iterable。
可以使用isinstance() 判斷一個對象是否是 Iterable 對象:
>>> from collections.abc import Iterable
>>> isinstance([], Iterable)
True
>>> isinstance({}, Iterable)
True
>>> isinstance('abc', Iterable)
True
>>> isinstance((x for x in range(10)), Iterable)
True
>>> isinstance(100, Iterable)
False
而生成器不但可以作用於 for 循環,還可以被 next () 函數不斷調用並返回下一個值,直到最後拋出 StopIteration 錯誤表示無法繼續返回下一個值了。
可以被 next () 函數調用並不斷返回下一個值的對象稱為迭代器:Iterator。
可以使用isinstance() 判斷一個對象是否是 Iterator 對象:
>>> from collections.abc import Iterator
>>> isinstance((x for x in range(10)), Iterator)
True
>>> isinstance([], Iterator)
False
>>> isinstance({}, Iterator)
False
>>> isinstance('abc', Iterator)
False
生成器都是 Iterator 對象,但 list、dict、str 雖然是 Iterable 卻不是 Iterator。
把 list、dict、str 等 Iterable 變成 Iterator 可以使用iter() 函數:
>>> isinstance(iter([]), Iterator)
True
>>> isinstance(iter('abc'), Iterator)
True
你可能會問,為什麼 list、dict、str 等數據類型不是 Iterator?
這是因為 Python 的 Iterator 對象表示的是一個數據流,Iterator 對象可以被 next () 函數調用並不斷返回下一個數據,直到沒有數據時拋出 StopIteration 錯誤。可以把這個數據流看做是一個有序序列,但我們卻不能提前知道序列的長度,只能不斷通過 next () 函數實現按需計算下一個數據,所以 Iterator 的計算是惰性的,只有在需要返回下一個數據時它才會計算。
Iterator 甚至可以表示一個無限大的數據流,例如全體自然數。而使用 list 是永遠不可能存儲全體自然數的。
小結
1 凡是可作用於 for 循環的對象都是 Iterable 類型;
2 凡是可作用於 next () 函數的對象都是 Iterator 類型,它們表示一個惰性計算的序列;
3 集合數據類型如 list、dict、str 等是 Iterable 但不是 Iterator,不過可以通過 iter () 函數獲得一個 Iterator 對象。
Python 的 for 循環本質上就是通過不斷調用 next () 函數實現的,例如:
for x in [1, 2, 3, 4, 5]:
pass
實際上完全等價於:
# 首先獲得Iterator對象:
it = iter([1, 2, 3, 4, 5])
# 循環:
while True:
try:
# 獲得下一個值:
x = next(it)
except StopIteration:
# 遇到StopIteration就退出循環
break