第5章 函數和函數式編程
5.1 引言
函數是組織好的,可重復使用的,用來實現單一,或相關聯功能的代碼段。函數能提高應用的模塊性,和代碼的重復利用率。在Python中有很多內建函數,比如print()。當然隨著學習的深入,你也可以學會創建對自己有用的函數(用戶自定義函數)。簡單的理解下函數的概念,就是用戶編寫了一些語句,為了方便使用這些語句,把這些語句組合在一起,給它起一個名字。使用的時候只要調用這個名字,就可以實現語句組的功能了。
在沒用過函數之前,我們要計算一個數的冪時會用到**,方法是這樣的:
>>>2**3
8 #此處為[**python 函數返回值**](http://www.iplaypy.com/jinjie/return.html "python 返回值")
現在知道了函數,就可以用內建函數pow來計算乘方了:
>>>pow(2,3)
8
5.2 調用函數
python系統中自帶的一些函數就叫做內建函數,比如:dir()、type()等等,不需要我們自己編寫。還有一種是第三方函數,就是其它程序員編好的一些函數,共享給大家使用。這兩種函數都是拿來就可以直接使用的。最后就是我們自己編些的方便自己工作學習用的函數,就叫做自定義函數了。
Python內置了很多有用的函數,我們可以直接調用。要調用一個函數,需要知道函數的名稱和參數,比如求絕對值的函數abs
,只有一個參數。可以直接從Python的官方網站查看文檔:
http://docs.python.org/3/library/functions.html#abs
也可以在交互式命令行通過help(abs)
查看abs
函數的幫助信息。
調用abs
函數:
>>> abs(100)
100
>>> abs(-10)
10
比如max函數可以接收任意多個參數,并返回最大的那個:
>>> max(1, 2)
2
>>> max(-2, 0, 1, 4)
4
同時在調用函數的時候,需要注意傳入的參數數量是否符合要求。例如當調用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'
數據類型轉換
Python內置的常用函數還包括數據類型轉換函數,比如int()函數可以把其他數據類型轉換為整數:
>>> int('12')
12
>>> int(12.3)
12
>>> float('12.3')
12.3
>>> str(1.23)
'1.23'
>>> str(10)
'10'
>>> bool(1)
True
>>> bool('')
False
也可以把函數名賦給一個變量,相當于給這個函數起了一個“別名”:
>>> a = abs # 變量a指向abs函數
>>> a(-1) # 通過a調用abs函數
1
定義一個函數:給了函數一個名稱,指定了函數里包含的參數,和代碼塊結構。
這個函數的基本結構完成以后,用戶可以通過另一個函數調用執行,也可以直接從 Python 命令提示符執行。
如下實例調用了 printme() 函數:
#!/usr/bin/python3
# 定義函數
def printme( str ):
"打印傳入的字符串"
print (str);
return;
# 調用函數
printme("調用用戶自定義函數!");
printme("再次調用同一函數");
調用用戶自定義函數!
再次調用同一函數
小結
調用Python的函數,需要根據函數定義,傳入正確的參數。
如果函數調用出錯,一定要注意看錯誤信息!
5.3 定義函數
在Python中,定義一個具有特定功能的函數需要符合一定規則:
- 函數代碼塊以 def 關鍵詞開頭,后接函數標識符名稱和圓括號 ()。
- 任何傳入參數和自變量必須放在圓括號中間,圓括號之間可以用于定義參數。
- 函數的第一行語句可以選擇性地使用文檔字符串—用于存放函數說明。
函數內容以冒號起始,并且縮進。 - return [表達式] 結束函數,選擇性地返回一個值給調用方。不帶表達式的return相當于返回 None。
一般格式如下:
def 函數名(參數列表):
函數體
默認情況下,參數值和參數名稱是按函數聲明中定義的的順序匹配起來的。
我們以自定義一個輸出"Hello World!"的函數為例:
>>> def hello() :
print("Hello World!")
>>> hello()
Hello World!
在更復雜的應用中,函數中帶上參數變量:
#!/usr/bin/python3
# 計算面積函數
def area(width, height):
return width * height
def print_welcome(name):
print("Welcome", name)
print_welcome("Runoob")
w = 4
h = 5
print("width =", w, " height =", h, " area =", area(w, h))
Welcome Runoob
width = 4 height = 5 area = 20
注意:函數體內部語句執行時,一旦執行到return時,函數就執行完畢,并將結果返回。因此,函數內部通過條件判斷和循環可以實現非常復雜的邏輯。
注意:如果沒有return語句,函數執行完畢后也會返回結果,只是結果為None。(return None可以簡寫為return)
如果用戶已經把hello()
的函數定義保存為test.py
文件了,那么,可以在該文件的當前目錄下啟動Python解釋器,用from test import hello
來導入hello()
函數,注意test
是文件名(不含.py
擴展名):
>>> from test import hello
>>> hello()
Hello World!
import
的用法在后續“模塊”一節中會詳細介紹。
空函數
如果想定義一個什么事也不做的空函數,可以用pass語句:
def nop():
pass
pass可以用來作為占位符,比如現在還沒想好怎么寫函數的代碼,就可以先放一個pass,讓代碼能運行起來。
參數傳遞
在 python 中,類型屬于對象,變量是沒有類型的:
a=[1,2,3]
a="Runoob"
以上代碼中,[1,2,3] 是 List 類型,"Runoob" 是 String 類型,而變量 a 是沒有類型,她僅僅是一個對象的引用(一個指針),可以是指向 List 類型對象,也可以是指向 String 類型對象。
可更改(mutable)與不可更改(immutable)對象
在 python 中,strings, tuples, 和 numbers 是不可更改的對象,而 list,dict 等則是可以修改的對象。
- 不可變類型:變量賦值 a=5 后再賦值 a=10,這里實際是新生成一個 int 值對象 10,再讓 a 指向它,而 5 被丟棄,不是改變a的值,相當于新生成了a。
- 可變類型:變量賦值 la=[1,2,3,4] 后再賦值 la[2]=5 則是將 list la 的第三個元素值更改,本身la沒有動,只是其內部的一部分值被修改了。
python 函數的參數傳遞:
- 不可變類型:類似 c++ 的值傳遞,如 整數、字符串、元組。如fun(a),傳遞的只是a的值,沒有影響a對象本身。比如在 fun(a)內部修改 a 的值,只是修改另一個復制的對象,不會影響 a 本身。
- 可變類型:類似 c++ 的引用傳遞,如 列表,字典。如 fun(la),則是將 la 真正的傳過去,修改后fun外部的la也會受影響。
python 中一切都是對象,嚴格意義我們不能說值傳遞還是引用傳遞,我們應該說傳不可變對象和傳可變對象。
python 傳不可變對象實例
#!/usr/bin/python3
def ChangeInt( a ):
a = 10
b = 2
ChangeInt(b)
print( b ) # 結果是 2
實例中有 int 對象 2,指向它的變量是 b,在傳遞給 ChangeInt 函數時,按傳值的方式復制了變量 b,a 和 b 都指向了同一個 Int 對象,在 a=10 時,則新生成一個 int 值對象 10,并讓 a 指向它。
傳可變對象實例
可變對象在函數里修改了參數,那么在調用這個函數的函數里,原始的參數也被改變了。例如:
#!/usr/bin/python3
# 可寫函數說明
def changeme( mylist ):
"修改傳入的列表"
mylist.append([1,2,3,4]);
print ("函數內取值: ", mylist)
return
# 調用changeme函數
mylist = [10,20,30];
changeme( mylist );
print ("函數外取值: ", mylist)
傳入函數的和在末尾添加新內容的對象用的是同一個引用。故輸出結果如下:
函數內取值: [10, 20, 30, [1, 2, 3, 4]]
函數外取值: [10, 20, 30, [1, 2, 3, 4]]
小結
定義函數時,需要確定函數名和參數個數;
如果有必要,可以先對參數的數據類型做檢查;
函數體內部可以用return隨時返回函數結果;
函數執行完畢也沒有return語句時,自動return None。
5.4 函數的參數
在定義函數時,我們把參數的名字和位置確定后,函數的接口定義就完成了。對于函數的調用者來說,只需知道如何傳遞正確的參數,以及函數的返回值即可,函數內部被封裝起來,調用者無需了解。
Python的函數定義非常簡單,但靈活度卻非常大。除了正常定義的必選參數外,還可以使用默認參數、可變參數和關鍵字參數,使得函數定義出來的接口,不但能處理復雜的參數,還可以簡化調用者的代碼。
鑒于函數定義中可能包含多個形參,因此函數調用中也可能包含多個實參。想函數傳遞實參的方式很多,可使用位置實參,和要求實參的順序相同;也可使用關鍵字實參,其中每個實參都由變量名和值組成,等等。以下是調用函數時可使用的正式參數類型:
位置參數
5.4.1位置實參
你調用函數時,Python必須將函數調用中的每個實參都關聯到函數定義的一個形參。為此,最簡單的關聯方式是基于實參的順序,這種關聯方式被稱為位置實參,可以多次調用。
為明白其中的工作原理,來看一個顯示學生信息的函數。這個函數指出一名學生的名字以及年齡,如下所示:
? def describe_student(person_name, student_age):
"""顯示學生的信息"""
print("\nMy name is " + person_name + ".")
print(person_name + " is " + student_age+ " years old.")
? describe_student('Jack', '18')
這個函數的定義表明,它需要一名學生的姓名和一個年齡數字(見?)。調用describe_student() 時,需要按順序提供一名學生的姓名和一個年齡數字。例如,在前面的函數調用中,實參'Jack' 存儲在形參person_name中,而實參'18' 存儲在形參student_age 中(見?)。在函數體內,使用了這兩個形參來顯示學生的信息。輸出描述了一名18歲的學生Jack:
My name is Jack.
Jack is 18 years old.
- 調用函數多次
用戶可以根據需要調用函數任意次。要再描述一名學生,只需再次調用describe_student() 即可:
def describe_student(person_name, student_age):
"""顯示學生的信息"""
print("\nMy name is " + person_name + ".")
print(person_name + " is " + student_age+ " years old.")
describe_student('Jack', '18')
describe_student('Bob', '17')
第二次調用describe_student() 函數時,我們向它傳遞了實參'Bob' 和'17' 。與第一次調用時一樣,Python將實參'Bob' 關聯到形參person_name,并將實參'17' 關聯到形參student_age 。與前面一樣,這個函數完成其任務,但打印的是一名17歲的學生Bob的信息。至此,我們描述了一名18歲的學生Jack和17歲的學生Bob.
My name is Jack.
Jack is 18 years old.
My name is Bob.
Jack is 17 years old.
調用函數多次是一種效率極高的工作方式。我們只需在函數中編寫描述學生的代碼一次,然后每當需要描述新學生時,都可調用這個函數,并向它提供新學生的信息。即便描述全校的學生,用戶依然只需使用一行調用函數的代碼,就可描述一名新學生。
在函數中,可根據需要使用任意數量的位置實參,Python將按順序將函數調用中的實參關聯到函數定義中相應的形參。
- 位置實參的順序很重要
使用位置實參來調用函數時,如果實參的順序不正確,結果可能出乎意料:
def describe_student(person_name, student_age):
"""顯示學生的信息"""
print("\nMy name is " + person_name + ".")
print(person_name + " is " + student_age+ " years old.")
describe_student('18', 'Jack')
在這個函數調用中,我們先指定名字,再指定學生年齡。由于實參'18' 在前,這個值將存儲到形參person_name中;同理,'Jack' 將存儲到形參student_age中。結果是我們得到了一名年齡為Jack的18:
My name is 18.
Jack is Jack years old.
如果結果像上面一樣荒謬,請確認函數調用中實參的順序與函數定義中形參的順序一致。
5.4.2 默認參數
編寫函數時,可給每個形參指定默認值 。在調用函數中給形參提供了實參時,Python將使用指定的實參值;否則,將使用形參的默認值。因此,給形參指定默認值后,可在函數調用中省略相應的實參。使用默認值可簡化函數調用,還可清楚地指出函數的典型用法。
例如describe_student函數定義沒有問題,但若如下調用會出現錯誤:
>>> describe_student('Jack')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: describe_student() missing 1 required positional argument: 'student_age'
Python的錯誤信息很明確:調用函數describe_student()缺少了一個位置參數student_age。這個時候,默認參數就排上用場了。若大部分學生的年齡為18歲,我們可以把第二個參數student_age的默認值設定為18:
def describe_student(person_name, student_age=18):
"""顯示學生的信息"""
print("\nMy name is " + person_name + ".")
print(person_name + " is " + student_age+ " years old.")
這樣,當我們調用describe_student(Jack)時,相當于調用describe_student(Jack,18):
>>> describe_student('Jack')
My name is Jack.
Jack is 18 years old.
>>> describe_student('Jack','18')
My name is Jack.
Jack is 18 years old.
而對于student > 18的其他情況,就必須明確地傳入student_age,比如describe_student(Herbie,19)。
從上面的例子可以看出,默認參數可以簡化函數的調用。
注意:
設置默認參數時,必選參數在前,默認參數在后,否則Python的解釋器會報錯(思考一下為什么默認參數不能放在必選參數前面)
當函數有多個參數時,把變化大的參數放前面,變化小的參數放后面。變化小的參數就可以作為默認參數。
使用默認參數有什么好處?最大的好處是能降低調用函數的難度。
舉個例子,我們編寫一個學生注冊的函數,需要傳入name和gender兩個參數:
def enroll(name, gender):
"""注冊學生的信息"""
print("name: ",name)
print("gender: ",gender)
這樣,調用enroll()函數只需要傳入兩個參數:
>>> enroll('Jack', 'F')
name: Jack
gender: F
如果要繼續傳入年齡、城市等信息怎么辦?這樣會使得調用函數的復雜度大大增加。
我們可以把年齡和城市設為默認參數:
def enroll(name, gender,age=18, city='Beijing'):
print('name: ', name)
print('gender: ', gender)
print('age: ', age)
print('city:', city)
這樣,大多數學生注冊時不需要填寫年齡和城市,只提供必須的兩個參數:
>>> enroll('Sarah', 'F')
name: Sarah
gender: F
age: 18
city: Beijing
只有與默認參數不符的學生才需要提供額外的信息:
enroll('Bob', 'M', 17)
enroll('Adam', 'M', city='Tianjin')
可見,默認參數降低了函數調用的難度,而一旦需要更復雜的調用時,又可以傳遞更多的參數來實現。無論是簡單調用還是復雜調用,函數只需要定義一個。
有多個默認參數時,調用的時候,既可以按順序提供默認參數,比如調用enroll('Bob', 'M', 17),意思是,除了name,gender這兩個參數外,最后一個參數應用在參數age上,city參數由于沒有提供,仍然使用默認值。也可以不按順序提供部分默認參數。當不按順序提供部分默認參數時,需要把參數名寫上。比如調用enroll('Adam', 'M', city='Tianjin'),意思是,city參數用傳進去的值,其他默認參數繼續使用默認值。
默認參數很有用,但定義默認參數時要牢記一點:默認參數必須指向不變對象!否則會出現重大錯誤,例如:
先定義一個函數,傳入一個list,添加一個END再返回:
def test_add(H=[]):
H.append('END')
return H
當正常調用時,結果似乎不錯:
>>> test_add([1, 2, 3])
[1, 2, 3, 'END']
>>> add_end(['a', 'b', 'c'])
['a', 'b', 'c', 'END']
當初次使用默認參數調用時,結果也是對的:
>>> test_add()
['END']
但是當再次調用test_add()時,結果就出現錯誤了:
>>> test_add()
['END', 'END']
>>> test_add()
['END', 'END', 'END']
我們會發現,默認參數是[],但是函數test_add()似乎每次都“記住了”上次添加了'END'后的list。這是為什么呢?原因如下:
Python函數在定義的時候,默認參數H的值就被計算出來了,即[],因為默認參數H也是一個變量,它指向對象[],每次調用該函數,如果改變了H的內容,則下次調用時,默認參數的內容就變了,不再是函數定義時的[]了。
注意:定義默認參數時,默認參數必須指向不變對象!
我們可以用None這個不變對象來解決此問題:
def test_add(H=None):
if H is None:
H = []
H.append('END')
return H
現在,無論調用多少次,都不會有問題:
>>> test_add()
['END']
>>> test_add()
['END']
為什么要設計str、None這樣的不變對象呢?因為不變對象一旦創建,對象內部的數據就不能修改,這樣就減少了由于修改數據導致的錯誤。此外,由于對象不變,多任務環境下同時讀取對象不需要加鎖,同時讀一點問題都沒有。我們在編寫程序時,如果可以設計一個不變對象,那就盡量設計成不變對象。
5.4.3 不定長參數
在Python函數中,還可以定義不定長參數,也叫可變參數。顧名思義,不定長參數就是傳入的參數個數是可變的。
我們以數學題為例子,給定一組數字a,b,c……,請計算a+b+c+ ……要定義出這個函數,我們必須確定輸入的參數。由于參數個數不確定,我們首先想到可以把a,b,c……作為一個list或tuple傳進來,這樣,函數可以定義如下:
def calc(numbers):
sum = 0
for n in numbers:
sum = sum + n
return sum
但是調用的時候,需要先組裝出一個list或tuple:
>>> calc([1, 2, 3])
6
>>> calc((1, 2, 3, 4))
10
但若把函數的參數改為不定長參數:
def calc(*numbers):
sum = 0
for n in numbers:
sum = sum + n
return sum
則調用函數的方式可以簡化成這樣:
>>> calc(1, 2, 3)
6
>>> calc(1, 2, 3, 4)
10
定義不定長參數和定義一個list或tuple參數相比,僅僅在參數前面加了一個*號。在函數內部,參數numbers接收到的是一個tuple,因此,函數代碼完全不變。但是,調用該函數時,可以傳入任意個參數,包括0個參數:
>>> calc()
0
如果已經有一個list或者tuple,要調用一個不定長參數可如下:
>>> nums = [1, 2, 3]
>>> calc(*nums)
6
*nums表示把nums這個list的所有元素作為可變參數傳進去。這種寫法相當有用,而且很常見。
5.4.4 關鍵字參數
關鍵字實參是傳遞給函數的名稱—值對。你直接在實參中將名稱和值關聯起來了,因此向函數傳遞實參時不會混淆(不會得到名為18的Jack這樣的結果)。關鍵字實參讓用戶無需考慮函數調用中的實參順序,還清楚地指出了函數調用中各個值的用途。
下面重新編寫describe_student()函數,在其中使用關鍵字實參來調用describe_student():
def describe_student(person_name, student_age):
"""顯示學生的信息"""
print("\nMy name is " + person_name + ".")
print(person_name + " is " + student_age+ " years old.")
describe_student(person_name='Jack', student_age='18')
函數describe_pet() 還是原來那樣,但調用這個函數時,我們向Python明確地指出了各個實參對應的形參。看到這個函數調用時,Python知道應該將實參'Jack' 和'18' 分別存儲在形參person_name和 student_age中。輸出正確無誤,它指出我們有一名名叫Jack,年齡為18歲的學生。關鍵字實參的順序無關緊要,因為Python知道各個值該存儲到哪個形參中。下面兩個函數調用是等效的:
describe_pet(animal_type='hamster', pet_name='harry')
describe_pet(pet_name='harry', animal_type='hamster')
注意:使用關鍵字實參時,務必準確地指定函數定義中的形參名。
不定長參數允許傳入0個或任意個參數,這些不定長參數在函數調用時自動組裝為一個元組(tuple)。而關鍵字參數允許你傳入0個或任意個含參數名的參數,這些關鍵字參數在函數內部自動組裝為一個字典(dict)。如下,函數person除了必選參數name和age外,還接受關鍵字參數kw。在調用該函數時,可以只傳入必選參數:
def enroll(name, age,**kw)):
print('name: ', name, 'age: ', age, 'other: ', kw)
>>> enroll('Michael', 18)
name: Michael age: 18 other: {}
也可以傳入任意個數的關鍵字參數:
>>> enroll('Bob', 17, city='Beijing')
name: Bob age: 17 other: {'city': 'Beijing'}
>>> enroll('Adam', 19, gender='M', job='Engineer')
name: Adam age: 19 other: {'gender': 'M', 'job': 'Engineer'}
關鍵字參數有擴展函數的功能。比如,在enroll函數里,我們保證能接收到name和age這兩個參數,但是,如果調用者愿意提供更多的參數,我們也能收到。試想你正在做一個用戶注冊的功能,除了用戶名和年齡是必填項外,其他都是可選項,利用關鍵字參數來定義這個函數就能滿足注冊的需求。
和不定長參數類似,也可以先組裝出一個dict,然后,把該dict轉換為關鍵字參數傳進去:
>>> extra = {'gender':'M','city': 'Beijing'}
>>> enroll('Jack', 18, gender=extra['M'], city=extra['city'])
name: Jack age: 18 other: {'gender':'M','city': 'Beijing'}
當然,上面復雜的調用可以用簡化的寫法:
>>> extra = {'gender':'M','city': 'Beijing'}
>>> enroll('Jack', 18, **extra)
name: Jack age: 18 other: {'gender':'M','city': 'Beijing'}
注意:**extra
表示把extra這個dict的所有key-value用關鍵字參數傳入到函數的**kw參數,kw將獲得一個dict,注意kw獲得的dict是extra的一份拷貝,對kw的改動不會影響到函數外的extra。
5.4.5 命名關鍵字參數
如果要限制關鍵字參數的名字,就可以用命名關鍵字參數。
和關鍵字參數**kw
不同,如果沒有可變參數,命名關鍵字參數就必須加一個*
作為特殊分隔符。如果缺少*
,Python解釋器將無法識別位置參數和命名關鍵字參數。例如,若只接收age和city作為關鍵字參數。這種方式定義的函數如下:
def enroll(name, gender, *, age, city):
print(name, gender, age, city)
>>> enroll('Jack', ' M', age='18', city='Beijing')
Jack M 18 Beijing
如果函數定義中已經有了一個可變參數,后面跟著的命名關鍵字參數就不再需要一個特殊分隔符*了:
def enroll(name, gender, *grade, age, city):
print(name, gender, grader, age, city)
注意:和位置參數不同,命名關鍵字參數必須傳入參數名。
如果沒有傳入參數名,調用將報錯:
>>> enroll(' Jack',' M',' 18',' Beijing')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: enroll() takes 2 positional arguments but 4 were given
由報錯信息可知,由于調用時缺少參數名age和city,Python解釋器把這4個參數均視為位置參數,但enrroll()函數僅接受2個位置參數。
注意:命名關鍵字參數可以有缺省值。
由于命名關鍵字參數city具有默認值,調用時可不傳入city參數:
def enroll(name, gender, *, age='18', city):
print(name, gender, age, city)
>>> enroll('Jack','M',city='Beijing')
Jack M 18 Beijing
5.4.6 參數組合
現在我們知道python定義函數的參數類型有:
必選參數、默認參數、可變參數、關鍵字參數和命名關鍵字參數
在Python中定義函數,我們是可以組合使用這些參數的。但要注意的是,參數定義是有順序的,定義的順序必須是:必選參數、默認參數、可變參數、命名關鍵字參數和關鍵字參數。
比如定義一個函數,包含上述若干種參數:
def func(a, b, c=0, *args, **kw):
print('a =', a, 'b =', b, 'c =', c, 'args =', args, 'kw =', kw)
在函數調用的時候,Python解釋器自動按照參數位置和參數名把對應的參數傳進去。
>>> func(1, 2)
a = 1 b = 2 c = 0 args = () kw = {}
>>> func(1, 2, c=3)
a = 1 b = 2 c = 3 args = () kw = {}
>>> func(1, 2, 3, 'a', 'b')
a = 1 b = 2 c = 3 args = ('a', 'b') kw = {}
>>> func(1, 2, 3, 'a', 'b', x=4)
a = 1 b = 2 c = 3 args = ('a', 'b') kw = {'x': 4}
另外對于任意函數,都可以通過類似 func(*args, **kw) 的形式調用它,無論它的參數是如何定義的。以一個元組(tuple)和字典(dict)為例:
>>> args = (1, 2, 3, 4)
>>> kw = {'x': 5}
>>> func(*args, **kw)
a = 1 b = 2 c = 3 args = (4,) kw = {'x': 5}
小結
Python的函數具有非常靈活的參數形態,既可以實現簡單的調用,又可以傳入非常復雜的參數。
默認參數一定要用不可變對象,如果是可變對象,運行會有邏輯錯誤!
要注意定義可變參數和關鍵字參數的語法:
args 是可變參數,args接收的是一個tuple;
kw 是關鍵字參數,kw接收的是一個dict。
以及調用函數時如何傳入可變參數和關鍵字參數的語法:
可變參數既可以直接傳入: 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 返回值
函數并非總是直接顯示輸出,相反,它可以處理一些數據,并返回一個或一組值。函數返回的值被稱為返回值 。在函數中,可使用return 語句將值返回到調用函數的代碼行。返回值讓你能夠將程序的大部分繁重工作移到函數中去完成,從而簡化主程序。
return [表達式] 語句用于退出函數,選擇性地向調用方返回一個表達式。不帶參數值的return語句返回None。以下實例演示了 return 語句的用法:
#!/usr/bin/python3
def sum( arg1, arg2 ):
total = arg1 + arg2 # 返回2個參數的和
print ("函數內 : ", total)
return total;
total = sum( 10, 20 );
print ("函數外 : ", total)
函數內 : 30
函數外 : 30
5.7 函數式編程
函數式編程是一種編程范式,我們常見的編程范式有命令式編程、函數式編程、邏輯式編程,常見的面向對象編程也是一種命令式編程。命令式編程是面向計算機硬件的抽象,在計算機的層次上,CPU執行的是加減乘除的指令代碼,以及各種條件判斷和跳轉指令。而函數式編程是面向數學的抽象,將計算描述為一種表達式求值,越是抽象的計算,離計算機硬件越遠。
值得注意的是,函數式編程中的“函數”不是指計算機中的函數,而是指數學中的函數,即自變量的映射,一個函數的值僅決定于函數參數的值,不依賴其它狀態。在編程式語言中,“函數”可以在任何地方定義,在函數內或函數外,可以作為函數的參數或返回值,可以對函數進行組合。純函數式編程中的變量也不是命令式編程語言中的變量,而是代數中的變量,即一個值的名稱,變量的值是不可變的,比如命令式編程中的"x = x+1"這種依賴可變狀態的事實此時被認為為假。
例:通過別的名稱使用函數,再把函數作為參數傳遞
>>> fact = factorial
>>> fact
<function factorial at >
>>> func(*args, **kw)
a = 1 b = 2 c = 3 args = (4,) kw = {'x': 5}
函數式編程最主要的好處主要是不可變性帶來的,函數是“引用透明”的,并可避免“副作用”,它不會依賴也不會改變當前函數以外的數據。
高階函數
接受函數為參數,或者把函數作為結果返回的函數是高階函數(higher-
order function)。map 函數就是一例,此外,內置函數 sorted 也是:可選的 key 參數用于提供一個函數,它會應用到各個元素上進行排序.
例,若想根據單詞的長度排序,只需把 len 函數傳給 key 參數,如下:
根據單詞長度給一個列表排序
>>> fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
>>> sorted(fruits, key=len)
['fig', 'apple', 'cherry', 'banana', 'raspberry', 'strawberry']
任何單參數函數都能作為 key 參數的值。例如,為了創建押韻詞典,可以把各個單詞反過來拼寫,然后排序。注意,示例 5-4 中列表里的單詞沒有變,我們只是把反向拼寫當作排序條件,因此各種漿果(berry)都排在一起。
示例:根據反向拼寫給一個單詞列表排序
>>> def reverse(word):
... return word[::-1]
>>> reverse('testing')
'gnitset'
>>> sorted(fruits, key=reverse)
['banana', 'apple', 'fig', 'raspberry', 'strawberry', 'cherry']
在函數式編程范式中,最為人熟知的高階函數有map、filter、reduce 和 apply。apply 函數在 Python 2.3 中標記為過時,在 Python 3 中移除了,因為不再需要它了。如果想使用不定量的參數調用函數,可以編寫 fn(*args, **keywords),不用再編寫 apply(fn, args, kwargs)。
5.5 匿名函數
所謂匿名,意即不再使用 def 語句這樣標準的形式定義一個函數。python 使用 lambda 來創建匿名函數。
lambda 只是一個表達式,函數體比 def 簡單很多。lambda 函數的語法如下:
lambda [arg1 [,arg2,.....argn]]:expression
lambda的主體是一個表達式,而不是一個代碼塊,僅能在lambda表達式中封裝有限的邏輯進去。
注意:lambda 函數擁有自己的命名空間,且不能訪問自己參數列表之外或全局命名空間里的參數。
注意:雖然lambda函數看起來只能寫一行,卻不等同于C或C++的內聯函數,后者的目的是調用小函數時不占用棧內存從而增加運行效率。
如下實例:
#!/usr/bin/python3
sum = lambda arg1, arg2: arg1 + arg2;
# 調用sum函數
print ("相加后的值為 : ", sum( 1, 2 ))
print ("相加后的值為 : ", sum( 2, 2 ))
相加后的值為 : 3
相加后的值為 : 4
變量作用域
Python 中,程序的變量并不是在哪個位置都可以訪問的,訪問權限決定于這個變量是在哪里賦值的。
變量的作用域決定了在哪一部分程序可以訪問哪個特定的變量名稱。Python的作用域一共有4種,分別是:
L (Local) 局部作用域
E (Enclosing) 閉包函數外的函數中
G (Global) 全局作用域
B (Built-in) 內建作用域
以 L –> E –> G –>B 的規則查找,即:在局部找不到,便會去局部外的局部找(例如閉包),再找不到就會去全局找,再者去內建中找。
x = int(2.9) # 內建作用域
g_count = 0 # 全局作用域
def outer():
o_count = 1 # 閉包函數外的函數中
def inner():
i_count = 2 # 局部作用域
Python 中只有模塊(module),類(class)以及函數(def、lambda)才會引入新的作用域,其它的代碼塊(如 if/elif/else/、try/except、for/while等)是不會引入新的作用域的,也就是說這這些語句內定義的變量,外部也可以訪問,如下代碼:
>>> if True:
... msg = 'I am from Runoob'
...
>>> msg
'I am from Runoob'
實例中 msg 變量定義在 if 語句塊中,但外部還是可以訪問的。
如果將 msg 定義在函數中,則它就是局部變量,外部不能訪問:
>>> def test():
... msg_inner = 'I am from Runoob'
...
>>> msg_inner
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'msg_inner' is not defined
從報錯的信息上看,說明了 msg_inner 未定義,無法使用,因為它是局部變量,只有在函數內可以使用。
全局變量和局部變量
定義在函數內部的變量擁有一個局部作用域,定義在函數外的擁有全局作用域。
局部變量只能在其被聲明的函數內部訪問,而全局變量可以在整個程序范圍內訪問。調用函數時,所有在函數內聲明的變量名稱都將被加入到作用域中。如下實例:
#!/usr/bin/python3
total = 0; # 這是一個全局變量
# 可寫函數說明
def sum( arg1, arg2 ):
#返回2個參數的和."
total = arg1 + arg2; # total在這里是局部變量.
print ("函數內是局部變量 : ", total)
return total;
#調用sum函數
sum( 10, 20 );
print ("函數外是全局變量 : ", total)
函數內是局部變量 : 30
函數外是全局變量 : 0
global 和 nonlocal關鍵字
當內部作用域想修改外部作用域的變量時,就要用到global和nonlocal關鍵字了。
以下實例修改全局變量 num:
#!/usr/bin/python3
num = 1
def fun1():
global num # 需要使用 global 關鍵字聲明
print(num)
num = 123
print(num)
fun1()
1
123
如果要修改嵌套作用域(enclosing 作用域,外層非全局作用域)中的變量則需要 nonlocal 關鍵字了,如下實例:
#!/usr/bin/python3
def outer():
num = 10
def inner():
nonlocal num # nonlocal關鍵字聲明
num = 100
print(num)
inner()
print(num)
outer()
100
100
另外有一種特殊情況,假設下面這段代碼被運行:
#!/usr/bin/python3
a = 10
def test():
a = a + 1
print(a)
test()
以上程序執行,報錯信息如下:
Traceback (most recent call last):
File "test.py", line 7, in <module>
test()
File "test.py", line 5, in test
a = a + 1
UnboundLocalError: local variable 'a' referenced before assignment
錯誤信息為局部作用域引用錯誤,因為 test 函數中的 a 使用的是局部,未定義,無法修改。
全局變量和局部變量
定義在函數內部的變量擁有一個局部作用域,定義在函數外的擁有全局作用域。
局部變量只能在其被聲明的函數內部訪問,而全局變量可以在整個程序范圍內訪問。調用函數時,所有在函數內聲明的變量名稱都將被加入到作用域中。如下實例:
#!/usr/bin/python3
total = 0 # 這是一個全局變量
# 可寫函數說明
def sum( arg1, arg2 ):
#返回2個參數的和."
total = arg1 + arg2 # total在這里是局部變量.
print ("函數內是局部變量 : ", total)
return total
#調用sum函數
sum( 10, 20 )
print ("函數外是全局變量 : ", total)
以上實例輸出結果:
函數內是局部變量 : 30
函數外是全局變量 : 0