1. 程式人生 > >Python 語言學習 第七篇:函式1(定義、呼叫和變數的作用域)

Python 語言學習 第七篇:函式1(定義、呼叫和變數的作用域)

函式是把一些語句集合在一起的程式結構,用於把複雜的流程細分成不同的元件,能夠減少程式碼的冗餘、程式碼的複用和修改程式碼的代價。

函式可以0個、1個或多個引數,向函式傳遞引數,可以控制函式的流程。函式還可以返回程式碼執行的結果,從技術上講,任何函式都要返回結果,一個沒有返回值的函式會自動返回none物件。如果呼叫者需要函式返回結果,需要顯式使用return語句。

一,函式的定義

Python使用def 語句將建立一個函式物件,並將其賦值給一個變數名,()內是函式的引數,引數通過賦值向引數傳值。

def fun_name (arg1,age2...):
    fun_body

return語句表示函式呼叫的結束,並把結果傳遞給呼叫者。return語句是可選的,如果它沒有出現,那麼函式將會在控制流執行完函式主體時結束。

1,def是可執行的語句

def語句是實時執行的,不僅def語句,Python中的所有語句都是實時執行的,並沒有獨立的編譯時間。def語句是一個可執行的語句,在def執行前,函式並不存在,直到def語句執行之後,函式才被建立。

2,函式是一個物件,函式名是變數

def語句是一個賦值語句,函式是一個物件,函式名是一個變數,def語句設定函式名和函式物件的引用。

當def語句執行的時候,它建立了一個新的函式物件,並將其賦值給一個變數名。

3,引數是通過賦值傳遞的

Python通過賦值(=)把引數傳遞給函式,這和普通的賦值語句(a=1,或a=b)的行為是相同。當傳遞常量給引數時,引數引用的是一個全新的物件;當傳遞變數給引數,引數和變數是物件的共享引用。

4,函式是可以巢狀的

一個def是一個語句,可以出現在任一語句可以出現的地方,甚至巢狀在其他的語句中。這就是說,函式內部可以巢狀函式的定義,例如:

def times(x,y):
     z=x*y
     def print_result():
             print(z)
     print_result()

函式 print_result()是一個嵌入函式,定義在函式times(x,y)之內。

實際上,Python的函式是有層次結構,最外層的def是頂層函式,在頂層函式內部定義的函式是巢狀函式。

二,函式的呼叫 

函式通過表示式呼叫,傳入一些值,並返回結果。函式呼叫的格式是:函式名 + (args),小括號中是傳遞給引數的變數或值,例如:

func_name(var1,var2...)

如果函式存在返回值,使用變數來接收函式的返回值:

ret_value=fun_name(var1,var2...)

在呼叫函式時,函式的行為依賴於型別,這是因為函式是語句的集合,語句包含操作符,而操作符的行為是依賴於型別的,

例如,函式times返回兩個引數的乘積,當傳入數字時,函式 times(2,4) 返回8,* 的作用是計算乘積;當傳入字元型別時,函式 times('ab',2) 返回'abab',*號的作用是重複字串。換句話說,函式times()的作用取決於傳遞給它的值。

def times(x,y):
    return x *y

呼叫函式時,這種依賴於型別的行為稱為多型,就是說,一個操作的作用取決於操作物件的型別。函式的多型性,使得函式可以自動適用於所有類別的物件型別。

如果傳給函式的物件有預期的方法和表示式操作符,那麼函式就相容物件。如果傳遞的物件不支援預期的介面,Python會在 * 表示式執行時檢測到錯誤,並自動丟擲一個異常。

這種特性,使得Python程式碼不應該關心特定的型別,函式應該為物件編寫介面,而不是資料型別。當然,這種多型的程式設計模型意味著:必須測試程式碼去檢測執行結果是否錯誤,而不是編寫程式碼進行型別檢查。

三,變數的作用域

在Python程式碼中變數無處不在,名稱空間就是儲存變數名的地方,變數名能夠訪問(可見)的名稱空間叫做作用域。

當在程式中使用變數名時,Python建立、改變或者查詢變數都是在名稱空間中進行的,變數名被賦值的位置決定了變數名能夠被訪問的範圍。

1,變數的分類

Python中的變數在第一次被賦值時建立,並且必須經過賦值後才能使用。由於沒有變數的宣告,Python把變數名被賦值的地點關聯為(繫結為)一個特定的名稱空間。

根據變數的名稱空間,把變數大致分為三類:

  • 模組是全域性名稱空間,其名稱是模組檔案的名稱,位於模組內的頂層函式名和變數名叫做全域性變數。
  • 函式是區域性名稱空間,其名稱是函式的名稱,位於函式內的函式名和變數名叫做本地變數。
  • 由於一個函式可以巢狀在其他函式內,這使得函式的定義具有層次。我們把一個不在本函式內定義的、而是在上層函式中定義的本地變數叫做非本地變數,也就是,這個變數不是當前函式的本地變數,而是上層函式的本地變數。

2,變數的作用域

變數的作用域是指變數可見的範圍,一個變數的作用域總是由變數被賦值的地方決定的,也就是說,變數被賦值的地方決定了變數可見的範圍。

  • 如果一個變數賦值的地點是在def之內,那麼該變數是本地變數,作用域在def之內,在def之外,本地變數是不可見的。
  • 如果一個變數賦值的地點是在def之外,那麼該變數是全域性變數,作用域是全域性的,在函式內可以引用全域性變數。

在預設情況下,一個函式的所有變數名都是與函式的名稱空間相關聯的:

  • 一個在def內定義的變數名只能被def的程式碼使用,不能在函式的外部呼叫該變數名;
  • def中的變數名與def之外的變數名並不衝突,一個在def之外被賦值的變數x和在def中被賦值的變數x是不同的變數。

由於變數可以在三個地方分配,那麼變數的作用域實際上分為三類:

  • 如果一個變數是在def之內賦值,變數可見範圍是在函式內,變數的作用域是本地(local);
  • 如果一個變數是在def之內賦值,對該函式中巢狀的函式來說,該變數的作用域是非本地的(nonlocal),或稱作巢狀(enclosed)作用域;
  • 如果一個變數是在def之外賦值,變數可見的範圍是整個模組,變數的作用域是全域性(global)。

3,作用域法則

所有變數名都可以歸納為內建的(builtin)、全域性的(global)和本地的(local)。

內建的模組是Python預先定義好,可以直接引用的。

模組定義的是全域性作用域,全域性作用域的作用範圍僅限於單個模組(檔案),也就是說,在一個檔案的頂層的變數名對於這個檔案內部的程式碼而言是全域性的。

在預設情況下,在函式內部,賦值的變數名除非宣告為全域性變數或非本地變數之外,都是本地作用域內的。函式還定義了巢狀的作用域,使其內部使用的變數名本地化,以便函式內部使用的變數名不會與函式外的變數名衝突。每次對函式的呼叫都會建立一個新的本地作用域。如果需要給一個巢狀的def中的變數名賦值,從Python 3.0開始,可以使用 nonlocal語句宣告來做到。

注意:模組頂層的函式名是全域性變數,函式內部的def定義的是區域性變數;函式的引數是本地變數;一個函式內部的任何型別的賦值都會把一個變數劃定為本地的,這意味著,函式內部的賦值(=)語句,def語句等,定義的都是本地變數。

4,變數名解析(LEGB原則)

變數名的解析遵從LEGB原則,當引用一個變數時,Python按照以下順序依次進行查詢:從本地變數中、在任意上層函式的作用域、在全域性作用域,最後在內建作用域中查詢。

LEGB法則解析變數名的詳細機制:

  • 當在函式中引用變數名時,Python依次搜尋4個作用域:本地作用域(L),然後是上一層結構中def的本地作用域(E),再然後是全域性作用域(G),最後是內建作用域(B),並且再第一處能夠找到該變數名的地方停下來。如果變數名再這次搜尋中沒有找到,Python會報錯。
  • 當在函式中給一個變數名賦值時(而不是在一個表示式中對其進行引用),Python總是建立或改變本地作用域的變數名,除非它已經在當前函式中宣告為全域性變數(global)或者非本地變數(nonlocal)。

把巢狀作用域定義為:在當前的def語句之外,在頂層def語句之內的作用域,巢狀作用域的解析細節:

  • 對變數x的引用,首先在當前函式內查詢變數名x;之後會向上層的函式中查詢變數名x,從內向外依次查詢巢狀作用域;之後查詢當前的全域性作用域(模組);最後再到內建作用域內查詢。而全域性宣告將會直接從全域性作用域(模組)進行搜尋。
  • 對變數x的賦值,如果變數x在函式內部宣告為全域性變數(global x),那麼賦值會修改全域性變數x的值;如果變數x在函式內被宣告為非本地變數(nonlocal x),那麼賦值會修改最近的巢狀函式的本地作用域內的變數x的值。

5,在函式內引用全域性變數

global不是宣告一個型別,而是宣告命名的名稱空間是全域性的,也就是說,告訴函式打算宣告一個或多個全域性變數名。

