函數(shù)的參數(shù)
在 C/C++ 中,傳值和傳引用是函數(shù)參數(shù)傳遞的兩種方式,學 Python 時,有人喜歡生搬硬套地問類似的問題:“Python 函數(shù)中,參數(shù)是傳值,還是傳引用?”。
為了把這個問題弄清楚,先了解 Python 中變量與對象之間的關系。
變量與對象
Python 中一切皆為對象,數(shù)字是對象,列表是對象,函數(shù)也是對象,任何東西都是對象。
而變量是對象的一個引用(又稱為名字或者標簽),對象的操作都是通過引用來完成的。例如,[]是一個空列表對象,變量 a 是該對象的一個引用。
a = []
a.append(1)
在 Python 中,「變量」更準確叫法是「名字」,賦值操作 = 就是把一個名字綁定到一個對象上。就像給對象添加一個標簽。
a = 1
整數(shù) 1 賦值給變量 a 就相當于是在整數(shù)1上綁定了一個 a 標簽。
a = 2
整數(shù) 2 賦值給變量 a,相當于把原來整數(shù) 1 身上的 a 標簽撕掉,貼到整數(shù) 2 身上。
b = a
把變量 a 賦值給另外一個變量 b,相當于在對象 2 上貼了 a,b 兩個標簽,通過這兩個變量都可以對對象 2 進行操作。
變量本身沒有類型信息,類型信息存儲在對象中,這和C/C++中的變量有非常大的出入(C中的變量是一段內存區(qū)域)
函數(shù)參數(shù)
Python 函數(shù)中,參數(shù)的傳遞本質上是一種賦值操作,而賦值操作是一種名字到對象的綁定過程,清楚了賦值和參數(shù)傳遞的本質之后,現(xiàn)在再來分析兩段代碼。
代碼段一:
def foo(arg):
arg = 2
print(arg)
a = 1
foo(a) # 輸出:2
print(a) # 輸出:1

在代碼段1中,變量 a 綁定了 1,調用函數(shù) foo(a) 時,相當于給參數(shù) arg 賦值 arg=1,這時兩個變量都綁定了 1。在函數(shù)里面 arg 重新賦值為 2 之后,相當于把 1 上的 arg 標簽撕掉,貼到 2 身上,而 1 上的另外一個標簽 a 一直存在。因此 print(a) 還是 1。
代碼段二:
def bar(args):
args.append(1)
b = []
print(b)# 輸出:[]
print(id(b)) # 輸出:4324106952
bar(b)
print(b) ?!≥敵觯篬1]
print(id(b)) # 輸出:4324106952

執(zhí)行 append 方法前 b 和 arg 都指向(綁定)同一個對象,執(zhí)行 append 方法時,并沒有重新賦值操作,也就沒有新的綁定過程,append 方法只是對列表對象插入一個元素,對象還是那個對象,只是對象里面的內容變了。
因為 b 和 arg 都是綁定在同一個對象上,執(zhí)行 b.append 或者 arg.append 方法本質上都是對同一個對象進行操作,因此 b 的內容在調用函數(shù)后發(fā)生了變化(但id沒有變,還是原來那個對象)
最后,回到問題本身,究竟是是傳值還是傳引用呢?說傳值或者傳引用都不準確。非要安一個確切的叫法的話,叫傳對象(call by object)。如果作為面試官,非要考察候選人對 Python 函數(shù)參數(shù)傳遞掌握與否,與其討論字面上的意思,還不如來點實際代碼。
代碼段三:
def bad_append(new_item, a_list=[]):
a_list.append(new_item)
return a_list
這段代碼是初學者最容易犯的錯誤,用可變(mutable)對象作為參數(shù)的默認值。
函數(shù)定義好之后,默認參數(shù) a_list 就會指向(綁定)到一個空列表對象,每次調用函數(shù)時,都是對同一個對象進行 append 操作。因此這樣寫就會有潛在的bug,同樣的調用方式返回了不一樣的結果。
>>> print(bad_append('one'))
['one']
>>> print(bad_append('one'))
['one', 'one']

而正確的方式是,把參數(shù)默認值指定為None
def good_append(new_item, a_list=None):
if a_list is None:
a_list = []
a_list.append(new_item)
return a_list

