Python入門學習---第三天
函式
函式
基本上所有的高階語言都支援函式,Python也不例外。Python不但能非常靈活地定義函式,而且本身內建了很多有用的函式,可以直接呼叫。
抽象是數學中非常常見的概念。寫計算機程式也是一樣,函式就是最基本的一種程式碼抽象的方式。
呼叫函式
Python內建了很多有用的函式,我們可以直接呼叫(這也正是吸引眾多程式設計師來學習python的原因)。
例如:abs函式,可以在互動式命令列通過help(abs)檢視abs函式的幫助資訊。
>>> abs(100)
100
>>> abs(-20)
20
>>> abs (12.34)
12.34
# 呼叫函式的時候,如果傳入的引數數量不對,會報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)
# 而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內建的hex()函式把一個整數轉換成十六進位制表示的字串:
# -*- coding: utf-8 -*-
n1 = 255
n2 = 1000
0xff
0x3e8
呼叫Python的函式,需要根據函式定義,傳入正確的引數。如果函式調用出錯,一定要學會看錯誤資訊,所以英文很重要!
定義函式
在Python中,定義一個函式要使用def語句,依次寫出函式名、括號、括號中的引數和冒號 : ,然後,在縮排塊中編寫函式體,函式的返回值用return語句返回。
# -*- coding: utf-8 -*-
# 我們以自定義一個求絕對值的my_abs函式為例:
def my_abs(x):
if x >= 0:
return x
else:
return -x
如果你已經把 my_abs() 的函式定義儲存為abstest.py檔案了,那麼,可以在該檔案的當前目錄下啟動Python直譯器,用 from abstest import my_abs 來匯入 my_abs() 函式,注意abstest是檔名(不含.py副檔名):
如下:
>>> from abstest import my_abs
>>> my_abs(-9)
9
import的用法在後續模組一節中會詳細介紹。
空函式
如果想定義一個什麼事也不做的空函式,可以用 pass語句:
def nop():
pass
pass語句什麼都不做,那有什麼用?實際上pass可以用來作為佔位符,比如現在還沒想好怎麼寫函式的程式碼,就可以先放一個pass,讓程式碼能執行起來。
pass還可以用在其他語句裡,比如:
# 缺少了pass,程式碼執行就會有語法錯誤。
if age >= 18:
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,但寫起來更方便。
小結
- 定義函式時,需要確定函式名和引數個數;
- 如果有必要,可以先對引數的資料型別做檢查;
- 函式體內部可以用return隨時返回函式結果;
- 函式執行完畢也沒有return語句時,自動return None。
- 函式可以同時返回多個值,但其實就是一個tuple。
練習
請定義一個函式quadratic(a, b, c),接收3個引數,返回一元二次方程:
的兩個解。# 提示:計算平方根可以呼叫math.sqrt()函式:
# -*- coding: utf-8 -*-
import math
def quadratic(a, b, c):
x1 = (-b + math.sqrt(b*b-4*a*c))/(2*a)
x2 = (-b - math.sqrt(b*b-4*a*c))/(2*a)
return x1,x2
print('quadratic(2, 3, 1) =', quadratic(2, 3, 1))
print('quadratic(1, 3, -4) =', quadratic(1, 3, -4))
if quadratic(2, 3, 1) != (-0.5, -1.0):
print('測試失敗')
elif quadratic(1, 3, -4) != (1.0, -4.0):
print('測試失敗')
else:
print('測試成功')
函式的引數
遞迴函式
在函式內部,可以呼叫其他函式。如果一個函式在內部呼叫自身本身,這個函式就是遞迴函式。
舉個例子,我們來計算 階乘n! = 1 x 2 x 3 x … x n ,用函式fact(n)表示,可以看出:
def fact(n):
if n==1:
return 1
return n * fact(n - 1)
>>> fact(1)
1
>>> fact(5)
120
>>> fact(100)
93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000
使用遞迴函式需要注意防止棧溢位。在計算機中,函式呼叫是通過棧(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
解決遞迴呼叫棧溢位的方法是通過尾遞迴優化,事實上尾遞迴和迴圈的效果是一樣的,所以,把迴圈看成是一種特殊的尾遞迴函式也是可以的。
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)
我們來看看遞迴跟尾遞迴的區別:
遞迴過程:
===> 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
每當進入一個函式呼叫,棧就會加一層棧幀,每當函式返回,棧就會減一層棧幀。
尾遞迴過程:
===> fact_iter(5, 1)
===> fact_iter(4, 5)
===> fact_iter(3, 20)
===> fact_iter(2, 60)
===> fact_iter(1, 120)
===> 120
可以看到,return fact_iter(num - 1, num * product)僅返回遞迴函式本身,num - 1和num * product在函式呼叫前就會被計算,不影響函式呼叫。
尾遞迴呼叫時,如果做了優化,棧不會增長,因此,無論多少次呼叫也不會導致棧溢位。
遺憾的是,大多數程式語言沒有針對尾遞迴做優化,Python直譯器也沒有做優化,所以,即使把上面的fact(n)函式改成尾遞迴方式,也會導致棧溢位。
小結
使用遞迴函式的優點是邏輯簡單清晰,缺點是過深的呼叫會導致棧溢位。
針對尾遞迴優化的語言可以通過尾遞迴防止棧溢位。尾遞迴事實上和迴圈是等價的,沒有迴圈語句的程式語言只能通過尾遞迴實現迴圈。
Python標準的直譯器沒有針對尾遞迴做優化,任何遞迴函式都存在棧溢位的問題。
練習
遞迴函式
閱讀: 387346
在函式內部,可以呼叫其他函式。如果一個函式在內部呼叫自身本身,這個函式就是遞迴函式。
舉個例子,我們來計算階乘n! = 1 x 2 x 3 x … x n,用函式fact(n)表示,可以看出:
fact(n) = n! = 1 x 2 x 3 x … x (n-1) x n = (n-1)! x n = fact(n-1) x 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(100)
93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000
如果我們計算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)函式改成尾遞迴方式,也會導致棧溢位。
小結
使用遞迴函式的優點是邏輯簡單清晰,缺點是過深的呼叫會導致棧溢位。
針對尾遞迴優化的語言可以通過尾遞迴防止棧溢位。尾遞迴事實上和迴圈是等價的,沒有迴圈語句的程式語言只能通過尾遞迴實現迴圈。
Python標準的直譯器沒有針對尾遞迴做優化,任何遞迴函式都存在棧溢位的問題。
練習
漢諾塔的移動可以用遞迴函式非常簡單地實現。
請編寫move(n, a, b, c)函式,它接收引數n,表示3個柱子A、B、C中第1個柱子A的盤子數量,然後打印出把所有盤子從A藉助B移動到C的方法,例如:
# -*- coding: utf-8 -*-
def move(n, a, b, c):
if n == 1: # 如果a只有1盤子
print(a, '-->', c); # 直接把盤子從a移到c
else: # 如果a有n個盤子(n > 1),那麼分三步
move(n-1, a, c, b) # 先把上面n-1個盤子,藉助c,從a移到b
move(1, a, b, c) # 再把最下面的1個盤子,藉助b,從a移到c
move(n-1, b, a, c) # 最後把n-1個盤子,藉助a,從b移到c
move(4,'A','B','C') # 測試