對於全域性變數名,這裡對用法作一個總結:

  • 全域性變數是位於模組內部頂層的變數名;
  • 如果要在函式內對全域性變數進行賦值,那麼必須宣告該變數是全域性的;
  • 全域性變數名在函式的內部可以直接引用。

例如,x是全域性變數,在函式func中使用global 宣告x是全域性變數,對x賦值,就是修改全域性變數的值:

x=11
def func():
    global x
    x=12

使用global語句把變數宣告為全域性變數,這樣,在函式內部就可以修改全域性變數的值,也就是說,global語句允許在def中修改全域性變數的值。

global語句包含了關鍵字global,其後跟著一個或多個由逗號分開的變數名,當在函式內被賦值或引用時,所有列出來的變數都被對映到全域性變數名。

global x,y,z

6,在函式內引用上層的非本地變數

Pytho 3.0 引入了nonlocal語句,用於在一個函式內宣告一個非本地的變數,該變數定義於一個def語句中,並且位於巢狀作用域的上層。

例如,函式foo1定義了變數var1和var2,要想在函式foo2中改變它們的值,必須在foo2中使用nonlocal語句把它們宣告為非本地變數:

def foo1:
    var1=1
    var2=2
    ...
    def foo2:
        nonlocal var1,var2,.. 

nonlocal語句是一個宣告語句,用於把函式內的變數宣告為非本地變數。非本地變數是指不在本函式內定義的,而是在上層函式中定義的本地變數。

nonlocal語句的用法解析:

  • nonlocal語句完全忽略當前函式的本地作用域,這意味著,nonlocal語句使得對該語句列出的名稱的查詢從上層函式的作用域開始,而不是從語句宣告的本地作用域開始。
  • nonlocal語句列出的名稱,必須在一個巢狀的def中提前定義過,否則,Python將會產生一個錯誤,也就是說,nonlocal語句宣告的變數只能是def中定義的本地變數,而不能是模組的全域性變數。
  • nonlocal語句允許對非本地變數賦值,修改其值。
  • 在內嵌的函式中,可以直接引用非本地變數,不需要使用nonlocal語句宣告。

nonlocal語句提供了一種方式,使得巢狀的函式能夠提供可寫的狀態資訊,以便在隨後呼叫巢狀的函式時,能夠記住這些資訊。簡而言之,nonlocal語句的引入使得Python允許修改非本地變數。

四,閉合函式

Python的閉合函式是指一個能夠記住巢狀作用域的變數值的函式,儘管那個作用域已經不存在了。

例如,建立一個閉合函式maker,該函式生成並返回一個巢狀函式action,卻並呼叫這個內嵌的函式。

def maker(x):
    def action(y):
        return x*y
    return action

呼叫閉合函式,得到的是生成的內嵌函式的一個引用。當我們呼叫閉合函式,它會返回內嵌函式的引用;當呼叫內嵌函式action時,我們發現儘管閉合函式已經返回並退出,但是,內嵌函式記住了閉合函式內部的變數x的值。

f=maker(2)
f(3)

也就是說,閉合函式的本地作用域的資訊被保留了下來。為了能夠在內嵌的def中使用變數x的值,Python自動記住了所需要的上層作用域的任意值。

注意:如果lambda或者def在函式中定義,巢狀在一個迴圈之中,並且巢狀的函式引用了一個上層作用域的變數,該變數被迴圈所改變,所有在這個迴圈中產生的函式將會由相同的值——在最後一次迴圈中完成時被引用變數的值。 

>>> def maker():
...     acts=[]
...     for i in range(5):
...             acts.append(lambda x:i**x)
...     return acts
...
>>> acts=maker()
>>> acts[0](2)
16

因為巢狀作用域中的變數在巢狀的函式被呼叫時才進行檢查,所以,它們實際上記住的是同樣的值(在最後一次迴圈迭代中迴圈變數的值)。

也就是說,只有當呼叫acts[0](2)時,採取檢查變數i的值,此時變i的值是最後一次迭代的值4。要解決這類問題,必須在函式maker呼叫時,對i的值進行評估,並儲存起來。

>>> def maker():
...     acts=[]
...     for i in range(5):
...             acts.append(lambda x, i=i : i**x)
...     return acts

為了讓這類程式碼能夠工作,必須使用預設引數把當前的值傳遞給巢狀作用域的變數。因為預設引數是在巢狀函式建立時評估的(而不是其稍後呼叫時),所以,每一個函式都記住了自己的變數 i 的值。

 

參考文件: