Python函式的定義、匿名函式、函式的引數、函式呼叫、引數傳遞、變數作用域、遞迴呼叫
Python函式:
函式是組織好的,可重複使用的,用來實現單一,或相關聯功能的程式碼段。
Python提供了許多內建函式,比如print()。你也可以自己建立函式,這被叫做使用者自定義函式。
定義函式:
在Python中,定義一個函式要使用def
語句,依次寫出函式名、括號、括號中的引數和冒號:
,然後,在縮排塊中編寫函式體,函式的返回值用return
語句返回。函式執行完畢也沒有return
語句時,自動return None
。函式可以同時返回多個值,但其實返回的是一個元組(tuple)。
如:
def my_abs(x): if x >= 0: return x else: return -x
預設情況下,引數值和引數名稱是按函式宣告中定義的順序匹配起來的。
空函式:
如果想定義一個什麼事也不做的空函式,可以用pass
語句:
def nop():
pass
pass
可以用來作為佔位符,比如現在還沒想好怎麼寫函式的程式碼,就可以先放一個pass
,讓程式碼能執行起來。
pass
還可以用在其他語句裡,比如:
if age >= 18:
pass
缺少了pass
,程式碼執行就會有語法錯誤。
匿名函式:
當我們在傳入函式時,有些時候,不需要顯式地定義函式,直接傳入匿名函式更方便。
在Python中,對匿名函式提供了有限支援。python 使用 lambda 來建立匿名函式。
所謂匿名,意即不再使用 def 語句這樣標準的形式定義一個函式。
lambda 只是一個表示式,函式體比 def 簡單很多。
lambda的主體是一個表示式,而不是一個程式碼塊。僅僅能在lambda表示式中封裝有限的邏輯進去。
lambda 函式擁有自己的名稱空間,且不能訪問自己引數列表之外或全域性名稱空間裡的引數。
雖然lambda函式看起來只能寫一行,卻不等同於C或C++的行內函數,後者的目的是呼叫小函式時不佔用棧記憶體從而增加執行效率。
語法
lambda 函式的語法只包含一個語句,如下:
lambda [arg1 [,arg2,.....argn]]:expression
如:
>>> list(map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9]))
[1, 4, 9, 16, 25, 36, 49, 64, 81]
上面的lambda函式實際就是:
關鍵字lambda
表示匿名函式,冒號前面的x
表示函式引數。
注意:
匿名函式有個限制,就是隻能有一個表示式,不用寫return
,返回值就是該表示式的結果。用匿名函式有個好處,因為函式沒有名字,不必擔心函式名衝突。此外,匿名函式也是一個函式物件,也可以把匿名函式賦值給一個變數,再利用變數來呼叫該函式。同樣,也可以把匿名函式作為返回值返回。
如:
>>> f = lambda x: x * x
>>> f
<function <lambda> at 0x101c6ef28>
>>> f(5)
25
def build(x, y):
return lambda: x * x + y * y
函式的引數:
在Python中定義函式,可以用必選引數、預設引數、可變引數、關鍵字引數和命名關鍵字引數,這5種引數都可以組合使用。但是請注意,引數定義的順序必須是:必選引數、預設引數、可變引數、命名關鍵字引數和關鍵字引數。
命名關鍵字引數:
函式的呼叫者可以傳入任意不受限制的關鍵字引數。至於到底傳入了哪些,就需要在函式內部通過kw
檢查。
以person()
函式為例,我們希望檢查是否有city
和job
引數:
def person(name, age, **kw):
if 'city' in kw:
# 有city引數
pass
if 'job' in kw:
# 有job引數
pass
print('name:', name, 'age:', age, 'other:', kw)
但是呼叫者仍可以傳入不受限制的關鍵字引數:
>>> person('Jack', 24, city='Beijing', addr='Chaoyang', zipcode=123456)
如果要限制關鍵字引數的名字,就可以用命名關鍵字引數。這種方式定義的函式如下:
def person(name, age, *, city, job):
print(name, age, city, job)
和關鍵字引數**kw
不同,命名關鍵字引數需要一個特殊分隔符*
,*
後面的引數被視為命名關鍵字引數。
呼叫方式如下:
>>> person('Jack', 24, city='Beijing', job='Engineer')
Jack 24 Beijing Engineer
如果函式定義中已經有了一個可變引數,後面跟著的命名關鍵字引數就不再需要一個特殊分隔符*。
def person(name, age, *args, city, job):
print(name, age, args, city, job)
命名關鍵字引數必須傳入引數名,這和位置引數不同。如果沒有傳入引數名,呼叫將報錯:
>>> 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
由於呼叫時缺少引數名city
和job
,Python直譯器把這4個引數均視為位置引數,但person()
函式僅接受2個位置引數。
命名關鍵字引數可以有預設值,從而簡化呼叫。
def person(name, age, *, city='Beijing', job):
print(name, age, city, job)
由於命名關鍵字引數city
具有預設值,呼叫時,可不傳入city
引數:
>>> person('Jack', 24, job='Engineer')
Jack 24 Beijing Engineer
使用命名關鍵字引數時,要特別注意,如果沒有可變引數,就必須加一個*
作為特殊分隔符。
如果缺少*
,Python直譯器將無法識別位置引數和命名關鍵字引數:
def person(name, age, city, job):
# 缺少 *,city和job被視為位置引數
pass
通過一個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)
的形式呼叫它,無論它的引數是如何定義的。
注意:
注意定義可變引數和關鍵字引數的語法:
*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的習慣寫法,當然也可以用其他引數名,但最好使用習慣用法。
命名的關鍵字引數是為了限制呼叫者可以傳入的引數名,同時可以提供預設值。
定義命名的關鍵字引數在沒有可變引數的情況下不要忘了寫分隔符*
,否則定義的將是位置引數。
函式呼叫:
Python內建了很多有用的函式,我們可以直接呼叫。
如呼叫abs
函式(求絕對值的函式):
>>> abs(100)
100
>>> abs(-20)
20
>>> abs(12.34)
12.34
函式名其實就是指向一個函式物件的引用,完全可以把函式名賦給一個變數,相當於給這個函式起了一個“別名”。
>>> a = abs # 變數a指向abs函式
>>> a(-1) # 所以也可以通過a呼叫abs函式
1
如上面所示,把函式名賦給a後,相當於給函式起了一個別名a,可以直接呼叫a(-1)即相當於呼叫了abs(-1)。
遞迴呼叫:
在函式內部,可以呼叫其他函式。如果一個函式在內部呼叫自身本身,這個函式就是遞迴函式。遞迴函式的優點是定義簡單,邏輯清晰。理論上,所有的遞迴函式都可以寫成迴圈的方式,但迴圈的邏輯不如遞迴清晰。
如:
計算階乘n! = 1 x 2 x 3 x ... x n
def fact(n):
if n==1:
return 1
return n * fact(n - 1)
使用遞迴函式需要注意防止棧溢位。在計算機中,函式呼叫是通過棧(stack)這種資料結構實現的,每當進入一個函式呼叫,棧就會加一層棧幀,每當函式返回,棧就會減一層棧幀。由於棧的大小不是無限的,所以,遞迴呼叫的次數過多,會導致棧溢位。
解決遞迴呼叫棧溢位的方法是通過尾遞迴優化,事實上尾遞迴和迴圈的效果是一樣的,可以把迴圈看成是一種特殊的尾遞迴函式。
尾遞迴:
在函式返回的時候,呼叫自身本身,並且,return語句不能包含表示式。這樣,編譯器或者直譯器就可以把尾遞迴做優化,使遞迴本身無論呼叫多少次,都只佔用一個棧幀,不會出現棧溢位的情況。
如:
上例優化成尾遞迴函式:
def fact(n):
return fact_iter(n, 1)
def fact_iter(num, product):
if num == 1:
return product
return fact_iter(num - 1, num * product)
此時return fact_iter(num - 1, num * product)
僅返回遞迴函式本身,num - 1
和num * product
在函式呼叫前就會被計算,不影響函式呼叫。
事實上,大多數程式語言沒有針對尾遞迴做優化,Python直譯器也沒有做優化,所以,即使把上面的fact(n)
函式改成尾遞迴方式,也會導致棧溢位。
引數傳遞:
可更改(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的作用域一共有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 未定義,無法使用,因為它是區域性變數,只有在函式內可以使用。
全域性變數和區域性變數:
定義在函式內部的變數擁有一個區域性作用域,定義在函式外的擁有全域性作用域。
區域性變數只能在其被宣告的函式內部訪問,而全域性變數可以在整個程式範圍內訪問。呼叫函式時,所有在函式內宣告的變數名稱都將被加入到作用域中。
global 和 nonlocal關鍵字:
當函式在區域性作用域內想修改全域性作用域的變數時,就要用到global關鍵字。
以下例項修改全域性變數 num:
num = 1
def fun1():
global num # 需要使用 global 關鍵字宣告
print(num)
num = 123
print(num)
fun1()
print(num)
執行截圖如下:
如果要修改巢狀作用域(閉包函式外的函式中作用域,即外層非全域性作用域)中的變數則需要 nonlocal 關鍵字。
如:
def outer():
num = 10
def inner():
nonlocal num # nonlocal關鍵字宣告
num = 100
print(num)
inner()
print(num)
outer()
執行截圖如下: