1. 程式人生 > >Python學習總結(三)——Function(函式)

Python學習總結(三)——Function(函式)

 函式定義

def 函式名():
    函式體
    return 返回值1, 返回值2

       在Python中,函式有五大要點,分別是def、函式名、函式體、引數、返回值,以及兩個英文版符號,分別是括號(括號內為引數)和冒號(:)。

  def:函式的關鍵字,沒它可不行。

  函式名:函式的名稱,根據函式名呼叫函式。

  函式體:函式中進行一系列的具體操作。

  引數:為函式體提供資料。

  返回值:當函式執行完畢後,可以給呼叫者返回資料。

  上述函式的要點中,最重要的是引數和返回值。

1.返回值

  函式是一個功能塊,該功能到底執行成功與否,需要通過返回值來告知呼叫者。函式體內部的語句在執行時,一旦執行到return時,函式就執行完畢。如果沒有return語句,函式執行完畢後也會返回結果,只是結果為None。

2.引數

  定義函式時,引數是一定需要考慮的。Python的函式定義非常簡單,但靈活度卻非常大。

  對於函式的呼叫者來說,只需要知道如何傳遞正確的引數,以及函式將返回什麼樣的值就夠了,函式內部的複雜邏輯被封裝起來,呼叫者無需瞭解。

  Python中,引數型別有:必選引數、預設引數、可變引數、關鍵字引數和命名關鍵字引數。函式中,引數定義的順序必須是:必選引數、預設引數、可變引數、命名關鍵字引數和關鍵字引數

3.空函式

  空函式:什麼事也不做,可以用pass語句。既然“一事不做”,那空函式還有什麼用處?實際上pass可以用來作為佔位符,比如現在還沒想好怎麼寫函式的程式碼,就可以先放一個pass,讓程式碼能執行起來。如此,執行程式碼程式就不會出現錯誤了。

# 空函式
def fun():
    pass

函式引數

  Python中,引數是非常靈活的。掌握引數就能領悟函式的真諦了。這是真的!引數是比較難理解的,特別是引數組合。

  1.位置引數

# 位置引數(必選引數)
def involution(x):
    return x * x


print(involution(6))

  如程式碼所示,引數x就是一個位置引數。

  2.預設引數

# 預設引數
def involution(x, n=2):
    s = 1
    while n > 0:
        n = n - 1
        s = s * x
    return s


print(involution(6))
print(involution(5, 3))

  Python函式支援預設引數,即可以給函式的引數指定預設值。當該引數沒有傳入相應的值時,該引數就使用預設值。

  如程式碼所示,當我們呼叫involution(6),就相當於呼叫involution(6,2)。

       注:1.設定預設引數時,必選引數在前,預設引數在後,否則Python的直譯器會報錯;

   2.定義預設引數要牢記:預設引數必須指向不可變物件!  

def add_end(L=[]):
    L.append('END')
    return L


print(add_end())
print(add_end())
print(add_end())

       上述程式碼展示的是預設引數不為不可變物件的現象。因此,預設引數必須指向不可變物件【字串、None……】。

  3.可變引數

  在Python函式中,還可以定義可變引數。顧名思義,可變引數就是傳入的引數個數是可變的,可以是1個、2個到任意個,還可以是0個。

  我們以數學題為例子,給定一組數字a,b,c……,請計算a2 + b2 + c2 + ……。

  要定義出這個函式,我們必須確定輸入的引數。由於引數個數不確定,我們首先想到可以把a,b,c……作為一個list或tuple傳進來,這樣,函式可以定義如下:

複製程式碼

1 #一般性函式
2 def calc(numbers):
3     sum = 0
4     for n in numbers:
5         sum = sum + n * n
6     return sum

複製程式碼

  如何呼叫calc()函式呢?需要呼叫時,需要為引數引入list或者tuple。

1 #函式呼叫
2 >>> calc([1, 2, 3])
3 14
4 >>> calc((1, 3, 5, 7))
5 84

  然而,如果我們使用可變引數,我們可以進行簡化,方法如下:

複製程式碼

1 #可變引數
2 def calc(*numbers):
3     sum = 0
4     for n in numbers:
5         sum = sum + n * n
6     return sum

複製程式碼

  咋呼叫呢?這個可簡單啦,再也不用list或者tuple了。引數呼叫只需如下所示:

