Python語法速查: 7. 函數基礎

 

 (1)函數基本

● 函數是第一類對象python

Python中萬物皆對象,全部對象都是第一類的(first class),函數也不例外,也是第一類對象。既然是對象,那就能夠看成普通的對象數據處理,好比:擁有本身的屬性、可賦值給一個變量、可放入容器存儲、可做爲另外一個函數的參數或是返回值等等。當你在使用 def 定義函數時,就至關於生成了一個函數對象。閉包

下例中,將3個內置函數放入一個序列,並用迭代器取出後分別調用:併發

line = 'abc,1,3.14'
fun_list = [str, int, float]
para_list = line.split(',')
obj_list = [f(val) for f, val n zip(fun_list, para_list)]

# obj_list結果爲:['abc', 1, 3.14]

 

● 文檔字符串app

一般,將函數def後的第一行,做爲描述函數用途的「文檔字符串」,保存在函數的__doc__屬性中。用內置函數 help(函數名) 也能夠查看函數的描述文檔。函數

 

● 可調用類型spa

可調用類型表示支持函數調用操做的對象,包括:用戶定義的函數、內置函數、實例方法、類、提供了可調用接口的實例。可使用內置函數 callable() 來檢查一個對象是不是可調用的。線程

類都是是能夠調用的,調用類時,會自動將入參傳遞給類的__init__()方法,用以建立一個新實例。code

實例對象通常是不可調用的,可是若是這個實例實現了__call__()方法,那麼這個實例就可直接調用。例如:若x是某個實例,執行 x(args) 就至關於調用 x.__call__(args) 方法。對象

 

● 函數的屬性blog

函數做爲一種對象,理論上能夠給函數添加任意屬性。函數也有一些內部默認的屬性,見下列各表。

「內置函數」具備如下屬性

屬性 描述
__doc__ 文檔字符串。
__name__ 函數名稱。
__self__ 與方法相關的實例。 說明:對於像len()這樣的內置函數,__self__爲None(代表未綁定);而像s.append()這樣的內置方法,__self__爲列表對象s。

 

「用戶定義函數」具備如下屬性

屬性 描述
__doc__ 文檔字符串。
__name__ 函數名稱。
__dict__ 包含函數屬性的字典。
__code__ 編譯後的代碼。
__defaults__ 包含默認參數的元組。
__globals__ 函數應用時對應的全局命名空間的字典。
__clousre__ 閉包(包含與嵌套做用域相關數據的元組)

 

「實例方法」具備如下屬性

屬性 描述
__doc__ 文檔字符串。
__name__ 方法名稱。
__class__ 定義該方法的類。
__func__ 實現方法的函數對象
__self__ 與方法相關的實例(若是是非綁定方法,則爲None)

 

 

● 綁定與非綁定方法

經過實例調用方法時,有綁定和非綁定兩種用法。綁定方法封裝了成員函數和一個對應實例,調用綁定方法時,實例會做爲第一個參數self自動傳遞給方法。而非綁定方法僅封裝了成員函數,並無實例,用戶在調用非綁定方法時,須要顯式地將實例做爲第一個參數傳遞進去。詳見下面2個例子

綁定用法(bound method):

class Foo():
    def meth(self, a):
        print(a)

obj = Foo()     # 建立一個實例
m = obj.meth    # 將meth方法綁定到obj這個實例上
m(2)            # 調用這個方法時,Python會自動將obj做爲self參數傳遞給meth()方法

非綁定用法(unbound method):

class Foo():
    def meth(self, a):
        print(a)

obj = Foo()     # 建立一個實例
um = Foo.meth   # 非綁定,僅僅是將這個方法賦值給um,並不須要實例存在
um(obj, 2)      # 調用這個非綁定方法時,用戶須要顯式地傳入obj實例做爲第一個參數。

 

 

● 匿名函數

