歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux編程 >> Linux編程

Python之函數進階

本節內容

已經介紹了Python中函數的定義、函數的調用、函數的參數以及變量的作用域等內容,現在來說下函數的一些高級特性:

  1. 遞歸函數
  2. 嵌套函數與閉包
  3. 匿名函數
  4. 高階函數
  5. 內置函數
  6. 總結

一、遞歸函數


函數是可以被調用的,且一個函數內部可以調用其他函數。如果一個函數在內部調用本身,這個函數就是一個遞歸函數。函數遞歸調用的過程與循環相似,而且理論上,所有的遞歸函數都可以寫成循環的方式,但是遞歸函數的優點是定義簡單,邏輯清晰。遞歸和循環都是一個重復的操作的過程,這些重復性的操作必然是需要有一定的規律性的。另外,很明顯遞歸函數也需要一個結束條件,否則就會像死循環一樣遞歸下去,直到由於棧溢出而被終止(這個下面介紹)。

可見,要實現一個遞歸函數需要確定兩個要素:

  • 遞歸規律
  • 結束條件

1. 實例:計算正整數n的階乘 n! = 1 * 2 * 3 * ... * n

循環實現

思路有兩個:

  • 從1乘到n,需要額外定義一個計數器存放n當前的值
  • 從n乘到1,無需額外定義計數器,直接對n進行減1操作,直到n=0返回1結束
def fact(n):
    if n == 0:
        return 1
    
    result = 1
    while n >= 1:
        result *= n
        n -= 1
    return result

遞歸實現

先來確定遞歸函數的兩個要素:

  • 遞歸規律:n!=1 * 2 * 3 * ... * n = (n-1)! * n,也就是說fact(n) = fact(n-1) * n,且n逐一減小
  • 結束條件:當n==0時返回1結束
def fact(n):
    if n == 0:
        return 1
    return fact(n-1) * n

怎麼樣?遞歸函數的實現方式是不是既簡單、又清晰。

我們計算fact(5)的計算過程是這樣的:

===> fact(5)
===> 5 * fact(4)
===> 5 * (4 * fact(3))
===> 5 * (4 * (3 * fact(2)))
===> 5 * (4 * (3 * (2 * fact(1))))
===> 5 * (4 * (3 * (2 * 1)))
===> 5 * (4 * (3 * 2))
===> 5 * (4 * 6)
===> 5 * 24
===> 120

同理,要實現求1 + 2 + 3 + ... + n,可以這樣寫:

def fact(n):
    if n == 1:
        return 1
    return fact(n-1) + n

2. 遞歸函數優缺點

遞歸函數的優點:
定義簡單、邏輯清晰。

遞歸函數的缺點:
效率並不高且需要注意防止棧溢出。

其他特點:
大家會發現上面實現的遞歸函數在運算的過程中n是逐漸減小的,也就是說問題規模應該是逐層減少的。

3. 遞歸特性總結

下面我們來總結寫遞歸的特性:

  • 必須有一個明確的結束條件
  • 每次進入更深一層的遞歸時,問題規模相比上次遞歸都應有所減少
  • 遞歸效率不高,遞歸層次過多會導致棧溢出。

因為在計算中,函數調用時通過棧(stack,特點是後進先出--LIFO)這種數據結構實現的,每當進入一個函數調用,棧就會加一層棧針,每當函數返回,棧就會減少一層棧針。由於棧的大小不是無限的,所有遞歸調用的次數過多,會導致棧溢出。關於堆棧的介紹可以看下這裡:<<內存堆和棧的區別>>。

每種編程語言都對遞歸函數可遞歸的深度有限制(可以看看這裡),有些是跟相應內存空間的分配有關(因為棧是在內存空間中的),如Java。Python中對遞歸的深度限制默認為1000,可以通過sys.getrecursionlimit()函數來獲取該值,超過這個深度會報錯:RecursionError: maximum recursion depth exceeded in comparison。當然也可以通過sys.setrecursionlimit(n)來設置新的限制值。

二、嵌套函數與閉包


1. 嵌套函數

嵌套函數是指在函數內部定義一個函數,這些函數都遵循各自的作用域和生命周期規則。

來看個例子:

def outer():
    level = 1
    print('outer', level)
    def inner():
        print('inner', level)
    inner()

調用outer函數outer(),輸出結果如下:

outer 1
inner 1

再來看個例子:

def outer():
    level = 1
    print('outer', level)
    def inner():
        level = 2
        print('inner', level)
    inner()

調用outer函數outer(),輸出結果如下:

