1. 程式人生 > >Python函式的定義、匿名函式、函式的引數、函式呼叫、引數傳遞、變數作用域、遞迴呼叫

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()函式為例,我們希望檢查是否有cityjob引數:

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

由於呼叫時缺少引數名cityjob,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 - 1num * 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()

執行截圖如下: