1. 程式人生 > >Python函式基礎

Python函式基礎

函式宣告、呼叫、返回基礎

Python中使用def關鍵字來宣告函式,宣告函式的格式為:

def func_name(args):
    ...body...
    [return ...]

有3個需要注意的地方:

  1. 函式名後面必須加冒號
  2. 如果函式體和def不在同一行,則必須縮排
  3. return指定函式返回值,用來結束函式
    • 但return語句是可有可無的,如果不給return,則等價於加上了return None,即函式預設返回None結構

如果函式體body語句只有一行,或者可以簡寫為一行,則可以寫在def的同行。例如:

def myfunc(x,y,z): print(x+y+z)

函式宣告好之後,就可以執行函式,執行函式也稱為呼叫函式,方式為func_name(args),例如:

myfunc(1,2,3)

函式中往往會包含一個return或多個return語句,它可以出現在函式中的任意位置處,它用來結束函式的執行,並返回給定的值。例如:

def func(x):
    return x+5

表示返回x+5的值,返回值是一種值型別,所以可以賦值給變數、可以輸出、可以操作等等。例如:

print(func(3))    # 輸出返回值

a=func(4)         # 賦值給變數
print(a)

print(func(5)+3)  # 數值操作

return語句是可選的,如果函式中不指定return語句,則預設返回None,即類似於return None

關於函式引數

函式的引數其實也是變數,只不過這些變數是獨屬於函式的本地變數,函式外部無法訪問。在函式呼叫的時候,會將給定的值傳遞給函式的引數,這實際上是變數賦值的過程。

def myfunc(x,y,z):
    print(x,y,z)

myfunc(1,2,3)

def首先宣告好函式,然後到了myfunc(1,2,3)時,表示呼叫函式(執行函式),呼叫函式時會將給定的值1,2,3傳遞給函式的引數x,y,z,其實就是變數賦值x=1,y=2,z=3,然後使用print輸出它們。

由於python是動態語言,無需先宣告變數,也無需指定變數的型別,所以python的函式引數和返回值非常的靈活。任何型別的變數或資料結構都可以傳遞給引數,這實際上是變數賦值的過程。例如:

myfunc(1,2,3)
myfunc("abc",2,"def")
myfunc([1,2,3],4,5)

上面幾個函式呼叫語句中,賦值給引數的值可以是數值型別,可以是字串型別,可以是列表型別,也可以是其它任何資料型別。

python函式的引數相比其它語言要複雜一些,意味著要靈活很多,短短一個小節的篇幅完全沒法解釋清楚,關於引數細節,詳細內容見後面的文章

函式宣告、呼叫的過程詳述

def用來宣告一個函式,python的函式包括函式名稱、引數、函式體、函式體中涉及到的變數、返回值。

實際上,函式名稱其實是一個變數名,def表示將儲存在某塊記憶體區域中的函式程式碼體賦值給函式名變數。例如:

def myfunc(x,y,z):
    ...CODE...

上面表示將函式體賦值給變數名myfunc。如下圖:

既然是變數,就可以進行輸出:

def myfunc(x):
    return x+5

print(myfunc)

輸出結果:

<function myfunc at 0x032EA6F0>

由於python是解釋型語言,所以必須先定義函式,才能呼叫函式。

如果匯入一個模組檔案,匯入的時候會解釋、執行檔案中的程式碼,包括def語句,也就是說,匯入檔案時會先宣告好函式。

函式變數的細節

請一定理解本節內容,也許細節方面可能會有些不準確,但對於深入理解函式來說(不僅限於python語言),是非常有幫助的,特別是理解作用域規則的時候。

python是解釋性語言,讀一行解釋一行,解釋一行忘記一行。而函式是一種程式碼塊,程式碼塊是一個解釋單元,是一個整體。在程式碼塊範圍內不會忘記讀取過的行,也不會讀一行就立即解釋一行,而是讀取完所有程式碼塊內的行,然後統籌安排地進行解釋。關於這一點,在後面的文章程式碼塊詳述中有非常詳細的解釋,建議一讀。

當python讀取到def所在行的時候,知道這是一個函式宣告語句,它有一個屬於自己的程式碼塊範圍,於是會讀完整個程式碼塊,然後解釋這個程式碼塊。在這個解釋過程中,會記錄好變數以及該變數的所屬作用域(是全域性範圍內的變數還是函式的本地變數),但一定注意,def宣告函式的過程中不會進行變數的賦值(引數預設值除外,見下文),只有在函式呼叫的時候才會進行變數賦值。換句話說,在def宣告函式的過程中,在函式被呼叫之前,函式所記錄的變數一直都是變數的地址,或者通俗一點理解為記錄變數的名稱,而不會進行變數的賦值替換

如下函式:

x=3
def myfunc(a,b):
    c=10
    print(x,a,b,c)

myfunc(5,6)

輸出結果為:"3 5 6 10"。

上面的函式涉及到了4個變數:a、b、c、x。其中:

  • 全域性變數x
  • 本地變數a、b、c,其中本地變數a和b是函式的引數

在def的過程中,會完完整整地記錄好這些變數以及所屬作用域,但只會記錄,不會進行變數的賦值。如下圖:

然後函式被呼叫,這時候才會開始根據記錄的作用域搜尋變數是否存在,是否已經賦值(非本地變數),並對需要賦值的變數賦值:

  • 查詢全域性變數變數x,它在全域性作用域內已經賦值過了,所以只需找到這個全域性變數即可
  • 查詢本地變數a、b、c,它們是屬於函式myfunc的本地變數,而a和b是引數變數,所以最先對它們進行賦值a=5,b=6,然後賦值普通的本地變數c=10

如圖:

最後執行print(x,a,b,c)輸出這些變數的值。

還需注意,python是讀一行解釋一行的,在函式呼叫過程中,因為c=10print()的前面,所以是先賦值c=10,再執行print,如果print在c=10前面,則先執行print,再賦值,這顯然是錯誤的,因為print()中使用了變數c,但目前還沒有對其賦值。這和其它語言可能有些不同(特別是編譯型語言),它們可能會無視變數賦值以及變數使用的位置前後關係。

如果上面的示例中,函式myfunc呼叫之前,將變數x賦值為另一個值:

x=3
def myfunc(a,b):
    c=10
    print(x,a,b,c)

x=33
myfunc(5,6)

這時將輸出:"33 5 6 10"。因為x是全域性變數,只有在函式呼叫的時候才會去找到變數x對應的值,而這時全域性變數的值已經是33。

匿名函式lambda

匿名函式是指沒有名稱的函式,任何程式語言中,匿名函式都扮演著重要角色,它的功能非常靈活,但是匿名函式中的邏輯一般很簡單,否則直接使用命名函式更好,匿名函式常用於回撥函式、閉包等等。

在python中使用lambda關鍵字宣告匿名函式,python中的lambda是一個表示式而不是一個語句,這意味著某些語句環境下可能無法使用def宣告函式,但卻可以使用lambda宣告匿名函式。當然,匿名函式能實現的功能,命名函式也以一樣都能實現,只不過有時候可能會比較複雜,可讀性會更差。

lambda宣告匿名函式的方式很簡單,lambda關鍵字後面跟上引數列表,然後一個冒號,冒號後跟一個表示式。

lambda argl, arg2,... argN :expression statement

lambda表示式返回一個匿名函式,這個匿名函式可以賦值給一個變數

例如:

# 宣告匿名函式,並賦值給變數f
f = lambda x,y,z: x+y+z

print(f)

輸出結果:

<function <lambda> at 0x027EA6F0>

既然匿名函式賦值給了變數,這個函式就像是命名變數一樣,可以通過這個變數去呼叫這個匿名函式。當然,它畢竟還是匿名函式,正如上面輸出的結果中function <lambda>所示。而且,匿名函式並非一定要賦值給變數。

# 呼叫匿名函式
print(f(2,3,4))  # 輸出9

