Python函數(shù)

函數(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ù)是如何定義的。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • 第5章 函數(shù)和函數(shù)式編程 5.1 引言函數(shù)是組織好的,可重復使用的,用來實現(xiàn)單一,或相關聯(lián)功能的代碼段。函數(shù)...
    VIVAFT閱讀 1,086評論 0 5
  • 一、位置參數(shù) 例如我們寫一個計算x2的函數(shù): 對于power(x)函數(shù),參數(shù)x就是一個位置參數(shù)。當我們調用powe...
    劉光軍_MVP閱讀 364評論 0 1
  • 當火塘里的的火第N次熄火的時候,我終于頹然地發(fā)現(xiàn),我把自己弄得灰頭土臉,卻不能,在煙熏火燎的廚房里,生出一簇火焰...
    夜里飛行的貓閱讀 343評論 0 0
  • 在你不知道因為什么而焦急時, 那就選擇一件事做起來! 不知你有沒有這樣的狀態(tài),沒有任何原因,心中會莫名的空洞,忽然...
    愛微笑閱讀 456評論 0 0

友情鏈接更多精彩內容