outer 1
inner 2

嵌套函數查找變量的順序是:先查找自己函數體內部是否包含該變量,如果包含則直接應用,如果不包含則查找外層函數體內是否包含該函數,依次向外。

2. 閉包

首先要說明一個問題:函數名其實也是一個變量,我們通過def定義一個函數時,實際上就是在定義一個變量,函數名就是變量名稱,函數體就是該變量的值。我們知道,變量是可以賦值給其他變量的,因此函數也是可以被當做返回值返回的,並且可以賦值給其他變量。

def outer(x):
    def inner(y):
        print(x+y)
    return inner
f1 = outer(10)
f2 = outer(20)

f1(100)
f2(100)

上面操作的執行結果是:

110
120

我們知道局部變量的作用域是在定義它的函數體內部,局部變量在函數執行時進行聲明,函數執行完畢則會被釋放。上面也提到過了,函數也是一個變量,那麼嵌套函數內部定義的函數也是一個局部變量,也就是說嵌套函數每調用一次,其內部的函數都會被定義一次。因此,在上面的示例中

f1 = outer(10)
f2 = outer(20)

對於f1和f2而言,兩次調用嵌套函數outer並返回的內部函數inner是不同的,且它們取到的x值也是不同的。從表面上來看f1和f2相當於把x分別替換成了10和20:

def f1(y):
    print(10+y)

def f2(y):
    print(20+y)

但實際上不是這樣的,f1和f2還是這樣的:

def f1(y):
    print(x+y)

def f2(y):
    print(x+y)

f1和f2被調用時,y的值是通過參數傳遞進來的(100),而x還是個變量。inner函數會在自己的函數體內部查找該局部變量x,發現沒找到,然後去查找它外層的函數局部變量x,找到了。這裡好像出現問題了,因為之前說過了局部變量會在函數執行結束後被釋放,那麼f1和f2被調用時outer函數已經執行完了,理論上x的值應該被釋放了才對啊,為什麼還能引用x的值?其實,這就是閉包的作用。

閉包的定義

如果在一個內部函數中,引用了外部非全局作用域中的變量,那麼這個內部函數就被認為是閉包(closure)。

在一些語言中,在函數中可以(嵌套)定義另一個函數時,如果內部的函數應用了外部函數的變量,則可能產生閉包。閉包可以用來在一個函數與一組“私有”變量之間創建關聯關系。在該內部函數被多次調用的過程中,這些私有變量能夠保持其持久性。在支持將函數作為對象使用的編程語言中,一般都支持閉包,比如:Python、PHP、Javascript等。

閉包就是根據不同的配置信息得到不同的結果。專業解釋是:閉包(closure)是詞法閉包(Lexical Closure)的簡稱,是引用了自由變量的函數。這個被引用的自由變量將和這個函數一同存在,即使已經離開了創造它的環境也不例外。所以,有另一種說法認為閉包是由函數和與其相關的應用環境組合而成的實體。

閉包的工作原理

Ptyhon支持一種特性叫做函數閉包(function closres),它的工作原理是:在非全局(global)作用域(函數)中定義inner函數時,這個inner函數會記錄下外層函數的namespaces(外層函數作用域的locals,其中包括外層函數局部作用域中的所有變量),可以稱作:定義時狀態,inner函數可以通過__closure__(早期版本中為func_closure)這個屬性來獲得inner函數外層嵌套函數的namespaces。其實我們可以通過打印一個函數的__closesure__屬性值是否為None來判斷閉包是否發生。

閉包與裝飾器

其實裝飾器就是一種閉包,或者說裝飾器是閉包的一種經典應用。區別在於,裝飾器的參數(配置信息)是一個函數或類,專門對類或函數進行加工、處理和功能增強。關於裝飾器,我們會在後面詳細介紹。

三、匿名函數


在Python中有兩種定義函數的方式:

  • 通過def關鍵字定義的函數:這是最常用的方式,前面已經介紹過
  • 通過lambda關鍵字定義的匿名函數:這是本次要說的主角

lambda作為一個關鍵字,作為引入表達式的語法。與def定義的函數相比較而言,lambda是單一的表達式,而不是語句塊。也就是說,我們僅僅能夠在lambda中封裝有限的業務邏輯(通常只是一個表達式),這樣設計的目的在於:讓lambda純粹為了編寫簡單的函數(通常稱為小函數)而設計,def則專注於處理更大的業務。

1. 匿名函數的定義

語法:
lambda argument1, argument2, ... argumentN :expression using argments

冒號左邊是函數的參數,冒號右邊是一個整合參數並計算返回值的表達式。

實例:定義一個求兩個數之和的函數

def函數

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

lambda函數

lambda x, y: x+y

2. 匿名函數的調用方式:

調用方式1:匿名函數也是一個函數對象,可以將匿名函數賦值給一個變量,然後通過在這個變量後加上一對小括號來調用:

add = lambda x, y: x+y
sum = add(1, 2)

調用方式2:直接在lambda函數後加上一堆小括號調用:

sum = (lambda x, y: x+y)(1, 3)

3. 匿名函數的特性:

  • 函數體只能包含一個表達式
  • 不能有return語句(表達式的值就是它的返回值)
  • 參數個數不限,可以有0個、1個或多個

4. 什麼時候用匿名函數

從上面提到的“匿名函數的調用方式”來看,匿名函數貌似沒有什麼卵用,反而可讀性更差了。那麼匿名函數在Python中存在的意義是什麼呢?匿名函數一般應用於函數式編程中,在Python中通常是指與高階函數的配合使用--把匿名函數當做高階函數的參數來使用,下面的高階函數實例中會用到。

四、高階函數


我們上面已經提到過:函數名也是變量,函數名就是指向函數的變量。並且我們已經知道:變量是可以作為參數傳遞給函數的。由此,我們得出一個結論:函數是一個接受另外一個函數作為參數的,而這種函數就稱為高階函數(Higher-order function)。

1. 自定義高階函數

我們來自定義一個高階函數,這個函數用於求兩個數的和,同時接收一個函數用於在求和之前對兩個數值參數做一些額外的處理(如:取絕對值、求平方或其他任意操作)

def nb_add(x, y, f):
    return f(x) + f(y)

其中x,y是用於求和的兩個數值參數,f是對x,y進行處理的函數。我們試著先給f傳遞一個內置的abs(取絕對值)函數,也就是說先對x和y分別取絕對值,然後再相加:

result = nb_add(10, -20, abs)
print(result)

運行結果是:30

我們來自定義一個求平方的方法,然後傳遞給f試試:

def pow2(x):
    return pow(x, 2)

result = nb_add(10, -20, pow2)
print(result)

輸出結果是:500

我們發現上面定義的pow2(x)函數的函數體只有一個表達式,因此我們完全可以不單獨定義該函數而使用匿名函數來實現,這樣可以使diamante變得更簡潔:

def nb_add(x, y, f):
    return f(x) + f(y)

result = nb_add(10, 20, lambda x: pow(x, 2))
print(result)

2. 常見內置高階函數

Python內置了一些非常有用的高階函數,下面我們來看看常見的幾個:

map函數

map(function, iterable, ...)

map函數的參數說明:

  • map函數接收兩類參數:函數和可迭代對象(Iterable)
  • 第一個參數是函數,後面的參數都是可迭代對象。
  • 處理函數的參數個數需要與傳入的可迭代對象參數的個數對應,否則會報錯。
  • 如果傳入的可迭代對象參數有多個,且每個iterable元素數量不相等時,結果中的元素個數與最短的那個iterable的元素個數一致。

map函數的作用是:

將傳入的函數依次作用到可迭代對象的每個元素,並把結果作為新的迭代器對象(Iterator)返回(Python2.x中會直接返回一個列表)。

實例1:計算給定列表中的每個元素的平方值並放回一個新的列表

def pow2(x):
    return x * x
    
L = [1, 2, 3, 4, 5, 6]

list1 = list(map(pow2, L))
print(list1)

輸出結果為:[1, 4, 9, 16, 25, 36]

上面已經演示過,pow2()可以直接使用匿名函數:

L = [1, 2, 3, 4, 5, 6]

list1 = list(map(lambda x: pow(x, 2), L))
print(list1)

可見map函數作為高階函數,事實上是把運算規則抽象了,因此,我們不僅可以計算簡單的f(x)=x*x,還可以計算任意復雜的函數。

實例2:計算兩個序列中對應元素的和並保存至一個新的列表中

L = [1, 2, 3, 4, 5, 6]
T = (7, 8, 9, 10)

list1 = list(map(lambda x, y: x+y, L, T))
print(list1)

輸出結果為:[8, 10, 12, 14]

reduce函數

這裡需要說明一下:reduce函數在Python 2.x中跟map函數一樣都是Python內置函數,Python 3.x中已經被轉移到functools模塊了。

reduce(function, sequence, initializer=None)

reduce函數的參數說明:

  • 接收一個函數參數、一個序列參數和一個可選的initalizer參數
  • 如果可選參數initializer被提供,則相當於把它作為sequence的一個元素插入sequence的首部

reduce函數的作用是:

把一個函數作用在指定的序列上,這個函數必須接收兩個參數,然後把計算結果繼續和序列的下一個元素做累計計算,最終返回一個結果。簡單來講,就是對一個序列中的元素做聚合運算。

實例1:計算指定數列中所有元素的和

from functools import reduce

L = [1, 2, 3, 4, 5]
sum1 = reduce(lambda x, y: x + y, L)
print(sum1)

sum2 = reduce(lambda x, y: x + y, L, 6)
print(sum2)

輸出結果為:

15
21

這個過程相當於:(((1 + 2) + 3) + 4) + 5

實例2:將數字字符串轉成int

from functools import reduce

def fn(x, y):
    return int(x)*10 + int(y)

num = reduce(lambda x, y: int(x)*10 + y, '12345')
print(num)

也可以封裝成一個函數:

from functools import reduce

def str2int(s):
    return reduce(lambda x, y: int(x)*10 + int(y), s)

num = str2int('12345')
print(num)

也可以先通過map函數將字符串中的字符轉成int,然後再通過reduce進行運算:

from functools import reduce

def str2int(s):
    def char2num(c):
        return {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}[c]
    return reduce(lambda x, y: x*10 + y, map(char2num, s))

num = str2int('12345')
print(num)

其實char2sum也可以用匿名函數來實現,但是可讀性不太好。另外我舉這個例子的本義不是為了單純的演示map/reduce/匿名函數的使用,而是想說明嵌套函數與高階函數綜合使用的場景,這在某些場景下可以使代碼邏輯變得更清晰。

filter函數

filter(function, iterable)

filter函數的參數說明:

  • filter函數接收一個函數參數和一個可迭代對象參數,函數參數可以為None
  • 函數的返回值(True或False)用於判斷可迭代對象的當前元素是否要保留

filter函��的作用是:

用於過濾可迭代對象,具體過程是:把傳入的函數依次作用於可迭代對象的每個元素,如果函數返回值為Ture則保留該元素,如果返回值為False則丟棄該元素,並最終把保留的元素作為一個iterator(迭代器)返回。如果function是None,則根據可迭代對象各元素的真值測試結果決定是否保留該元素。

與Python內置的filter函數作用剛好相反的函數是itertools.filterfalse(function, sequence),它用於過濾出序列中通過function函數計算結果為False的元素。

實例1:分別打印出指定列表中的奇數和偶數

from itertools import filterfalse

L = [1, 2, 3, 4, 5, 6, 7, 8, 9]

odd_num = list(filter(lambda x: x % 2 == 1, L))
even_num = list(filterfalse(lambda x: x%2 == 1, L))
print('奇數:', odd_num)
print('偶數:', even_num)

輸出結果:

奇數: [1, 3, 5, 7, 9]
偶數: [2, 4, 6, 8]

實例2:刪除序列中的空字符串

L = ['ABC', '', 'DEF', '  ', '1233', None]

list1 = list(filter(None, L))
print(list1)

輸出結果為:['ABC', 'DEF', ' ', '1233']

由於第4個由3個空白字符組成的字符串的真值測試結果為True,因此它還是會被保留。被看來還是需要傳遞個函數參數才行:

L = ['ABC', '', 'DEF', '  ', '1233', None]

list1 = list(filter(lambda s: s and s.strip(), L))
print(list1)

輸出結果:['ABC', 'DEF', '1233']

sorted函數

sorted(iterable[, key][, reverse])

sorted函數的參數說明:

  • sorted函數可以接收一個可迭代對象iterable作為必選參數,還可以接收兩個可選參數key和reverse,但是這兩個可選參數如果要提供的話,需要作為關鍵字參數進行傳遞;
  • 參數key接收的是一個函數名,該函數用來實現自定義排序;如,要按照絕對值大小進行排序:key=abs
  • 參數reverse接收的是一個布爾值:如果reverse=Ture,表示倒敘排序,如果reverse=False,表示正序排序;reverse默認值為False

關於參數key的進一步說明: 排序的核心是比較兩個元素的大小。如果要比較的是兩個數字,我們可以直接比較;如果是字符串,也可以按照ASCII碼的大小進行比較。但是,如果要比較的元素是兩個序列或dict等復雜數據呢?這時,我們可能需要指定一個計算“用於比較的值”的運算規則,比如我們指定取兩個dict中的某個共同的key對應的值來進行比較,又比如我們指定用將兩個字符串都轉換為小寫或者大寫後的結果值進行比較。其實說簡單點,參數key這個函數作用是:計算/獲取用來進行比較的值。如果我們需要自定義這個函數時,需要注意該函數應該有一個參數,這個參數接收的就是可迭代對象中每個元素的值。

sorted函數的作用是:

對可迭代對象iterable中的元素進行排序,並將排序結果作為一個新的list返回。

實例1:數字列表排序

list1 = sorted([10, 9, -21, 13, -30])
list2 = sorted([10, 9, -21, 13, -30], key=abs)
list3 = sorted([10, 9, -21, 13, -30], key=abs, reverse=True)

print(list1)
print(list2)
print(list3)

輸出結果:

[-30, -21, 9, 10, 13]
[9, 10, 13, -21, -30]
[-30, -21, 13, 10, 9]

實例2:字符串列表排序

list1 = sorted(['how', 'What', 'check', 'Zero'])
list2 = sorted(['how', 'What', 'check', 'Zero'], key=lower)
list3 = sorted(['how', 'What', 'check', 'Zero'], key=lower, reverse=True)

print(list1)
print(list2)
print(list3)

輸出結果:

['What', 'Zero', 'check', 'how']
['check', 'how', 'What', 'Zero']
['Zero', 'What', 'how', 'check']

實例3:tuple列表排序
假設我們用一組tuple表示姓名和年齡,然後用sorted()函數分別按姓名升序和年齡降序進行排序:

def sort_by_name(t):
    return t[0]

def sort_by_age(t):
    return t[1]

L = [('Tom', 18), ('Jerry', 15), ('Peter', 16), ('John', 20)]
list1 = sorted(L, key=sort_by_name)
list2 = sorted(L, key=sort_by_age, reverse=True)
print('sort by name asc: ', list1)
print('sort by age desc: ', list2)

輸出結果:

sort by name asc:  [('Jerry', 15), ('John', 20), ('Peter', 16), ('Tom', 18)]
sort by age desc:  [('John', 20), ('Tom', 18), ('Peter', 16), ('Jerry', 15)]

實例4:字典內容排序

對字典排序的方法有很多中,但核心思想都是一樣的:把dict中的key或value或item分離出來放到一個list中,然後在對這個list進行排序,從而間接實現對dict的排序。

D = {'Tom': 18, 'Jerry': 15, 'Peter': 16, 'John': 20}

list1 = sorted(D.items(), key=lambda d: d[0])
list2 = sorted(D.items(), key=lambda d: d[1], reverse=True)

print('sort by key asc:', list1)
print('sort by value desc:', list2)

輸出結果:

sort by key asc: [('Jerry', 15), ('John', 20), ('Peter', 16), ('Tom', 18)]
sort by value desc: [('John', 20), ('Tom', 18), ('Peter', 16), ('Jerry', 15)]

五、內置函數


Python解釋器有許多內置的函數和類型,有一些之前已經用到過,比如:

  • 數學函數:abs()、max()、min()、pow()、sum()
  • 類型轉換函數:int()、float()、str()、bool()、list()、tuple()、dict()、set()
  • 進制轉換函數:bin()、oct()、hex()
  • 高階函數:map()、filter()、sorted()
  • 打開文件用的函數:的open()
  • 輸入與輸出函數:input()、raw_input()、print()
  • 獲取對象內存地址的函數:id()

這些函數我們在之前的文章中基本都演示了,不在此贅述。關於他們的詳細說明以及其它內置函數的使用可以參考下面給出的列表及官方文檔連接地址。

Python 3相對於Python 2的內置函數有些變動:

  • 新增了一些內置方法,如:ascii()、bytes()、exec()
  • 刪除了一些內置方法, 如:cmp()、execfile()
  • 移動了一些內置方法,如:reduce()被移動到了functools模塊下
  • 修改了一些內置方法,如:sorted()函數在Python 3.5的文檔中麼有再提到cmp參數了(貌似用不到這個參數)

Python 3中的高階函數還有一個比較大的改變,如map()和filter()在Python 2中是直接返回一個列表(list),而在Python 3中是返回一個迭代器(Iterator)。

Python 3.5內置函數列表(官方文檔地址)

Python 2.7內置函數列表(官方文檔地址)

六、總結


這裡講了分別講了Python中函數的一些高級應用,如果能把這些內容整合起來靈活運用會發揮很大的威力。比如後面要說到的裝飾就是高階函數、嵌套函數以及閉包的一個典型應用。

Copyright © Linux教程網 All Rights Reserved