複製程式碼

1 #可變引數的魅力
2 >>> calc(1, 2, 3)
3 14
4 >>> calc(1, 3, 5, 7)
5 84
6 
7 #引數呼叫不用calc([1,2,3]),括號內還用寫中括號,好麻煩~~~

複製程式碼

  定義可變引數和定義一個list或tuple引數相比,僅僅在引數前面加了一個*號。在函式內部,引數numbers接收到的是一個tuple,因此,函式程式碼完全不變。但是,呼叫該函式時,可以傳入任意個引數,包括0個引數

1 >>> calc(1, 2)
2 5
3 >>> calc()
4 0

  如果已經有一個list或者tuple,要呼叫一個可變引數怎麼辦?可以這樣做:

1 >>> nums = [1, 2, 3]
2 >>> calc(nums[0], nums[1], nums[2])
3 14

  這種寫法當然是可行的,問題是太繁瑣,所以Python允許你在list或tuple前面加一個*號,把list或tuple的元素變成可變引數傳進去:

1 >>> nums = [1, 2, 3]
2 >>> calc(*nums)
3 14

  4.關鍵字引數

  可變引數允許你傳入0個或任意個引數,這些可變引數在函式呼叫時自動組裝為一個tuple。而關鍵字引數允許你傳入0個或任意個含引數名的引數,這些關鍵字引數在函式內部自動組裝為一個dict。dict就是字典,它是鍵值對組合,益處多多~~~

1 #引入關鍵字引數,預設為**kw
2 def person(name, age, **kw):
3     print('name:', name, 'age:', age, 'other:', kw)

  函式person除了必選引數name和age外,還接受關鍵字引數kw。在呼叫該函式時,可以只傳入必選引數(必選引數必須全部傳入,否則會出錯),也可以傳入關鍵字引數。注:關鍵字引數可是任意個的。

複製程式碼

 1 #呼叫關鍵字引數
 2 >>>def person(name,age,**kw):
 3 ...    print('name:',name,'age:',age,'other:',kw)
 4 ...
 5 >>>person('Jack')
 6 Traceback (most recent call last):
 7   File "<stdin>", line 1, in <module>
 8 TypeError: person() missing 1 required positional argument: 'age'
 9 >>>person('Jack',36)
10 name:Jack age:36 other:{}
11 >>>person('Jack',36,city='Hangzhou')
12 name:Jack age:36 other:{'city':'Hangzhou'}
13 >>>person('Jack',36,city='Hangzhou',job='Engineer')
14 name:Jack age:36 other:{'city':'Hangzhou','job':'Engineer'}

複製程式碼

  關鍵字引數有什麼用呢?其實,既然存在就有它的強大之處。就像自然界中的萬物,物競天擇,適者生存。如果它能夠在自然界中生存下來,那麼它就有獨特的生存本領。因此,關鍵字引數還是有用武之地的。

  它可以擴充套件函式的功能。比如,在person函式裡,我們保證能接收到name和age這兩個引數,但是,如果呼叫者願意提供更多的引數,我們也能收到。試想你正在做一個使用者註冊的功能,除了使用者名稱和年齡是必填項外,其他都是可選項,利用關鍵字引數來定義這個函式就能滿足註冊的需求。

  如何操作呢?我們可以先組裝出一個dict,然後,把該dict轉換為關鍵字引數傳進去:

1 >>> extra = {'city': 'Hangzhou', 'job': 'Engineer'}
2 >>> person('Jack', 36, city=extra['city'], job=extra['job'])
3 name: Jack age: 36 other: {'city': 'Hangzhou', 'job': 'Engineer'}

  當然了,上面程式碼呼叫方式有點煩,通過dict鍵來查詢值。我們可以通過關鍵字簡化一下:

1 >>> extra = {'city': 'Hangzhou', 'job': 'Engineer'}
2 >>> person('Jack', 36, **extra)
3 name: Jack age: 36 other: {'city': 'Hangzhou', 'job': 'Engineer'}

  **extra表示把extra這個dict的所有key-value用關鍵字引數傳入到函式的**kw引數,kw將獲得一個dict。

   5.命名關鍵字引數

  對於關鍵字引數,函式的呼叫者可以傳入任意不受限制的關鍵字引數。至於到底傳入了哪些,就需要在函式內部通過kw檢查。

  仍以person()函式為例,我們希望檢查是否有city和job引數:

複製程式碼

1 def person(name, age, **kw):
2     if 'city' in kw:
3         # 有city引數
4         pass
5     if 'job' in kw:
6         # 有job引數
7         pass
8     print('name:', name, 'age:', age, 'other:', kw)

複製程式碼

  如果要限制關鍵字引數的名字,就可以用命名關鍵字引數,例如,只接收city和job作為關鍵字引數。這種方式定義的函式如下:

1 def person(name, age, *, city, job):
2     print(name, age, city, job)

  和關鍵字引數*kw不同,命名關鍵字引數需要一個特殊分隔符,*後面的引數被視為命名關鍵字引數。    呼叫命名關鍵字引數方式如下:

1 #呼叫命名關鍵字引數
2 >>> person('Jack', 36, city='Hangzhou', job='Engineer')
3 Jack 36 Hangzhou Engineer

  那如果引數中有可變引數,那該怎麼辦呢?

  若可變引數後面跟著命名關鍵字引數,後面跟著的命名關鍵字引數就不再需要一個特殊分隔符*了。

1 def person(name, age, *args, city, job):
2     print(name, age, args, city, job)

  命名關鍵字引數必須傳入引數名,這和位置引數不同。如果沒有傳入引數名,呼叫將報錯。而命名關鍵字引數可以有預設值,從而簡化呼叫:

1 def person(name, age, *, city='Hangzhou', job):
2     print(name, age, city, job)

  由於命名關鍵字引數city具有預設值,呼叫時,可不傳入city引數:

1 >>> person('Jack', 36, job='Engineer')
2 Jack 36 Hangzhou Engineer

  6.引數組合

  目前,函式中共有5種常用的引數型別。若只傳入一種型別的引數,這太簡單了。難點在哪?難點就在引數組合使用,那是相當噁心。不過,平時最好不要混合使用引數,不然容易搞得“烏煙瘴氣”。

  OK!言歸正傳,不然跑題啦。

  Python中,定義一個函式,我們可以用必選引數、預設引數、可變引數、關鍵字引數和命名關鍵字引數,這5種引數都可以組合使用。但是請注意,引數定義的順序必須是:必選引數、預設引數、可變引數、命名關鍵字引數和關鍵字引數。

  下面來定義一個函式,該函式引數包含一種或幾種引數。

1 def f1(a, b, c=0, *args, **kw):
2     print('a =', a, 'b =', b, 'c =', c, 'args =', args, 'kw =', kw)
3 
4 def f2(a, b, c=0, *, d, **kw):
5     print('a =', a, 'b =', b, 'c =', c, 'd =', d, 'kw =', kw)

  在函式呼叫的時候,Python直譯器自動按照引數位置和引數名把對應的引數傳進去。

複製程式碼

 1 >>> f1(1, 2)
 2 a = 1 b = 2 c = 0 args = () kw = {}
 3 >>> f1(1, 2, c=3)
 4 a = 1 b = 2 c = 3 args = () kw = {}
 5 >>> f1(1, 2, 3, 'a', 'b')
 6 a = 1 b = 2 c = 3 args = ('a', 'b') kw = {}
 7 >>> f1(1, 2, 3, 'a', 'b', x=99)
 8 a = 1 b = 2 c = 3 args = ('a', 'b') kw = {'x': 99}
 9 >>> f2(1, 2, d=99, ext=None)
10 a = 1 b = 2 c = 0 d = 99 kw = {'ext': None}

複製程式碼

  最神奇的是通過一個tuple和dict,你也可以呼叫上述函式:

複製程式碼

1 >>> args = (1, 2, 3, 4)
2 >>> kw = {'d': 99, 'x': '#'}
3 >>> f1(*args, **kw)
4 a = 1 b = 2 c = 3 args = (4,) kw = {'d': 99, 'x': '#'}
5 >>> args = (1, 2, 3)
6 >>> kw = {'d': 88, 'x': '#'}
7 >>> f2(*args, **kw)
8 a = 1 b = 2 c = 3 d = 88 kw = {'x': '#'}