匿名函式的返回值是冒號後面的表示式計算得到的結果。對於上面的示例,它等價於return x+y+z

因為lambda是一個表示式,所以可以寫在任何表示式可以出現的位置處,而某些語句上下文環境中,並不能直接使用def來宣告。例如,將函式儲存到一個列表中:

L=[ lambda x: x * 2,
    lambda x: x * 3,
    lambda x: x * 4 ]

print(L[0](2))
print(L[1](2))
print(L[2](2))

上面的lambda出現在列表的內部,且這裡面的匿名函式並賦值給某個變數。像def語句就無法出現在這樣的環境中,如果真要使用def來宣告函式,並儲存到列表中,只能在L的外部使用def定義,然後將函式名來儲存。

def f1(x): return x * 2
def f2(x): return x * 3
def f3(x): return x * 4

L=[f1,f2,f3]

print(L[0](2))
print(L[1](2))
print(L[2](2))

看上去沒什麼問題,但函式定義的位置和列表L定義的位置可能會相差甚遠,可讀性可能會非常差。

同理的,還可以將匿名函式儲存在字典的value位置上:

key='four'
print(
    {
        'two':(lambda x: x * 2),
        'three':(lambda x: x * 3),
        'four':(lambda x: x * 4)
    }[key](2)
)

函式巢狀

函式內部可以巢狀函式。一般來說,在函式巢狀時,內層函式會作為外層函式的返回值(當然,並非必須)。既然內層函式要作為返回值,這個巢狀的內層函式更可能會是lambda匿名函式。

例如:

def f(x):
    y=10
    def g(z):
        return x+y+z
    return g

上面的函式f()中嵌套了一個g(),並返回這個g()。其實上面示例中的g()是一個閉包函式。

既然f()返回的是函式,這個函式可以賦值給其它變數,也可以直接呼叫:

# 將巢狀的函式賦值給變數myfunc
# 這時myfunc()和g()是等價的
myfunc = f(3)
print( myfunc(5) )

# 直接呼叫g()
print( f(3)(5) )

當然,巢狀lambda匿名函式也可以,且更常見:

def f(x):
    y=10
    return lambda z: x+y+z

巢狀在迴圈內部的函式

看下面巢狀在迴圈內部的函式,在每個迭代過程中都宣告一個匿名函式,這個匿名函式返回迴圈控制變數i,同時將宣告的匿名函式儲存到列表L中。

def f():
    L=[]
    for i in range(5):
        L.append( lambda : i )
    return L

但如果呼叫該函式f(),並呼叫儲存在列表中的每個匿名函式,會發現它們的值完全相同,且都是迴圈迭代的最後一個元素值i=4

List = f()
print(List[0]())
print(List[1]())
print(List[2]())
print(List[3]())
print(List[4]())

執行結果:

4
4
4
4
4

為什麼會如此?為什麼迴圈迭代過程中的i沒有影響到匿名函式的返回值?這是一個非常值得思考的問題,如果不理解結果,請仔細回顧前文函式變數的細節。如果還是不理解,請閱讀Python作用域詳述

巢狀函式的作用域

此處給幾個示例,這些示例的結果對於只學過python的人來說,可能會很容易理解,但對於學過其它語言的人來說,很容易混淆出錯。

此處並不會對這些示例的結果進行解釋,因為只要理解了前文函式變數的細節,這幾個示例的結果很容易理解。

如下示例:

x=3
def f():
    x=4
    g()
    print("f:",x)

def g():
    print("g:",x)

f()

輸出:

g: 3
f: 4

如果在呼叫函式前,修改全域性變數x的值:

x=3
def f():
    x=4
    g()
    print("f:",x)

def g():
    print("g:",x)

x=6
f()

輸出:

g: 6
f: 4

如果把g()的宣告放在f()的內部呢?

x=3
def f():
    x=4
    def g():
        print("g:",x)
    print("f:",x)
    x=5
    return g

f()()

輸出:

f: 4
g: 5