函數(shù)
調用函數(shù)
Python內置了很多有用的函數(shù),我們可以直接調用。
要調用一個函數(shù),需要知道函數(shù)的名稱和參數(shù),比如求絕對值的函數(shù)abs,只有一個參數(shù)。
可以直接從Python的官方網站查看文檔。
也可以在交互式命令行通過help(abs)查看abs函數(shù)的幫助信息。
調用函數(shù)的時候,如果傳入的參數(shù)數(shù)量不對,會報TypeError的錯誤,并且Python會明確地告訴你:abs()有且僅有1個參數(shù),但給出了兩個。
>>> abs(1, 2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: abs() takes exactly one argument (2 given)
如果傳入的參數(shù)數(shù)量是對的,但參數(shù)類型不能被函數(shù)所接受,也會報TypeError的錯誤,并且給出錯誤信息:str是錯誤的參數(shù)類型:
>>> abs('a')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: bad operand type for abs(): 'str'
Python內置的常用函數(shù)還包括數(shù)據類型轉換函數(shù),比如int()函數(shù)可以把其他數(shù)據類型轉換為整數(shù)。
函數(shù)名其實就是指向一個函數(shù)對象的引用,完全可以把函數(shù)名賦給一個變量,相當于給這個函數(shù)起了一個“別名”:
>>> a = abs # 變量a指向abs函數(shù)
>>> a(-1) # 所以也可以通過a調用abs函數(shù)
1
定義函數(shù)
定義函數(shù)
在Python中,定義一個函數(shù)要使用def語句,依次寫出函數(shù)名、括號、括號中的參數(shù)和冒號:,然后,在縮進塊中編寫函數(shù)體,函數(shù)的返回值用return語句返回。
函數(shù)體內部的語句在執(zhí)行時,一旦執(zhí)行到return時,函數(shù)就執(zhí)行完畢,并將結果返回。因此,函數(shù)內部通過條件判斷和循環(huán)可以實現(xiàn)非常復雜的邏輯。
如果沒有return語句,函數(shù)執(zhí)行完畢后也會返回結果,只是結果為None。return None可以簡寫為return。
在Python交互環(huán)境中定義函數(shù)時,注意Python會出現(xiàn)...的提示。函數(shù)定義結束后需要按兩次回車重新回到>>>提示符下。
>>> def my_abs(x):
... if x >= 0:
... return x
... else:
... return -x
...
>>> my_abs(-9)
9
>>> _
如果你已經把my_abs()的函數(shù)定義保存為abstest.py文件了,那么,可以在該文件的當前目錄下啟動Python解釋器,用from abstest import my_abs來導入my_abs()函數(shù),注意abstest是文件名(不含.py擴展名)。
>>> from abstest import my_abs
>>> my_abs(-9)
9
>>> _
空函數(shù)
如果想定義一個什么事也不做的空函數(shù),可以用pass語句:
def nop():
pass
實際上pass可以用來作為占位符,比如現(xiàn)在還沒想好怎么寫函數(shù)的代碼,就可以先放一個pass,讓代碼能運行起來。
pass還可以用在其他語句里,比如:
if age >= 18:
pass
缺少了pass,代碼運行就會有語法錯誤。
參數(shù)檢查
調用函數(shù)時,如果參數(shù)個數(shù)不對,Python解釋器會自動檢查出來,并拋出TypeError。但是如果參數(shù)類型不對,Python解釋器就無法幫我們檢查。
數(shù)據類型檢查可以用內置函數(shù)isinstance()實現(xiàn)。
def my_abs(x):
if not isinstance(x, (int, float)):
raise TypeError('bad operand type')
if x >= 0:
return x
else:
return -x
添加了參數(shù)檢查后,如果傳入錯誤的參數(shù)類型,函數(shù)就可以拋出一個錯誤:
>>> 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
返回多個值
返回值是一個tuple(元組)!但是,在語法上,返回一個tuple可以省略括號,而多個變量可以同時接收一個tuple,按位置賦給對應的值,所以,Python的函數(shù)返回多值其實就是返回一個tuple,但寫起來更方便。
函數(shù)的參數(shù)
位置參數(shù)
對于power(x)函數(shù),參數(shù)x就是一個位置參數(shù)。當我們調用power函數(shù)時,必須傳入有且僅有的一個參數(shù)x。
power(x, n)函數(shù)有兩個參數(shù):x和n,這兩個參數(shù)都是位置參數(shù),調用函數(shù)時,傳入的兩個值按照位置順序依次賦給參數(shù)x和n。
默認參數(shù)
新的power(x, n)函數(shù)定義沒有問題,但是,舊的調用代碼失敗了,原因是我們增加了一個參數(shù),導致舊的代碼因為缺少一個參數(shù)而無法正常調用。
這個時候,默認參數(shù)就排上用場了。由于我們經常計算x2,所以,完全可以把第二個參數(shù)n的默認值設定為2:def power(x, n=2):
這樣,當我們調用power(5)時,相當于調用power(5, 2)
設置默認參數(shù)時,有幾點要注意:
- 一是必選參數(shù)在前,默認參數(shù)在后,否則Python的解釋器會報錯
- 二是如何設置默認參數(shù)。函數(shù)有多個參數(shù)時,把變化大的參數(shù)放前面,變化小的參數(shù)放后面。變化小的參數(shù)就可以作為默認參數(shù)。
默認參數(shù)降低了函數(shù)調用的難度,而一旦需要更復雜的調用時,又可以傳遞更多的參數(shù)來實現(xiàn)。無論是簡單調用還是復雜調用,函數(shù)只需要定義一個。
Python函數(shù)在定義的時候,默認參數(shù)L的值就被計算出來了,即[],因為默認參數(shù)L也是一個變量,它指向對象[],每次調用該函數(shù),如果改變了L的內容,則下次調用時,默認參數(shù)的內容就變了,不再是函數(shù)定義時的[]了。
def add_end(L=[]):
L.append('END')
return L
定義默認參數(shù)要牢記一點:默認參數(shù)必須指向不變對象!
def add_end(L=None):
if L is None:
L = []
L.append('END')
return L
#無論調用多少次,都不會有問題
為什么要設計str、None這樣的不變對象呢?因為不變對象一旦創(chuàng)建,對象內部的數(shù)據就不能修改,這樣就減少了由于修改數(shù)據導致的錯誤。此外,由于對象不變,多任務環(huán)境下同時讀取對象不需要加鎖,同時讀一點問題都沒有。我們在編寫程序時,如果可以設計一個不變對象,那就盡量設計成不變對象。
可變參數(shù)
可變參數(shù)就是傳入的參數(shù)個數(shù)是可變的。
給定一組數(shù)字a,b,c……,請計算a2 + b2 + c2 + ……。
要定義出這個函數(shù),我們必須確定輸入的參數(shù)。由于參數(shù)個數(shù)不確定,我們首先想到可以把a,b,c……作為一個list或tuple傳進來,這樣,函數(shù)可以定義如下:
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
我們把函數(shù)的參數(shù)改為可變參數(shù):
def calc(*numbers):
sum = 0
for n in numbers:
sum = sum + n * n
return sum
調用函數(shù)的方式可以簡化成這樣:
>>> calc(1, 2, 3)
14
>>> calc(1, 3, 5, 7)
84
定義可變參數(shù)和定義一個list或tuple參數(shù)相比,僅僅在參數(shù)前面加了一個*號。在函數(shù)內部,參數(shù)numbers接收到的是一個tuple,因此,函數(shù)代碼完全不變。但是,調用該函數(shù)時,可以傳入任意個參數(shù),包括0個參數(shù)。
如果已經有一個list或者tuple,要調用一個可變參數(shù),在list或tuple前面加一個*號,把list或tuple的元素變成可變參數(shù)傳進去。
>>> nums = [1, 2, 3]
>>> calc(*nums)
14
# *nums表示把nums這個list的所有元素作為可變參數(shù)傳進去。
可變參數(shù)允許你傳入0個或任意個參數(shù),這些可變參數(shù)在函數(shù)調用時自動組裝為一個tuple。
關鍵字參數(shù)
關鍵字參數(shù)允許你傳入0個或任意個含參數(shù)名的參數(shù),這些關鍵字參數(shù)在函數(shù)內部自動組裝為一個dict。
def person(name, age, **kw):
print('name:', name, 'age:', age, 'other:', kw)
函數(shù)person除了必選參數(shù)name和age外,還接受關鍵字參數(shù)kw。在調用該函數(shù)時,可以只傳入必選參數(shù):
>>> person('Michael', 30)
name: Michael age: 30 other: {}
也可以傳入任意個數(shù)的關鍵字參數(shù):
>>> 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'}
關鍵字參數(shù)有什么用?它可以擴展函數(shù)的功能。比如,在person函數(shù)里,我們保證能接收到name和age這兩個參數(shù),但是,如果調用者愿意提供更多的參數(shù),我們也能收到。試想你正在做一個用戶注冊的功能,除了用戶名和年齡是必填項外,其他都是可選項,利用關鍵字參數(shù)來定義這個函數(shù)就能滿足注冊的需求。
**extra表示把extra這個dict的所有key-value用關鍵字參數(shù)傳入到函數(shù)的**kw參數(shù),kw將獲得一個dict,注意kw獲得的dict是extra的一份拷貝,對kw的改動不會影響到函數(shù)外的extra。
>>> 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'}
命名關鍵字參數(shù)
對于關鍵字參數(shù),函數(shù)的調用者可以傳入任意不受限制的關鍵字參數(shù)。至于到底傳入了哪些,就需要在函數(shù)內部通過kw檢查。
仍以person()函數(shù)為例,我們希望檢查是否有city和job參數(shù):
def person(name, age, **kw):
if 'city' in kw:
# 有city參數(shù)
pass
if 'job' in kw:
# 有job參數(shù)
pass
print('name:', name, 'age:', age, 'other:', kw)
但是調用者仍可以傳入不受限制的關鍵字參數(shù)。
如果要限制關鍵字參數(shù)的名字,就可以用命名關鍵字參數(shù),例如,只接收city和job作為關鍵字參數(shù)。
def person(name, age, *, city, job):
print(name, age, city, job)
和關鍵字參數(shù)**kw不同,命名關鍵字參數(shù)需要一個特殊分隔符*,*后面的參數(shù)被視為命名關鍵字參數(shù)。
調用方式如下:
def person(name, age, *, city, job):
print(name, age, city, job)
>>> person('Jack', 24, city='Beijing', job='Engineer')
Jack 24 Beijing Engineer
如果函數(shù)定義中已經有了一個可變參數(shù),后面跟著的命名關鍵字參數(shù)就不再需要一個特殊分隔符*了。
def person(name, age, *args, city, job):
print(name, age, args, city, job)
命名關鍵字參數(shù)必須傳入參數(shù)名,這和位置參數(shù)不同。如果沒有傳入參數(shù)名,調用將報錯:
>>> person('Jack', 24, 'Beijing', 'Engineer')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: person() takes 2 positional arguments but 4 were given
由于調用時缺少參數(shù)名city和job,Python解釋器把這4個參數(shù)均視為位置參數(shù),但person()函數(shù)僅接受2個位置參數(shù)。
命名關鍵字參數(shù)可以有缺省值,從而簡化調用:
def person(name, age, *, city='Beijing', job):
print(name, age, city, job)
>>> person('Jack', 24, job='Engineer')
Jack 24 Beijing Engineer
使用命名關鍵字參數(shù)時,要特別注意,如果沒有可變參數(shù),就必須加一個*作為特殊分隔符。如果缺少*,Python解釋器將無法識別位置參數(shù)和命名關鍵字參數(shù)。
參數(shù)組合
在Python中定義函數(shù),可以用必選參數(shù)、默認參數(shù)、可變參數(shù)、關鍵字參數(shù)和命名關鍵字參數(shù),這5種參數(shù)都可以組合使用。
參數(shù)定義的順序必須是:必選參數(shù)、默認參數(shù)、可變參數(shù)、命名關鍵字參數(shù)和關鍵字參數(shù)。
雖然可以組合多達5種參數(shù),但不要同時使用太多的組合,否則函數(shù)接口的可理解性很差。
對于任意函數(shù),都可以通過類似func(*args, **kw)的形式調用它,無論它的參數(shù)是如何定義的。