編程語言中的函數與數學中的函數是有區別的:數學中的函數有參數(輸入),就會有相應的結果(輸出)。編程語言中的函數有輸入,不一定會返回結果。編程語言中的函數其實就是一個用於完成某個特定功能的相關代碼的代碼段 。那麼哪些代碼語句應該被整合到一起定義為一個函數呢?這取決於你想讓這個函數完成的功能是什麼。
為什麼要將這個代碼段定義成一個函數呢?這其實就是函數的作用。假設我們在編寫一個可供用戶選擇的菜單程序,程序啟動時需要打印一遍菜單列表,而且程序運行過程中用戶也可以隨時打印菜單列表,也就是說打印菜單列表的代碼段可能要多次被用到,假設每次打印的菜單列表都是一樣的,而且列表很長,那麼我們是否應該每次在需要打印菜單的時候重復執行相同的代碼呢?那麼當我們需要增加或者減少一個菜單項時怎麼辦呢?顯然我們需要在每個打印菜單的代碼點都進行修改。如果我們把打印菜單的相關代碼拿出來定義為一個函數,又會出現這樣的場景呢?我們只需要在需要打印菜單列表的地方使用這個函數;當需要添加或減少一個菜單項時,只需要修改這個函數中的內容即可,程序的維護和擴展成本大大降低;同時,我們這個程序的代碼會更加簡潔,而且有條理性更加便於閱讀,而不是一坨亂糟糟的讓人看著就想重寫的東西。當然,如果你要打印的是多級菜單,你可以通過函數的參數或全部變量通知該函數要打印的是幾級菜單。總結一下,編程語言中的函數的作用就是實現代碼的可重用性,提高代碼可維護性、擴展性和可讀性。
高級編程語言通常會提供很多內置的函數來屏蔽底層差異,向上暴露一些通用的接口,比如我們之前用到的print()函數和open()函數。除此之外,我們也可以自定義我們需要的函數。由於函數本身也是程序代碼的一部分,因此為了標識出這段代碼是一個函數通常需要使用特定的格式或關鍵字。另外還涉及到參數、方法名稱、返回值等相關問題的約束。
說明: 函數名稱可以使用大寫字母,但是不符合PEP8規范;另外Python3中函數名可以使用中文,但是還是不要給自己找麻煩為好。另外return語句不一定要寫在函數末尾,而可以寫在函數體的任意位置。return語句代表著函數的結束,函數在執行過程中只要遇到return語句,就會停止執行並返回結果。
def 函數名稱( 參數 ):
"""
函數使用說明、參數介紹等文檔信息
"""
代碼塊
return [表達式]
def add(a, b):
"""
計算並返回兩個數的和
a: 被加數
b: 加數
"""
c = a + b
return c
通常寫成這個樣子:
def add(a, b):
"""
計算並返回兩個數的和
a: 被加數
b: 加數
"""
return a + b
Python中函數的調用方式與其他大部分編程語言都一樣(其實我目前使用過的編程語言當中,只有shell是個另類;好吧,其實它只是個腳本語言):函數名(參數)
def add(a, b):
"""
計算並返回兩個數的和
a: 被加數
b: 加數
"""
return a + b
sum = add(1, 9)
先來說下形參和實參的概念:
重點需要說下函數的各種不同種類的參數。函數的參數可以分為以下幾種:
說明: 這裡說的位置參數,其實是指“必選參數”,也就是函數調用時必須要傳遞的參數,而默認參數是一種有默認值的特殊的位置參數。通常情況下位置參數和默認參數的傳遞順序是不能變化的,但是當以指定參數名的方式(如: name='Tom')傳遞時參數位置時可以變化的。
不同編程語言對以上幾種函數參數的支持各不相同,但是位置參數是最基本的參數類型,基本上所有的編程語言都支持。以下是一個常見編程語言的對比表格(Y表示支持,N表示不支持):
可見只有Python支持全部參數類型,而且只有Python支持關鍵字參數;另外,C、Java和Go都不支持默認參數,其中Java和Go與它們支持的方法重載特性有關(具體可以看下這個帖子),並且它們可以通過方法重載實現默認參數的功能。
下面我們以一個自定義的打印函數來對以上各種參數進行說明:
位置參數,顧名思義是和參數的順序位置和數量有關的。函數調用時,實參的位置和個數要與形參對應,不然會報錯。
def my_print(name, age):
print('NAME: %s' % name)
print('AGE: %d' % age)
>>> my_print('Tom', 18)
NAME: Tom
AGE: 18
>>> my_print(18, 'Tom')
NAME: 18
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in my_print
TypeError: %d format: a number is required, not str
>>> my_print('Tom')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: my_print() missing 1 required positional argument: 'age'
默認參數:是指給函數的形參賦一個默認值,它是一個有默認值的位置參數。當調用函數時,如果為該參數傳遞了實參則該形參取實參的值,如果沒有為該參數傳遞實參則該形參取默認值。
默認參數的應用場景:參數值在大部分情況下是固定/相同的。比如這裡打印一個班中學生的姓名和年齡,這個班大部分為同齡人(年齡相同),這時我們就可以給“年齡”這個形參賦一個默認的值。
說明: 默認參數只是一個有默認值的位置參數,因此它還是受到位置參數的限制。默認參數可以避免位置參數的一個限制:傳遞實參的個數,但是參數位置(順序)仍然還是要一一對應。另外,默認參數必須放在位置參數後面(自己想想為什麼)。
def my_print(name, age=12):
print('NAME: %s' % name)
print('AGE: %d' % age)
>>> my_print('Tom', 18)
NAME: Tom
AGE: 18
age取的是函數調用時傳遞過來的實參
>>> my_print('Tom')
NAME: Tom
AGE: 12
函數調用時沒有給形參age傳值,因此age取的是默認值
>>> my_print(18)
NAME: 18
AGE: 12
可見,我們明明是想傳遞18給形參age的,結果18被賦給了name,而age仍然取得是默認值。上面已經提到過,位置參數只是可以讓我們少傳一些參數,但是不能改變參數的位置和順序。另外,這也說明了默認參數為什麼一定要放在後面:因為實參與形參是從前到後一一有序的對應關系,也就是說在給後面參數傳值的時候,不論前面的參數是否有默認值,必須要先給前面的參數先賦值。
>>> my_print('Tom', 18, 'F')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: my_print() takes from 1 to 2 positional arguments but 3 were given
這裡要說明的是:默認參數只能相應的減少實參的個數,但是不能增加實參的個數。這個很容易想明白,不做過多解釋,只是為下面的可變長(參數)做鋪墊。
可變(長)參數:顧名思義,是指長度可以改變的參數。通俗點來講就是,可以傳任意個參數(包括0個)。
可變(長)參數的應用場景:通常在寫一個需要對外提供服務的方法時,為了避免將來添加或減少什麼新的參數使得所有調用該方法的代碼點都要進行修改的情況發生,此時就可以用一個可變長的形式參數。
說明: 默認參數允許我們調用函數時,可以少傳遞一些實參;而可變(長)參數則允許我們調用函數時,可以多傳遞任意個實參。另外,可變長參數應該定義在默認參數之後,因為調用函數時傳遞的實參會按照順序一一賦值給各個形參,如果可變(長)參數定義在前面,那麼後面的參數將永遠無法取得傳遞的值。可變(長)參數名稱通常用args,且參數名稱前要有個"*"號,表示這是一個可變長參數。
def my_print(name, age=12, *args):
print('NAME: %s' % name)
print('AGE: %d' % age)
print(args)
再次強調:位置參數、默認參數、可變長參數在函數定義中的位置不能變。
>>> my_print('Tom')
NAME: Tom
AGE: 12
()
方法調用時,只傳遞了一個實參,該實參會按照函數中參數的定義位置賦值給形參name,因此name的值為‘Tom’;而形參age沒有接收到實參,但是它有默認值,因此它取的是默認值12;需要注意的是可變參數args也沒有接收到傳遞值,但是打印出來的是一對小括號(),說明args參數在函數內部會被轉換成tuple(元祖)類型,當沒有接收到實參時便是一個空tuple。
>>> my_print('Tom', 18)
NAME: Tom
AGE: 18
()
與值傳遞一個實參的情況基本相同,只是默認參數接收到了傳遞值,不再取默認值。
比如,現在需要多接收並打印一個人的性別(F: 表示女,M: 表示男),可以這樣用:
>>> my_print('Tom', 18, 'F')
NAME: Tom
AGE: 18
('F',)
比如,現在需要多接收並打印一個人的性別(F: 表示女,M: 表示男)和籍貫信息,可以這樣用:
>>> my_print('Tom', 18, 'F', 'Hebei')
NAME: Tom
AGE: 18
('F', 'Hebei')
當然,我們也可以直接將一個tuple或list實例傳遞給形參args,但是tuple實例前也要加上*號作為前綴:
>>> t = ('F', 'Hebei')
>>> my_print('Tom', 19, *t)
NAME: Tom
AGE: 19
('F', 'Hebei')
你甚至可以將傳遞給形參name和age的實參也放到要傳遞的tuple實例中,但是最好不要這樣做,因為很容易發生混亂:
>>> t = ('Jerry', 10, 'F', 'Hebei')
>>> my_print(*t)
NAME: Jerry
AGE: 10
('F', 'Hebei')
由於args接收到實參之後會被轉換成一個tuple(元祖)的實例,而tuple本身是一個序列(有序的隊列),因此我們可以通過下標(args[n])來獲取相應的實參。但是我們需要在函數使用文檔中寫明args中各實參的傳遞順序及意義,並且在獲取args中的元素之前應該對args做非空判斷。因此函數的定義及調用結果應該是這樣的:
函數定義:
def my_print(name, age=12, *args):
"""
Usage: my_print(name[, age[, sex[, address]]])
:param name: 姓名
:param age: 年齡
:param args: 性別、籍貫
:return: None
"""
print('NAME: %s' % name)
print('AGE: %d' % age)
if len(args) >= 1:
print('SEX: %s' % args[0])
if len(args) >= 2:
print('ADDRESS: %s' % args[1])
函數調用及結果:
>>> my_print('Tom')
NAME: Tom
AGE: 12
>>> my_print('Tom', 18)
NAME: Tom
AGE: 18
>>> my_print('Tom', 18, 'F')
NAME: Tom
AGE: 18
SEX: F
>>> my_print('Tom', 18, 'F', 'Hebei')
NAME: Tom
AGE: 18
SEX: F
ADDRESS: Hebei
>>> t = ('F', 'Hebei')
>>> my_print('Tom', 19, *t)
NAME: Tom
AGE: 19
SEX: F
ADDRESS: Hebei
關鍵字參數:顧名思義,是指調用函數時通過關鍵字來指定是為哪個形參指定的實參,如name="Tom", age=10。
說明: 這個地方很容易發生思維混淆,所以需要特別說明一下:這裡所說的關鍵字參數可以理解為以key=value的形式傳遞給函數的實參,注意是實參不是函數定義時聲明的形參。而且在函數調用時可以通過關鍵字參數給函數定義時所聲明的位置參數和默認參數傳值(但是不能通過關鍵參數給可變長參數*args傳值)。如果想實現像可變長參數那樣在函數調用時傳遞任意個關鍵字參數給函數,則需要在函數定義時聲明一個接受“可變長關鍵詞參數”的形參,該形參名稱通常為kwargs,且前面需要帶"**"前綴--
**kwargs
。
關鍵字參數應用場景:關鍵字參數一方面可以允許函數調用時傳遞實參的順序與函數定義時聲明形參的順序不一致,提高靈活性;另一方面,它彌補了可變長參數的不足。想一下,如果想為上面定義了可變長參數的函數只傳遞“籍貫”參數就必須同時傳遞“性別”參數;另外還要不斷地判斷tuple的長度,這是相當不方便的。而關鍵參數可以通過關鍵字來判斷某個參數是否有傳遞值並獲取該參數的實參值。
def my_print(name, age=12, *args, **kwargs):
print('NAME: %s' % name)
print('AGE: %d' % age)
print(args)
print(kwargs)
>>> my_print('Tom')
NAME: Tom
AGE: 12
()
{}
方法調用時,只傳遞了一個實參,該實參會按照函數中參數的定義位置賦值給形參name,因此name的值為‘Tom’;而形參age沒有接收到實參,但是它有默認值,因此它取的是默認值12;可變參數args也沒有接收到傳遞值,因此args的值是一個空元組;重點需要注意的是關鍵字參數kwargs也沒有接收到傳遞值,但是其打印值為一個空字典(dict)實例。
>>> my_print('Tom', 18)
NAME: Tom
AGE: 18
()
{}
與值傳遞一個實參的情況基本相同,只是默認參數接收到了傳遞值,不再取默認值。
>>> my_print(age=18, name='Tom')
NAME: Tom
AGE: 18
()
{}
可以不按照形參聲明的順序傳遞實參
以非key=value的形式傳遞所有參數:
>>> my_print('Tom', 18, 'F', 'Hebei')
NAME: Tom
AGE: 18
('F', 'Hebei')
{}
可見後面多余的兩個實參都傳遞給了可變長參數args
最後一個addr參數以key=value的形式傳遞:
>>> my_print('Tom', 18, 'F', addr='Hebei')
NAME: Tom
AGE: 18
('F',)
{'addr': 'Hebei'}
>>>
最後兩個參數sex和addr都以key=value的形式傳遞:
>>> my_print('Tom', 18, sex='F', addr='Hebei')
NAME: Tom
AGE: 18
()
{'sex': 'F', 'addr': 'Hebei'}
由以上兩個示例可見,對於除去傳遞給位置參數和默認參數之外多余的參數,如果是直接以value的形式提供實參,則會被傳遞給可變長參數args而成為一個元組中的元素;如果是以key=value的形式提供實參,則會被傳遞給關鍵字參數kwargs而成為一個字典中的元素。
>>> t=('Jerry', 19, 'F', 'Hebei')
>>> my_print(*t)
NAME: Jerry
AGE: 19
('F', 'Hebei')
{}
>>> d={'name':'Tom', 'age':18, 'sex':'F', 'addr':'Hebei'}
>>> my_print(**d)
NAME: Tom
AGE: 18
()
{'sex': 'F', 'addr': 'Hebei'}
>>> d={'sex':'F', 'addr':'Hebei'}
>>> my_print(age=18, name='Tom', **d)
NAME: Tom
AGE: 18
()
{'sex': 'F', 'addr': 'Hebei'}
>>> t=('Tom', 18, 'abc')
>>> d={'sex':'F', 'addr':'Hebei'}
>>> my_print(*t, **d)
NAME: Tom
AGE: 18
('abc',)
{'sex': 'F', 'addr': 'Hebei'}
>>> my_print(name='Tom', 18, sex='F', addr='Hebei')
File "<stdin>", line 1
SyntaxError: positional argument follows keyword argument
關於Python中的函數參數說了這麼多,我覺得很多必要來個總結:
一個程序中的變量是有作用域的,作用域的大小會限制變量可訪問的范圍。根據作用域范圍的大小不同可以分為:全局變量和局部變量。顧名思義,全局變量表示變量在全局范圍內都可以被訪問,而局部變量只能在一個很小的范圍內生效。這就好比國家主席與各省的省長:在全國范圍內國家主席都是同一個人,因此國家主席就是個全局變量;而各省的省長只能在某個省內生效,河北省省長是一個人,河南省省長又是另外一個人,因此省長就是個局部變量。對於Python編程語言而言,定義在一個函數內部的變量就是一個局部變量,局部變量只能在其被聲明的函數內訪問;定義在函數外部的變量就是全局變量,全局變量可以在整個程序范圍內訪問。
來看個示例:
#!/usr/bin/env python
# -*- encoding:utf-8 -*-
name = 'Tom'
def func1():
age = 10
print(name)
print(age)
def func2():
sex = 'F'
print(name)
print(sex)
print(name)
func1()
func2()
輸出結果:
Tom
Tom
10
Tom
F
上面的示例中,name是一個全局變量,因此它在程序的任何地方都可以被訪問;而func1函數中的age變量和func2函數中的sex變量都是局部變量,因此它們只能在各自定義的函數中被訪問。
#!/usr/bin/env python
# -*- encoding:utf-8 -*-
name = 'Tom'
def func3():
name = 'Jerry'
print(name)
print(name)
func3()
print(name)
輸出結果:
Tom
Jerry
Tom
通過上面兩個示例的輸出結果我們可以得出這樣的結論:
可以在函數內部通過global關鍵字聲明該局部變量就是全局變量:
#!/usr/bin/env python
# -*- encoding:utf-8 -*-
name = 'Tom'
def func4():
global name
name = 'Jerry'
print(name)
print(name)
func4()
print(name)
輸出結果:
Tom
Jerry
Jerry
可見全局name的值的確被func4函數內部的操作改變了。
變量值的改變通常有兩種方式:(1) 重新賦值 (2) 改變原有值。要想在函數內部通過重新賦值來改變全局變量的值,則只能通過上面介紹的使用global關鍵字來完成,通過傳參是無法實現的。而要想在函數內部改變全局變量的原有值的屬性就要看該參數是值傳遞還是引用傳遞了,如果是引用傳遞則可以在函數內部對全局變量的值進行修改,如果是值傳遞則不可以實現。具體請看下面的分析。
函數名也是變量,函數體就是這個變量的值:
calc = lambda x: x*3
這個話題在幾乎所有的編程語言中都會涉及,之所以把它放到最後是因為覺得這個問題對於編程新手來說比較難理解。與 “值傳遞與引用傳遞” 相似的概念是 “值拷貝與引用拷貝”。前者主要是指函數調用時傳遞參數的時候,後者是指把一個變量賦值給其他變量或其他一些專門的拷貝操作(如深拷貝和淺拷貝)的時候。
這裡我們需要先來說明下定義變量的過程是怎樣的。首先,我們應該知道變量的值是保存在內存中的;以name='Tom'為例,定義變量name的過程是這樣的:
也就是說變量保存的不是真實的值,而是存放真實值的內存空間的地址。
“值拷貝”和“值傳遞”比較好理解,就是直接把變量的值在內存中再復制一份;也就是說會分配並占用新的內存空間,因此變量指向的內存空間是新的,與之前的變量及其指向的內存空間沒有什麼關聯了。而“引用拷貝”和“引用傳遞”僅僅是把變量對內存空間地址的引用復制了一份,也就是說兩個變量指向的是同一個內存空間,因此對一個變量的值的修改會影響其他指向這個相同內存空間的變量的值。實際上,向函數傳遞參數時傳遞的也是實參的“值拷貝或引用拷貝”。
因此當我們判斷一個變量是否被修改時,只需要搞明白該變量所指向的內存地址以及該內存地址對應的內存空間中的值是否發生了改變即可。
name1 = 'Tom'
name2 = name1
name2 = 'Jerry'
print('name1: %s' % name1)
print('name2: %s' % name2)
思考:name1被改變了嗎?
分析下上面操作的過程:
name1指向的內存地址發生改變了嗎?-- 沒有,因為name1並沒有被重新進行賦值操作。
name1所指向的內存空間中的內容改變了嗎? -- 沒有,並沒有對它做什麼,並且字符串本就是個常量,是不可能被改變的。
So, 答案已經有了,name1並沒有被改變,因此輸出結果是:
name1: Tom
name2: Jerry
num1 = 10
num2 = num1
num2 += 1
print('num1: %d' % num1)
print('num2: %d' % num2)
與示例1過程相似,只是+=操作也是一個賦值的過程,其他不再做過多解釋。
輸出結果:
num1: 10
num2: 11
list1 = ['Tom', 'Jerry', 'Peter', 'Lily']
list2 = list1
list2.pop(0)
print('list1: %s' % list1)
print('list2: %s' % list2)
思考: list1被改變了嗎?
分析上面操作的過程:
list1指向的內存地址發生改變了嗎?-- 沒有,因為list1並沒有被重新進行賦值操作。
list2所指向的內存空間中的內容改變了嗎? -- 是的,因為list1和list2指向的是同一個內存地址,通過list2修改了該內存地址中的內容後就相當於修改了list1。
So, 答案已經有了,list1被改變了,因此輸出結果是:
list1: ['Jerry', 'Peter', 'Lily']
list2: ['Jerry', 'Peter', 'Lily']
其實函數參數的傳遞過程也是類似的,比如:
num1 = 10
name1 = 'Tom'
list1 = ['Tom', 'Jerry', 'Peter', 'Lily']
def fun1(num2, name2, list2):
num2 += 1
name2 = 'Jerry'
list2.pop(0)
print('num2: %d' % num2)
print('name2: %s' % name2)
print('list2: %s' % list2)
fun1(num1, name1, list1)
print('num1: %d' % num1)
print('name1: %s' % name1)
print('list1: %s' % list1)
為了跟上面的示例做對比,我故意把func1函數中的形參的名稱寫為num2、name2和list2,實際上他們可以為任意有意義的名稱。
輸出結果:
num2: 11
name2: Jerry
list2: ['Jerry', 'Peter', 'Lily']
num1: 10
name1: Tom
list1: ['Jerry', 'Peter', 'Lily']
其實這是相同的問題,因為上面說過了:參數傳遞的過程實際上就像先拷貝,然後將拷貝傳遞給形參。如果是值拷貝,那麼調用函數傳參時就是值傳遞;如果是引用拷貝,那麼調用函數傳參時就是引用(內存地址)傳遞。其實通過上面的示例,我們大概可以猜測到對於列表類型的變量貌似是引用傳遞,但是數字和字符串類型的變量是值傳遞還是引用傳遞呢?Python中的參數的傳遞都是引用傳遞,關於這個問題我們可以通過Python內置的一個id()函數來進行驗證。id()函數會返回指定變量所指向的內存地址,如果是引用傳遞,那麼實參和被賦值後的形參所指向的內存地址肯定是相同的。事實上,確實如此,如下所示:
num1 = 10
name1 = 'Tom'
list1 = ['Tom', 'Jerry', 'Peter', 'Lily']
def fun1(num2, name2, list2):
print(id(num2), id(name2), id(list2))
print(id(num1), id(name1), id(list1))
fun1(num1, name1, list1)
輸出結果:
1828586224 1856648389328 1856648385800
1828586224 1856648389328 1856648385800
實參和形參的內存地址一致,說明Python中的參數傳遞確實是“引用傳遞”。
這篇文章寫了很久,想說的東西太多。有時候手放到鍵盤上放了許久,卻不知從何寫起。算是對知識點的梳理,也希望對他人有所幫助。關於Python中關於函數的其它內容,如:函數遞歸、匿名函數、嵌套函數、高階函數等,之後再講。