複製程式碼

  所以,對於任意函式,都可以通過類似func(*args, **kw)的形式呼叫它,無論它的引數是如何定義的。

  然而,雖然函式引數型別多達5種,但不要同時使用太多的組合,否則函式介面的可理解性很差。哎,簡簡單單才是真啊。

  7.函式引數小結

  引數,作為函式傳入值的媒介,這裡有必要做一個總結。

  一、Python的函式具有非常靈活的引數形態,既可以實現簡單的呼叫,又可以傳入非常複雜的引數;

  二、預設引數一定要用不可變物件,如果是可變物件,程式執行時會有邏輯錯誤;

  三、*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中的函式部分,引數不容忽視。

函式呼叫

  在學習了函式的定義之後,我們應該需要呼叫函式,獲取我們想要的資料。

  如何呼叫函式呢?語法:函式名(引數)

  Python中,大佬們內建了許多有點用的函式,而我們只需拿來就行(這讓我想起了魯迅的“拿來主義”)。

  若要呼叫Python中的內建函式,我們首先要知道函式名和引數。哈哈,又是引數~~~

  比如我想要求某數的絕對值。如果你不知道Python有相關的內建函式,就只能這麼做:

複製程式碼

 1 #求取絕對值
 2 >>>def abs(num):
 3 ...    if num >= 0:
 4 ...        return num
 5 ...    else:
 6 ...        return (-num)
 7 ...
 8 >>>abs(9)
 9 9
10 >>>abs(0)
11 0
12 >>>abs(-8)
13 8

複製程式碼

  上述程式碼雖然可以實現求絕對值的功能,但是太繁瑣,需要敲幾行程式碼才能實現該功能。然而,Python中有這個函式可以直接呼叫並輸出結果。

複製程式碼

 1 #Python內建函式:abs()
 2 >>>abs(-9)
 3 9
 4 >>>abs(9)
 5 9
 6 #獲取幫助文件
 7 >>>help(abs)
 8 Help on built-in function abs in module builtins:
 9 
10 abs(x, /)
11     Return the absolute value of the argument.

複製程式碼

  對於函式引數,通常會遇到以下兩個問題:

  1.如果函式傳入引數的數量錯誤,會如何呢?簡單,直接Error唄。比如abs():

1 #函式傳入引數的數量錯誤
2 >>> abs(-9,89)
3 Traceback (most recent call last):
4   File "<stdin>", line 1, in <module>
5 TypeError: abs() takes exactly one argument (2 given)

  2.如果傳入的引數數量是對的,但引數型別不能被函式所接受,也會報TypeError的錯誤,並且給出錯誤資訊:str是錯誤的引數型別:

1 #傳入的引數型別錯誤
2 >>> abs('a')
3 Traceback (most recent call last):
4   File "<stdin>", line 1, in <module>
5 TypeError: bad operand type for abs(): 'str'

常見內建函式(Built-in Functions)

  1.資料結構相關:list()、tuple()、dict()、str()……

  2.數字相關:abs()、min()、max()、len()……

  3.其他:int()、float()……

  好,不一一例舉了,直接上圖吧~~~

資料型別轉換

  Python內建的常用函式還包括資料型別轉換函式,比如int()函式可以把其他資料型別轉換為整數:

複製程式碼

 1 #Python之資料型別轉換(int、float、str……)
 2 >>> int('123')
 3 123
 4 >>> int(12.34)
 5 12
 6 >>> float('12.34')
 7 12.34
 8 >>> str(1.23)
 9 '1.23'
10 >>> str(100)
11 '100'
12 >>> bool(1)
13 True
14 >>> bool('')
15 False

複製程式碼

函式別名

  瞭解Linux的讀者可能知道別名(alias,unalias)這個指令。Python中也有“別名”之說,比如把函式名賦給變數:

複製程式碼

 1 #函式“別名”
 2 >>>abs(-8)
 3 8
 4 >>>a = abs
 5 >>>a(-9)
 6 9
 7 >>>a(0)
 8 0
 9 >>>a(9)
10 9

複製程式碼

遞迴函式

  講述遞迴函式之前,我想起一個東西:階乘(n!)。舉個例子,我們來計算階乘n! = 1 x 2 x 3 x ... x n,用函式fact(n)表示,可以看出:

  fact(n) = n! = 1 x 2 x 3 x ... x (n-1) x n = (n-1)! x n = fact(n-1) x n

  因此,遞迴函式:在函式內部,一個函式在內部呼叫自身本身。

  於是,fact(n)用遞迴的方式寫出來就是:

