已經介紹了Python中函數的定義、函數的調用、函數的參數以及變量的作用域等內容,現在來說下函數的一些高級特性:
函數是可以被調用的,且一個函數內部可以調用其他函數。如果一個函數在內部調用本身,這個函數就是一個遞歸函數。函數遞歸調用的過程與循環相似,而且理論上,所有的遞歸函數都可以寫成循環的方式,但是遞歸函數的優點是定義簡單,邏輯清晰。遞歸和循環都是一個重復的操作的過程,這些重復性的操作必然是需要有一定的規律性的。另外,很明顯遞歸函數也需要一個結束條件,否則就會像死循環一樣遞歸下去,直到由於棧溢出而被終止(這個下面介紹)。
可見,要實現一個遞歸函數需要確定兩個要素:
思路有兩個:
def fact(n):
if n == 0:
return 1
result = 1
while n >= 1:
result *= n
n -= 1
return result
先來確定遞歸函數的兩個要素:
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
遞歸函數的優點:
定義簡單、邏輯清晰。
遞歸函數的缺點:
效率並不高且需要注意防止棧溢出。
其他特點:
大家會發現上面實現的遞歸函數在運算的過程中n是逐漸減小的,也就是說問題規模應該是逐層減少的。
下面我們來總結寫遞歸的特性:
因為在計算中,函數調用時通過棧(stack,特點是後進先出--LIFO)這種數據結構實現的,每當進入一個函數調用,棧就會加一層棧針,每當函數返回,棧就會減少一層棧針。由於棧的大小不是無限的,所有遞歸調用的次數過多,會導致棧溢出。關於堆棧的介紹可以看下這裡:<<內存堆和棧的區別>>。
每種編程語言都對遞歸函數可遞歸的深度有限制(可以看看這裡),有些是跟相應內存空間的分配有關(因為棧是在內存空間中的),如Java。Python中對遞歸的深度限制默認為1000,可以通過sys.getrecursionlimit()
函數來獲取該值,超過這個深度會報錯:RecursionError: maximum recursion depth exceeded in comparison
。當然也可以通過sys.setrecursionlimit(n)
來設置新的限制值。
嵌套函數是指在函數內部定義一個函數,這些函數都遵循各自的作用域和生命周期規則。
來看個例子:
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
嵌套函數查找變量的順序是:先查找自己函數體內部是否包含該變量,如果包含則直接應用,如果不包含則查找外層函數體內是否包含該函數,依次向外。
首先要說明一個問題:函數名其實也是一個變量,我們通過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中有兩種定義函數的方式:
lambda作為一個關鍵字,作為引入表達式的語法。與def定義的函數相比較而言,lambda是單一的表達式,而不是語句塊。也就是說,我們僅僅能夠在lambda中封裝有限的業務邏輯(通常只是一個表達式),這樣設計的目的在於:讓lambda純粹為了編寫簡單的函數(通常稱為小函數)而設計,def則專注於處理更大的業務。
lambda argument1, argument2, ... argumentN :expression using argments
冒號左邊是函數的參數,冒號右邊是一個整合參數並計算返回值的表達式。
def函數
def add(x, y):
return x + y
lambda函數
lambda x, y: x+y
調用方式1:匿名函數也是一個函數對象,可以將匿名函數賦值給一個變量,然後通過在這個變量後加上一對小括號來調用:
add = lambda x, y: x+y
sum = add(1, 2)
調用方式2:直接在lambda函數後加上一堆小括號調用:
sum = (lambda x, y: x+y)(1, 3)
從上面提到的“匿名函數的調用方式”來看,匿名函數貌似沒有什麼卵用,反而可讀性更差了。那麼匿名函數在Python中存在的意義是什麼呢?匿名函數一般應用於函數式編程中,在Python中通常是指與高階函數的配合使用--把匿名函數當做高階函數的參數來使用,下面的高階函數實例中會用到。
我們上面已經提到過:函數名也是變量,函數名就是指向函數的變量。並且我們已經知道:變量是可以作為參數傳遞給函數的。由此,我們得出一個結論:函數是一個接受另外一個函數作為參數的,而這種函數就稱為高階函數(Higher-order function)。
我們來自定義一個高階函數,這個函數用於求兩個數的和,同時接收一個函數用於在求和之前對兩個數值參數做一些額外的處理(如:取絕對值、求平方或其他任意操作)
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)
Python內置了一些非常有用的高階函數,下面我們來看看常見的幾個:
map(function, iterable, ...)
map函數的參數說明:
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函數在Python 2.x中跟map函數一樣都是Python內置函數,Python 3.x中已經被轉移到functools模塊了。
reduce(function, sequence, initializer=None)
reduce函數的參數說明:
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(function, iterable)
filter函數的參數說明:
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(iterable[, key][, reverse])
sorted函數的參數說明:
關於參數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解釋器有許多內置的函數和類型,有一些之前已經用到過,比如:
這些函數我們在之前的文章中基本都演示了,不在此贅述。關於他們的詳細說明以及其它內置函數的使用可以參考下面給出的列表及官方文檔連接地址。
Python 3相對於Python 2的內置函數有些變動:
Python 3中的高階函數還有一個比較大的改變,如map()和filter()在Python 2中是直接返回一個列表(list),而在Python 3中是返回一個迭代器(Iterator)。
這裡講了分別講了Python中函數的一些高級應用,如果能把這些內容整合起來靈活運用會發揮很大的威力。比如後面要說到的裝飾就是高階函數、嵌套函數以及閉包的一個典型應用。