使用 lambda 語句能夠建立表達式形式的匿名函數,其用途是指定短小的回調函數。語法爲:

lambda 參數 : 表達式

lambda匿名函數中不能出現非表達式語句、也不能出現多行語句。

下例定義一個匿名函數:

a = lambda x,y: x*y
b = a(2,5)   # b的結果爲:10

下例在序列的sort()方法中傳入lambda匿名函數:

[('b',2),('a',1)].sort(key=lambda x:x[1])  # 結果爲 [('a',1),('b',2)]

# 說明:使用匿名函數對列表元素進行了預處理,將本來的元組('a',1)預處理爲:取出元組中後一個元素(即:1),因此可以進行排序。

 

 

 

 (2)函數參數

● 位置參數、關鍵字參數、返回值

在調用函數時,傳入參數的順序數量必須與函數定義匹配,不然會引起TypeError異常。若是在調用時指定參數名,這樣就沒必要遵守函數定義中的參數順序了,這樣可大大增長調用時的可讀性。這種指定參數名傳入的參數叫作關鍵字參數,通常的未指定參數名的參數叫作位置參數。關鍵字參數只能放在全部的位置參數後面。

函數參數都是按值傳遞的,可是若是傳遞的是對象(即非單純數字),所謂按值傳遞就是函數參數僅僅是將對象的地址值複製一下傳過去而已,因此在函數中實際上是能夠改變外面對象中的內容的。通常最好避免使用這種風格,並且在涉及線程和併發的程序中,使用這類函數的效率很低,由於一般須要使用線程鎖來防止反作用的影響。

若省略return語句、或單一個return關鍵字,就會返回 None 對象。

 

● 參數的默認值

在函數定義時,能夠爲某些參數指定默認值,這樣在調用時能夠不提供這個參數了。一旦出現帶默認值的參數,此參數後續的參數都必須帶默認值,不然會引起SyntaxError異常。

建議不要使用可變對象做爲默認值(如空列表),這樣可能致使意外的bug,以下例所示:

def fun(x, seq=[]):
    seq.append(x)
    return seq
fun(1)    # 返回[1] 
fun(2)    # 返回[1,2]
fun(3)    # 返回[1,2,3]

上例的本意是若未傳入seq參數,則新建一個列表,並將x放入新列表。可是事實上會產生bug。這種狀況建議使用seq=None,再在函數中建新列表。

 

● 單星號 *

在函數定義時,參數前加單星號的意思爲:收集其他的位置參數,並將它們放入同一個元組中。這樣在函數調用時,用戶就能夠提供任意多個參數了。以下例所示:

def fun(x, *y):
    print(x)
    print(y)
    
fun('a', 1, 2, 'c')   # 結果爲:a 和 (1,2,'c')
fun(3)                # 結果爲:3 和 ()

 

單星號亦可反轉使用,即:在調用函數時使用*,自動將一個元組展開爲若干個指定名字的關鍵字參數。

def myadd(x, y):
    return x+y
    
t = (1,2)
myadd(*t)   # 調用時,單星號自動將元組(1,2)展開爲 x, y

 

● 雙星號 **

在函數定義時,參數前加雙星號的意思爲:收集其他的關鍵字參數,並將它們放入一個字典中。這樣在調用函數時,能夠傳入大量可擴充的配置項做爲參數。

def fun(x, **z):
    print(x)
    print(z)

fun(x=1, y=2, z=3)    # 結果爲:1 和 {'y':2, 'z':3}    

 

雙星號亦可反轉使用,在調用函數時使用**,自動將一個字典拆分爲若干個指定名字的關鍵字參數。

def myadd(x, y):
    return x+y

d = {'x':1, 'y':2}
myadd(**d)    # 調用時,雙星號自動將字典d展開爲:x=1, y=2

 

單星號和雙星號可組合使用,用於同時收集位置參數和關鍵字參數,**參數必須出如今*參數的後面。