複製程式碼

 1 #遞迴函式
 2 >>>def fact(n):
 3 ...    if n == 1:
 4 ...        return 1
 5 ...    else:
 6 ...        return fact(n - 1) * n
 7 ...
 8 >>>fact(1)
 9 1
10 >>>fact(5)
11 120
12 #遞迴函式之棧溢位
13 >>>fact(1000)
14 Traceback (most recent call last):
15   File "<stdin>", line 1, in <module>
16   File "<stdin>", line 5, in x
17   File "<stdin>", line 5, in x
18   File "<stdin>", line 5, in x
19   [Previous line repeated 994 more times]
20   File "<stdin>", line 2, in x
21 RecursionError: maximum recursion depth exceeded in comparison

複製程式碼

  如程式碼所示,使用遞迴函式的優點是邏輯簡單清晰,缺點是過深的呼叫會導致棧溢位。

  針對尾遞迴優化的語言可以通過尾遞迴防止棧溢位。尾遞迴事實上和迴圈是等價的,沒有迴圈語句的程式語言只能通過尾遞迴實現迴圈。

  Python標準的直譯器沒有針對尾遞迴做優化,任何遞迴函式都存在棧溢位的問題。

  什麼是尾遞迴?這個請讀者自行查詢唄,這裡就不介紹啦,嘿嘿~~~

  下面來個斐波拉契數列

複製程式碼

1 #斐波拉契數列
2 >>>def fibo(arg1,arg2):
3 ...    if arg1 == 0:
4 ...        print(arg1,arg2)
5 ...    arg3 = arg1 + arg2
6 ...    print(arg3)
7 ...    fibo(arg2, arg3)
8 ...
9 >>>fibo(0,1)    

複製程式碼

  上述程式碼展示的斐波拉契數列會一直計算,直至棧溢位:

複製程式碼

 1 #斐波拉契數列導致棧溢位
 2 488272859468887457959087733119242564077850743657661180827326798539177758919828135114407499369796465649524266755391104990099120377
 3 Traceback (most recent call last):
 4   File "<stdin>", line 1, in <module>
 5   File "<stdin>", line 6, in fibo
 6   File "<stdin>", line 6, in fibo
 7   File "<stdin>", line 6, in fibo
 8   [Previous line repeated 992 more times]
 9   File "<stdin>", line 5, in fibo
10 RecursionError: maximum recursion depth exceeded while calling a Python object
11 16602747662452097049541800472897701834948051198384828062358553091918573717701170201065510185595898605104094736918879278462233015981029522997836311232618760539199036765399799926731433239718860373345088375054249

複製程式碼

  如何才能避免棧溢位呢?自己想唄,要不大腦會生鏽的。

  對於漢羅塔問題,利用遞迴來解決該問題也是相當的簡單,且程式碼清晰:

 1 #遞迴解決漢羅塔問題
 2 >>>def hanrota(n,a,b,c):
 3 ...    if n == 1:
 4 ...        print(a,'-->',c)
 5 ...    else:
 6 ...        hanrota(n - 1,a,c,b)
 7 ...        hanrota(1,a,b,c)
 8 ...        hanrota(n - 1,b,a,c)
 9 ...
10 >>>hanrota(3,'A','B','C')
11 A --> C
12 A --> B
13 C --> B
14 A --> C
15 B --> A
16 B --> C
17 A --> C

漢羅塔問題

匿名函式

  定義函式真得不可多得,但有時候不需要顯示地定義函式。因此,函式也需要靈活地運用。使用匿名函式可以更加方便。

  匿名函式語法:

    lambda x: x * x(關鍵字lambda表示匿名函式,冒號前面的x表示函式引數)

1 def f(x):
2     return x * x

  匿名函式的好處:因為函式沒有名字,不必擔心函式名衝突。

1 def is_odd(n):
2    return n % 2==1
3 
4 L = list(filter(lambda n: n%2==1,range(1,20)))
5 print(L)  

1.匿名函式也是一個函式物件,也可以把匿名函式賦值給一個變數,再利用變數來呼叫該函式。

  2.Python中,匿名函式可以作為返回值返回並輸出結果。