5. 函數
Python 不僅能非常靈活地定義函數,而且本身內置了很多有用的函數,可以直接調用。
函數是最基本的一種代碼抽象的方式。
5.1 調用函數
內置函數的官方文檔地址:https://docs.python.org/3/library/functions.html。
abs()
函數的調用
# 調用 abs() 函數, 返回一個指定值:整數或浮點數的絕對值
print(abs(100))
print(abs(-20))
print(abs(12.34))
print(abs(-56.789))
# 錯誤演示一:
# print(abs(1, 2)) # 報錯:參數個數不對
'''
Traceback (most recent call last):
File "func_call.py", line 8, in <module>
print(abs(1, 2))
TypeError: abs() takes exactly one argument (2 given)
'''
# 錯誤演示二:
# print(abs('hello')) # 報錯:錯誤的參數類型
'''
Traceback (most recent call last):
File "func_call.py", line 17, in <module>
print(abs('hello'))
TypeError: bad operand type for abs(): 'str'
'''
max()
函數的調用
# 調用 max() 函數,它可以接收任意多個參數,返回最大的那個
print(max(1, 2, 3)) # 3
print(max(1, 0, -3, 7, 9, 12)) # 12
數據類型轉換函數的調用
# 把 str 轉換成整數
print(int('123')) # 123
# 把浮點數轉換成整數
print(int(12.34)) # 12
# 把 str 轉換成浮點數
print(float('12.34')) # 12.34
# 把浮點數轉換成 str
print(str(12.3)) # 12.3
# 把整數轉換成 str
print(str(1234)) # 1234
# 把 str 轉成布爾
print(bool('')) # False
print(bool('a')) # True
之前我們接觸的 chr()
函數,ord()
函數,len()
函數,range()
函數, set()
函數都屬於內置的函數。如果不記得,可以去複習一下。
函數名其實就是指向一個函數對象的引用,完全可以把函數名賦值給一個變量,相當於給這個函數起了一個“別名”。
上面這句話需要好好理解一下。
a = abs # 變量 a 指向 abs 函數
print(a(-1)) # 1,這裏是通過 a 調用 abs 函數
5.2 定義函數
在 Python 中,定義一個函數要使用 def
語句,依次寫出函數名、括號、括號中的參數(如果有參數的話)和冒號:
,然後在縮進代碼塊中編寫函數體,函數的返回值用 return
語句返回。
# 自定義一個求絕對值的my_abs函數
def my_abs(x):
if x >= 0:
return x
else:
return -x
print(my_abs(-99)) # 99
把 func_def.py 文件中定義的 my_abs()
函數導入 Python 交互式模式,需要在 func_def.py 所在的目錄,打開 Python 交互式模式,輸入 from func_def import my_abs
並回車就可以了。
>>> from func_def import my_abs
>>> my_abs(-9)
9
可以在 Python 交互式模式下直接定義 my_abs()
函數:
>>> def my_abs(x):
... if x >= 0:
... return x
... else:
... return -x
...
>>> my_abs(-10)
10
需要注意的是,函數定義結束後需要按兩次回車重新回到>>>
提示符。
函數執行完畢也沒有return
語句時,自動return None
的代碼演示:
# 沒有返回值的函數
def show1(x):
print(x)
def show2(x):
print(x)
return
def show3(x):
print(x)
return None
show1("no return")
show2("no return")
show3("no return")
空函數
如果想定義一個什麼都不做的空函數,要用到 pass
語句:
# 定義一個空函數
def nop():
pass
pass
是起佔位符的作用,這能保證代碼正常運行起來。如果沒有 pass
,那麼會報錯:SyntaxError: unexpected EOF while parsing。
pass
語句還可以用在其他語句裏。
對自定義的 my_abs() 函數的優化
# print(my_abs('A')) # 報錯
'''
報錯信息:
Traceback (most recent call last):
File "func_def.py", line 14, in <module>
print(my_abs('A'))
File "func_def.py", line 3, in my_abs
if x >= 0:
TypeError: '>=' not supported between instances of 'str' and 'int'
'''
# 而使用內置函數 abs()
# print(abs('A')) # 報錯
'''
報錯信息:
Traceback (most recent call last):
File "func_def.py", line 25, in <module>
print(abs('A'))
TypeError: bad operand type for abs(): 'str'
'''
# 可以看到,我們自定義的 my_abs() 函數,和內置函數 abs() 報錯信息不一樣:
# 自定義的不能檢查出參數錯誤,內置的可以檢查出參數錯誤。
# 優化自定義的 my_abs() 函數,對參數類型做檢查,只允許整數和浮點數的參數
def my_abs2(x):
if not isinstance(x, (int, float)):
raise TypeError('bad operand type for my_abs2(): %s' % type(x).__name__)
if x >= 0:
return x
else:
return -x
print(my_abs2(-1)) # 1
# print(my_abs2('a'))
isinstance(object, classinfo)
函數也是一個內置函數,classinfo
可以是給定的類型,也可以類型的元組。
返回多個值
在 Python 中,函數可以返回多個值。事實上,Python 的函數返回多個值就是返回一個 tuple
。在語法上,返回一個 tuple
,可以省略括號。
import math
def move(x, y, step, angle = 0):
nx = x + step * math.cos(angle)
ny = y + step * math.sin(angle)
return nx, ny
x, y = move(100, 100, 60, math.pi / 6)
print(x, y) # 151.96152422706632 130.0
# 但實際上,Python 函數返回的仍然是單一值
r = move(100, 100, 60, math.pi / 6)
print(r) # (151.96152422706632, 130.0)
# 可以看到,返回值實際上是一個 tuple
5.3 函數的參數
位置參數
位置參數,有時也稱必備參數,指的是必須按照正確的順序將實際參數傳到函數中,換句話說,調用函數時傳入實際參數的數量和位置都必須和定義函數時保持一致。
# 寫一個計算 x^2 的函數
# def power(x):
# return x * x
# print(power(5))
# print(power(15))
# 寫一個計算 x^n 的函數
def power(x, n):
s = 1
while n > 0:
n -= 1
s *= x
return s
print(power(5, 2))
print(power(15, 2))
print(power(2, 10))
上面參數列表中的 x 和 n,就是位置參數。它們都是必須的,順序和個數必須與函數定義時一致。
但是,對於原來的 2 次方寫法,power(x, n) 卻多出了一個位置參數,這樣導致不得不修改之前的代碼,怎麼辦呢?我們需要默認參數出場了。
默認參數
def power(x, n = 2):
s = 1
while n > 0:
n -= 1
s *= x
return s
print(power(5))
print(power(15))
print(power(2, 10))
第二個參數的默認值被設置爲 2。
默認參數可以簡化函數的調用。
設置默認參數時,需要注意:
- 必選參數在前,默認參數在後,否則 Python 的解釋器會報錯;
- 當函數有多個參數時,把變化大的參數放前面,變化小的參數放後面。變化小的參數就可以作爲默認參數。
當有多個默認參數時,當按順序提供默認參數時,可以省略參數名;當不按順序提供部分默認參數時,就必須把參數名寫上。
默認參數必須指向不可變對象!
def add_end(L = []):
L.append('END')
return L
print(add_end([1, 2, 3])) # [1, 2, 3, 'END']
print(add_end(['x', 'y', 'z'])) # ['x', 'y', 'z', 'END']
print(add_end()) # ['END']
print(add_end()) # ['END', 'END']
print(add_end()) # ['END', 'END', 'END']
# 描述一下,出現的大坑:第一次調用 add_end() 的輸出是 ok 的,但是第二次,第三次的輸出卻是不對的。
# 原因是:Python 函數在定義的時候,默認參數 L 的值就被計算出來了,即 [], 因爲默認參數 L 也是一個變量,
# 它指向對象 [],每次調用該函數,如果改變了 L 的內容,則下次調用時,默認參數的內容就變了,不再是函數定義時的 [] 了。
# 解決辦法:默認參數必須指向不變對象!
def add_end2(L = None):
if L is None:
L = []
L.append('END')
return L
print(add_end2()) # ['END']
print(add_end2()) # ['END']
print(add_end2()) # ['END']
可變參數
可變參數就是說傳入的參數個數是可變的,可以是 1 個,2 個或者任意個,還可以是 0 個。
# 給定一組數字a,b,c……,請計算a^2 + b^2 + c^2 + ……
# 這種寫法需要傳入一個 list 或者 tuple
def calc(numbers):
sum = 0
for n in numbers:
sum = sum + n ** 2
return sum
print(calc([1, 2, 3])) # 14
# 使用可變參數
def calc2(*numbers):
sum = 0
for n in numbers:
sum = sum + n ** 2
return sum
print(calc2(1, 2, 3)) # 14
t = (1, 3, 5, 7)
print(calc2(*t)) # 84
可以看到,定義可變參數,僅僅是在參數加了一個 *
號而已。
如果目前有一個 list
或者 tuple
,該怎樣調用可變參數呢?使用 *t
把 t
這個 tuple 的所有元素作爲可變參數傳進去。
關鍵字參數
可變參數允許傳入 0 個或者任意個參數,這些可變參數在函數調用時自動組裝爲一個 tuple
;
而關鍵字參數允許傳入 0 個或者任意個含參數名的參數,這些關鍵字參數在函數內部自動組裝爲一個 dict
。
def person(name, age, **kw):
# print(isinstance(kw, dict)) # True
print('name:', name, 'age:', age, 'other:', kw)
# 可以不傳遞關鍵字參數
person('Michael', 30) # name: Michael age: 30 other: {}
# 傳遞關鍵字參數
person('Bob', 35, city='Beijing') # name: Bob age: 35 other: {'city': 'Beijing'}
# 直接傳一個 dict
extra = {'city' : 'Beijing', 'job' : 'Engineer'}
person('Jack', 24, city=extra['city'], job=extra['job']) # name: Jack age: 24 other: {'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
。
命名關鍵字參數
如果要限制關鍵字參數的名字,就可以用命名關鍵字參數。
命名關鍵字參數必須傳入參數名。
# 使用命名關鍵字參數: 命名關鍵字參數需要一個特殊分隔符*,*後面的參數被視爲命名關鍵字參數
def person2(name, age, *, city, job):
print(name, age, city, job)
person2('Jack', 24, city='Beijing', job='Engineer') # Jack 24 Beijing Engineer
# 如果函數定義中已經有了一個可變參數,後面跟着的命名關鍵字參數就不再需要一個特殊分隔符*了
def person3(name, age, *args, city, job):
print(name, age, args, city, job)
person3('zhichao',32, *[1, 2, 3], city='shanghai', job='Engineer') # zhichao 32 (1, 2, 3) shanghai Engineer
# 命名關鍵字參數必須傳入參數名
# person2('Jack', 24, 'Beijing', 'Engineer') # 報錯: 命名關鍵字參數不傳參數名,就被當成了位置參數。
'''
Traceback (most recent call last):
File "func_args.py", line 110, in <module>
person2('Jack', 24, 'Beijing', 'Engineer')
TypeError: person2() takes 2 positional arguments but 4 were given
'''
# 命名關鍵字參數可以有缺省值,從而簡化調用
def person4(name, age, *, city='Beijing', job):
print(name, age, city, job)
# 由於命名關鍵字參數有默認值,調用時,可以不傳入 city 參數
person4('Bill', 32, job='Driver') # Bill 32 Beijing Driver
參數組合
在 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)
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=9, ext=None) # a= 1 b= 2 c= 0 d= 9 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.4 遞歸函數
如果一個函數在內部調用自身,那麼這個函數就是遞歸函數。
使用遞歸函數要防止棧溢出。
棧知識點補充:
在計算機中,函數調用是通過棧(stack)這種數據結構實現的,每當進入一個函數調用,棧就會加一層棧幀,每當函數返回,棧就會減一層棧幀。但是,棧的大小不是無限的。
所以,遞歸調用的次數過多,會導致棧溢出。
可以看這段演示代碼:
# 計算階乘n! = 1 x 2 x 3 x ... x n,用函數fact(n)表示
def fact(n):
if n == 1:
return 1
return n * fact(n - 1)
print(fact(1))
print(fact(5))
print(fact(100))
# print(fact(1000)) # 報錯
'''
Traceback (most recent call last):
File "func_recur.py", line 10, in <module>
print(fact(1000))
File "func_recur.py", line 5, in fact
return n * fact(n - 1)
File "func_recur.py", line 5, in fact
return n * fact(n - 1)
File "func_recur.py", line 5, in fact
return n * fact(n - 1)
[Previous line repeated 995 more times]
File "func_recur.py", line 3, in fact
if n == 1:
RecursionError: maximum recursion depth exceeded in comparison
解決遞歸調用棧溢出的方法是通過尾遞歸優化。尾遞歸就是從最後開始計算,每遞歸一次就算出相應的結果。
尾調用很重要的特性就是它可以不在調用棧上面添加一個新的堆棧幀,而是更新它。
# 尾調用的代碼: def foo(data): return b(data) def b(data): pass
若一個函數在尾位置調用本身(或是一個尾調用本身的其他函數等),則稱這種情況爲尾遞歸,是遞歸的一種特殊情形。而形式上只要是最後一個
return
語句返回的是一個完整函數,它就是尾遞歸。這裏注意:尾調用不一定是遞歸調用,但是尾遞歸一定是尾調用。
修改爲尾遞歸的形式:
# 使用尾遞歸
def fact2(n):
return fact_iter(n, 1)
def fact_iter(num, product):
if num == 1:
return product
return fact_iter(num - 1, num * product) # 這裏僅返回遞歸函數本身
# fact2(1000) # 報錯
'''
Traceback (most recent call last):
File "func_recur.py", line 35, in <module>
fact2(1000)
File "func_recur.py", line 28, in fact2
return fact_iter(n, 1)
File "func_recur.py", line 33, in fact_iter
return fact_iter(num - 1, num * product) # 這裏僅返回遞歸函數本身
File "func_recur.py", line 33, in fact_iter
return fact_iter(num - 1, num * product) # 這裏僅返回遞歸函數本身
File "func_recur.py", line 33, in fact_iter
return fact_iter(num - 1, num * product) # 這裏僅返回遞歸函數本身
[Previous line repeated 994 more times]
File "func_recur.py", line 31, in fact_iter
if num == 1:
RecursionError: maximum recursion depth exceeded in comparison
'''
但是,發現依然會出現棧溢出的錯誤。這是因爲大多數編程語言沒有針對尾遞歸做優化,Python 解釋器也沒有做優化。