def fun(*args, **kwargs):
    print(args)
    print(kwargs)
    
fun(1,2,3, x=4,y=5,z=6)
# 結果爲:(1,2,3) 和 {'x':4, 'y':5, 'z':6 }

 

 

 

 (3)做用域

每次執行一個函數時,就會建立一個新的局部命名空間。這個命名空間(又叫做用域),就像其內部有一個「不可見」的字典,其中包含本函數參數的名稱和全部在函數內部定義的局部變量。

除了每一個函數有有一個局部做用域之外,還有一個全局做用域。能夠經過內置函數locals()和globals()查看局部和全局做用域字典,內建函數vars(obj)能夠返回某個實例的做用域字典。

x = 1
def fun():
    y = 2
    print(locals())
    print(globals())

fun()

# locals()的顯示爲:{'y':2}
# globals()除了顯示全局變量x之外,還會顯示不少全局默認的變量:
{ 'x': 1,
  'fun': <function fun at 0x000001E5C52EA048>
  '__name__': '__main__',
  '__doc__':  None,
  '__package__': None,
  …… 
}

 

● 在函數內訪問全局變量

Python解釋器解析變量時,會先搜素局部做用域,若找不到就會搜索全局做用域,若再找不到就會搜索內置命名空間,若是仍然找不到,就會引起NameError異常。

此種方法雖然能夠訪問全局變量,但不能對全局變量賦值,若要賦值全局變量,須要使用global關鍵字。

在函數中修改全局變量:

x = 1
def fun():
    global x
    x = 2
fun()       # 運行fun()後,全局變量 x 的值變爲2

函數中局部變量名和全局變量名重複時:

x = 1
def fun():
    x = 2                 # 此處建立一個名爲 x 的局部變量
    globals()['x'] = 3    # 使用globals()內置函數,可經過字典的方式直接操做全局變量

fun()       # 運行fun()後,全局變量 x 的值變爲3

 

注意:Python中不支持訪問在函數中訪問上級調用函數的局部做用域,以下例的代碼會引起NameError異常

def fun_inner():
    print(x)

def fun_outer():
    x = 2
    fun_inner()
    
fun_outer()     # 調用時會引起NameError異常,由於在fun_inner()函數中,不能訪問外層調用函數fun_outer()的局部做用域中的 x

 

● 嵌套做用域

Python3支持在嵌套定義的函數中,使用外層定義函數(不是外層調用函數)的局部做用域中的變量,這個稱爲:動態做用域(dynamic scoping),須要使用nonlocal關鍵字,以下例所示:

def fun_outer():
    x = 4
    def fun_inner():
        nonlocal x      # 聲明綁定到外層定義的x
        x = 5
        print(x)
        
    fun_inner()
    print(x)
    
fun_outer()             #  會先運行fun_inner()中的print語句,打印出5;而後運行外層中的print語句,可看到x確實被修改爲了5

 

 

 

 (4)遞歸

函數調用自身稱爲遞歸(recursion)。遞歸最典型的使用場景是,能夠把一個大問題分解成重複的小問題來解決,並且這個小問題的解決結構很是簡潔。雖然大部分的遞歸均可以用循環來替代解決,但有時用遞歸寫函數要比循環可讀性更高,看起來也更優雅。

用遞歸計算n的階乘

def factorial(n):
    if n <= 1: 
        return 1
    else:
        return n * factorial(n-1)

 

用遞歸實現序列的二分法查找:

def search(seq, num, lower, upper):
    if lower == upper:
        return upper
    else:
        middle = (lower + upper) // 2
        if num > seq[middle]:
            return search(eq, num, middle+1, upper)
        else:
            return search(seq, num, lower, middle)
            
seq = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
seq.sort()
search(seq, num=2, lower=0, upper=9)    # 使用二分法查找比起遍歷查找,能夠很快地找到num

 

 

 

返回頁首

相關文章
相關標籤/搜索