Python進階之函數式編程
函數式編程
函數是Python內建支持的一種封裝,我們通過把大段代碼拆成函數,通過一層一層的函數調用,就可以把復雜任務分解成簡單的任務,這種分解可以稱之為面向過程的程序設計。函數就是面向過程的程序設計的基本單元。
而函數式編程(請註意多了一個“式”字)——Functional Programming,雖然也可以歸結到面向過程的程序設計,但其思想更接近數學計算。
我們首先要搞明白計算機(Computer)和計算(Compute)的概念。
在計算機的層次上,CPU執行的是加減乘除的指令代碼,以及各種條件判斷和跳轉指令,所以,匯編語言是最貼近計算機的語言。
而計算則指數學意義上的計算,越是抽象的計算,離計算機硬件越遠。
對應到編程語言,就是越低級的語言,越貼近計算機,抽象程度低,執行效率高,比如C語言;越高級的語言,越貼近計算,抽象程度高,執行效率低,比如Lisp語言。
函數式編程就是一種抽象程度很高的編程範式,純粹的函數式編程語言編寫的函數沒有變量,因此,任意一個函數,只要輸入是確定的,輸出就是確定的,這種純函數我們稱之為沒有副作用。而允許使用變量的程序設計語言,由於函數內部的變量狀態不確定,同樣的輸入,可能得到不同的輸出,因此,這種函數是有副作用的。
函數式編程的一個特點就是,允許把函數本身作為參數傳入另一個函數,還允許返回一個函數!
Python對函數式編程提供部分支持。由於Python允許使用變量,因此,Python不是純函數式編程語言。
高階函數
高階函數英文叫Higher-order function。把函數作為參數傳入,這樣的函數稱為高階函數,函數式編程就是指這種高度抽象的編程範式。
具體概念請參閱廖雪峰老師的官網:https://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000/0014317849054170d563b13f0fa4ce6ba1cd86e18103f28000
python把函數作為參數
編寫了一個簡單的高階函數:
def add(x, y, f): return f(x) + f(y)
如果傳入abs作為參數f的值:
add(-5, 9, abs)
根據函數的定義,函數執行的代碼實際上是:
abs(-5) + abs(9)
由於參數 x, y 和 f 都可以任意傳入,如果 f 傳入其他函數,就可以得到不同的返回值。
如:
import math def add(x, y, f): return f(x) + f(y) print add(25, 9, math.sqrt)
python中map()函數
map()是 Python 內置的高階函數,它接收一個函數 f 和一個 list,並通過把函數 f 依次作用在 list 的每個元素上,得到一個新的 list 並返回。
例如,對於list [1, 2, 3, 4, 5, 6, 7, 8, 9]
如果希望把list的每個元素都作平方,就可以用map()函數:
因此,我們只需要傳入函數f(x)=x*x,就可以利用map()函數完成這個計算:
def f(x): return x*x print map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9])
輸出結果:
[1, 4, 9, 10, 25, 36, 49, 64, 81]
註意:map()函數不改變原有的 list,而是返回一個新的 list。
利用map()函數,可以把一個 list 轉換為另一個 list,只需要傳入轉換函數。
由於list包含的元素可以是任何類型,因此,map() 不僅僅可以處理只包含數值的 list,事實上它可以處理包含任意類型的 list,只要傳入的函數f可以處理這種數據類型。
例子:
假設用戶輸入的英文名字不規範,沒有按照首字母大寫,後續字母小寫的規則,請利用map()函數,把一個list(包含若幹不規範的英文名字)變成一個包含規範英文名字的list:
輸入:[‘adam‘, ‘LISA‘, ‘barT‘]
輸出:[‘Adam‘, ‘Lisa‘, ‘Bart‘]
def format_name(s): return s[0].upper() + s[1:].lower() print map(format_name, [‘adam‘, ‘LISA‘, ‘barT‘])
python中reduce()函數
reduce()函數也是Python內置的一個高階函數。reduce()函數接收的參數和 map()類似,一個函數 f,一個list,但行為和 map()不同,reduce()傳入的函數 f 必須接收兩個參數,reduce()對list的每個元素反復調用函數f,並返回最終結果值。
例如,編寫一個f函數,接收x和y,返回x和y的和:
def f(x, y): return x + y
調用 reduce(f, [1, 3, 5, 7, 9])時,reduce函數將做如下計算:
先計算頭兩個元素:f(1, 3),結果為4; 再把結果和第3個元素計算:f(4, 5),結果為9; 再把結果和第4個元素計算:f(9, 7),結果為16; 再把結果和第5個元素計算:f(16, 9),結果為25; 由於沒有更多的元素了,計算結束,返回結果25。
上述計算實際上是對 list 的所有元素求和。雖然Python內置了求和函數sum(),但是,利用reduce()求和也很簡單。
reduce()還可以接收第3個可選參數,作為計算的初始值。如果把初始值設為100,計算:
reduce(f, [1, 3, 5, 7, 9], 100)
結果將變為125,因為第一輪計算是:
計算初始值和第一個元素:f(100, 1),結果為101。
例子:
Python內置了求和函數sum(),但沒有求積的函數,請利用recude()來求積:
輸入:[2, 4, 5, 7, 12]
輸出:2*4*5*7*12的結果
def prod(x, y): return x * y print reduce(prod, [2, 4, 5, 7, 12])
python中filter()函數
filter()函數是 Python 內置的另一個有用的高階函數,filter()函數接收一個函數 f 和一個list,這個函數 f 的作用是對每個元素進行判斷,返回 True或 False,filter()根據判斷結果自動過濾掉不符合條件的元素,返回由符合條件元素組成的新list。
例如,要從一個list [1, 4, 6, 7, 9, 12, 17]中刪除偶數,保留奇數,首先,要編寫一個判斷奇數的函數:
def is_odd(x): return x % 2 == 1
然後,利用filter()過濾掉偶數:
filter(is_odd, [1, 4, 6, 7, 9, 12, 17])
結果:[1, 7, 9, 17]
利用filter(),可以完成很多有用的功能,例如,刪除 None 或者空字符串:
def is_not_empty(s): return s and len(s.strip()) > 0 filter(is_not_empty, [‘test‘, None, ‘‘, ‘str‘, ‘ ‘, ‘END‘])
結果:[‘test‘, ‘str‘, ‘END‘]
註意: s.strip(rm) 刪除 s 字符串中開頭、結尾處的 rm 序列的字符。
當rm為空時,默認刪除空白符(包括‘\n‘, ‘\r‘, ‘\t‘, ‘ ‘),如下:
a = ‘ 123‘ a.strip()
結果: ‘123‘
a=‘\t\t123\r\n‘ a.strip()
結果:‘123‘
例子:
請利用filter()過濾出1~100中平方根是整數的數,即結果應該是:
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
import math def is_sqr(x): r = int(math.sqrt(x)) return r*r==x print filter(is_sqr, range(1, 101))
python中自定義排序函數
Python內置的 sorted()函數可對list進行排序:
>>>sorted([36, 5, 12, 9, 21])
[5, 9, 12, 21, 36]
但 sorted()也是一個高階函數,它可以接收一個比較函數來實現自定義排序,比較函數的定義是,傳入兩個待比較的元素 x, y,如果 x 應該排在 y 的前面,返回 -1,如果 x 應該排在 y 的後面,返回 1。如果 x 和 y 相等,返回 0。
因此,如果我們要實現倒序排序,只需要編寫一個reversed_cmp函數:
def reversed_cmp(x, y): if x > y: return -1 if x < y: return 1 return 0
這樣,調用 sorted() 並傳入 reversed_cmp 就可以實現倒序排序:
>>> sorted([36, 5, 12, 9, 21], reversed_cmp)
[36, 21, 12, 9, 5]
sorted()也可以對字符串進行排序,字符串默認按照ASCII大小來比較:
>>> sorted([‘bob‘, ‘about‘, ‘Zoo‘, ‘Credit‘]) [‘Credit‘, ‘Zoo‘, ‘about‘, ‘bob‘]
‘Zoo‘排在‘about‘之前是因為‘Z‘的ASCII碼比‘a‘小。
例子:
對字符串排序時,有時候忽略大小寫排序更符合習慣。請利用sorted()高階函數,實現忽略大小寫排序的算法。
輸入:[‘bob‘, ‘about‘, ‘Zoo‘, ‘Credit‘]
輸出:[‘about‘, ‘bob‘, ‘Credit‘, ‘Zoo‘]
def cmp_ignore_case(s1, s2): s1=s1.upper() s2=s2.upper() if s1<s2: return -1 else: return 1 print sorted([‘bob‘, ‘about‘, ‘Zoo‘, ‘Credit‘],cmp_ignore_case)
python中返回函數
Python的函數不但可以返回int、str、list、dict等數據類型,還可以返回函數!
例如,定義一個函數 f(),我們讓它返回一個函數 g,可以這樣寫:
def f(): print ‘call f()...‘ # 定義函數g: def g(): print ‘call g()...‘ # 返回函數g: return g
仔細觀察上面的函數定義,我們在函數 f 內部又定義了一個函數 g。由於函數 g 也是一個對象,函數名 g 就是指向函數 g 的變量,所以,最外層函數 f 可以返回變量 g,也就是函數 g 本身。
調用函數 f,我們會得到 f 返回的一個函數:
>>> x = f() # 調用f() call f()... >>> x # 變量x是f()返回的函數: <function g at 0x1037bf320> >>> x() # x指向函數,因此可以調用 call g()... # 調用x()就是執行g()函數定義的代碼
請註意區分返回函數和返回值:
def myabs(): return abs # 返回函數 def myabs2(x): return abs(x) # 返回函數調用的結果,返回值是一個數值
返回函數可以把一些計算延遲執行。例如,如果定義一個普通的求和函數:
def calc_sum(lst): return sum(lst)
調用calc_sum()函數時,將立刻計算並得到結果:
>>> calc_sum([1, 2, 3, 4])
10
但是,如果返回一個函數,就可以“延遲計算”:
def calc_sum(lst): def lazy_sum(): return sum(lst) return lazy_sum
# 調用calc_sum()並沒有計算出結果,而是返回函數:
>>> f = calc_sum([1, 2, 3, 4]) >>> f <function lazy_sum at 0x1037bfaa0>
# 對返回的函數進行調用時,才計算出結果:
>>> f()
10
由於可以返回函數,我們在後續代碼裏就可以決定到底要不要調用該函數。
例子:
請編寫一個函數calc_prod(lst),它接收一個list,返回一個函數,返回函數可以計算參數的乘積。
先定義能計算乘積的函數,再將此函數返回。
參考代碼:
def calc_prod(lst): def lazy_prod(): def f(x, y): return x * y return reduce(f, lst, 1) return lazy_prod f = calc_prod([1, 2, 3, 4]) print f()
python中閉包
在函數內部定義的函數和外部定義的函數是一樣的,只是他們無法被外部訪問:
def g(): print ‘g()...‘ def f(): print ‘f()...‘ return g
將 g 的定義移入函數 f 內部,防止其他代碼調用 g:
def f(): print ‘f()...‘ def g(): print ‘g()...‘ return g
但是,考察上一小節定義的 calc_sum 函數:
def calc_sum(lst): def lazy_sum(): return sum(lst) return lazy_sum
註意: 發現沒法把 lazy_sum 移到 calc_sum 的外部,因為它引用了 calc_sum 的參數 lst。
像這種內層函數引用了外層函數的變量(參數也算變量),然後返回內層函數的情況,稱為閉包(Closure)。
閉包的特點是返回的函數還引用了外層函數的局部變量,所以,要正確使用閉包,就要確保引用的局部變量在函數返回後不能變。舉例如下:
# 希望一次返回3個函數,分別計算1x1,2x2,3x3: def count(): fs = [] for i in range(1, 4): def f(): return i*i fs.append(f) #此處加入到數組中的是函數引用,並非函數值 return fs f1, f2, f3 = count() #所以此處可以f1,f2,f3接到的是函數引用,所以下文可以調用f1()
你可能認為調用f1(),f2()和f3()結果應該是1,4,9,但實際結果全部都是 9(請自己動手驗證)。
原因就是當count()函數返回了3個函數時,這3個函數所引用的變量 i 的值已經變成了3。由於f1、f2、f3並沒有被調用,所以,此時他們並未計算 i*i,當 f1 被調用時:
>>> f1() 9 # 因為f1現在才計算i*i,但現在i的值已經變為3
註意:
1)s.append(f),只是將每一個f()的引用保存進了list,並沒有進行對於i的計算,所以導致最後在運行了f(1)之後,i已經變為了3,所以會一樣結果都為9.只要將這裏改為 fs.append(f())即可,這樣就在這一步的時候已經進行了i*i的運算,將結果保存了,通過這一題可以註意到在python這門語言之中,f與f()的大區別。
2)f1, f2, f3 = count(),這句話的意思是f1=count()[0],f2=count()[1],f3=count()[2].這種賦值語句只能用於數組,一一對應。
因此,返回函數不要引用任何循環變量,或者後續會發生變化的變量。
例子:
返回閉包不能引用循環變量,請改寫count()函數,讓它正確返回能計算1x1、2x2、3x3的函數。
考察下面的函數 f:
def f(j): def g(): return j*j return g
它可以正確地返回一個閉包g,g所引用的變量j不是循環變量,因此將正常執行。
在count函數的循環內部,如果借助f函數,就可以避免引用循環變量i。
參考代碼:
def count(): fs = [] for i in range(1, 4): def f(j): def g(): return j*j return g #函數f()返回的是g函數 r=f(i) #變量r接到的也是函數引用 fs.append(r) #所以此處加入到數組的也是函數引用 return fs f1, f2, f3 = count() #所以此處函數返回的也是函數引用 print f1(), f2(), f3() #所以可以調用f1(), f2(), f3()
python中匿名函數
高階函數可以接收函數做參數,有些時候,我們不需要顯式地定義函數,直接傳入匿名函數更方便。
在Python中,對匿名函數提供了有限支持。還是以map()函數為例,計算 f(x)=x2 時,除了定義一個f(x)的函數外,還可以直接傳入匿名函數:
>>> map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9]) [1, 4, 9, 16, 25, 36, 49, 64, 81]
通過對比可以看出,匿名函數 lambda x: x * x 實際上就是:
def f(x): return x * x
關鍵字lambda 表示匿名函數,冒號前面的 x 表示函數參數。
匿名函數有個限制,就是只能有一個表達式,不寫return,返回值就是該表達式的結果。
使用匿名函數,可以不必定義函數名,直接創建一個函數對象,很多時候可以簡化代碼:
>>> sorted([1, 3, 9, 5, 0], lambda x,y: -cmp(x,y)) [9, 5, 3, 1, 0]
返回函數的時候,也可以返回匿名函數:
>>> myabs = lambda x: -x if x < 0 else x >>> myabs(-1) 1 >>> myabs(1) 1
例子:
利用匿名函數簡化以下代碼:
def is_not_empty(s): return s and len(s.strip()) > 0 #s不為空且不是空字符串則返回true filter(is_not_empty, [‘test‘, None, ‘‘, ‘str‘, ‘ ‘, ‘END‘])
定義匿名函數時,沒有return關鍵字,且表達式的值就是函數返回值。
print filter(lambda s: s and len(s.strip())>0, [‘test‘, None, ‘‘, ‘str‘, ‘ ‘, ‘END‘])
裝飾器
在面向對象(OOP)的設計模式中,decorator被稱為裝飾模式。OOP的裝飾模式需要通過繼承和組合來實現,而Python除了能支持OOP的decorator外,直接從語法層次支持decorator。Python的decorator可以用函數實現,也可以用類實現。
decorator可以增強函數的功能,定義起來雖然有點復雜,但使用起來非常靈活和方便。
詳細信息請參考廖雪峰老師的官網:https://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000/0014318435599930270c0381a3b44db991cd6d858064ac0000
python中編寫無參數decorator
Python的 decorator 本質上就是一個高階函數,它接收一個函數作為參數,然後,返回一個新函數。
使用 decorator 用Python提供的 @ 語法,這樣可以避免手動編寫f = decorate(f) 這樣的代碼。
考察一個@log的定義:
def log(f): def fn(x): print ‘call ‘ + f.__name__ + ‘()...‘ return f(x) return fn
對於階乘函數,@log工作得很好:
@log def factorial(n): return reduce(lambda x,y: x*y, range(1, n+1)) print factorial(10)
結果:
call factorial()...
3628800
把@log
放到factorial(n)
函數的定義處,相當於執行了語句:
factorial= log(factorial)
由於log()
是一個decorator,返回一個函數,所以,原來的factorial(n)
函數仍然存在,只是現在同名的factorial變量指向了新的函數,於是調用factorial()
將執行新函數,即在log()
函數中返回的fn(n)
函數。
但是,對於參數不是一個的函數,調用將報錯:
@log def add(x, y): return x + y print add(1, 2)
結果:
Traceback (most recent call last): File "test.py", line 15, in <module> print add(1,2) TypeError: fn() takes exactly 1 argument (2 given)
因為 add() 函數需要傳入兩個參數,但是 @log 寫死了只含一個參數的返回函數。
要讓 @log 自適應任何參數定義的函數,可以利用Python的 *args 和 **kw,保證任意個數的參數總是能正常調用:
def log(f): def fn(*args, **kw): print ‘call ‘ + f.__name__ + ‘()...‘ return f(*args, **kw) return fn
現在,對於任意函數,@log 都能正常工作。
例子:
請編寫一個@performance,它可以打印出函數調用的時間。
計算函數調用的時間可以記錄調用前後的當前時間戳,然後計算兩個時間戳的差。
參考代碼:
import time def performance(f): def fn(*args,**kw): t1=time.time() r=f(*args,**kw) t2=time.time() print ‘call%s() in %fs‘%(f.__name__,(t2-t1)) #註意這裏的f.__name__中的短橫是兩個“__”不是一個“_” return r return fn @performance def factorial(n): return reduce(lambda x,y: x*y, range(1, n+1)) print factorial(10)
補充:
1)函數對象有一個__name__
屬性,可以拿到函數的名字:
>>> now.__name__ ‘now‘ >>> f.__name__ ‘now‘
python中編寫帶參數decorator
考察上一節的 @log 裝飾器:
def log(f): def fn(x): print ‘call ‘ + f.__name__ + ‘()...‘ return f(x) return fn
發現對於被裝飾的函數,log打印的語句是不能變的(除了函數名)。
如果有的函數非常重要,希望打印出‘[INFO] call xxx()...‘,有的函數不太重要,希望打印出‘[DEBUG] call xxx()...‘,這時,log函數本身就需要傳入‘INFO‘或‘DEBUG‘這樣的參數,類似這樣:
@log(‘DEBUG‘) def my_func(): pass
把上面的定義翻譯成高階函數的調用,就是:
my_func = log(‘DEBUG‘)(my_func)
上面的語句看上去還是比較繞,再展開一下:
log_decorator = log(‘DEBUG‘) my_func = log_decorator(my_func)
上面的語句又相當於:
log_decorator = log(‘DEBUG‘) @log_decorator def my_func(): pass
所以,帶參數的log函數首先返回一個decorator函數,再讓這個decorator函數接收my_func並返回新函數:
def log(prefix): def log_decorator(f): def wrapper(*args, **kw): print ‘[%s] %s()...‘ % (prefix, f.__name__) return f(*args, **kw) return wrapper return log_decorator @log(‘DEBUG‘) def test(): pass print test()
執行結果:
[DEBUG] test()...
None
對於這種3層嵌套的decorator定義,你可以先把它拆開:
# 標準decorator: def log_decorator(f): def wrapper(*args, **kw): print ‘[%s] %s()...‘ % (prefix, f.__name__) return f(*args, **kw) return wrapper return log_decorator # 返回decorator: def log(prefix): return log_decorator(f)
拆開以後會發現,調用會失敗,因為在3層嵌套的decorator定義中,最內層的wrapper引用了最外層的參數prefix,所以,把一個閉包拆成普通的函數調用會比較困難。不支持閉包的編程語言要實現同樣的功能就需要更多的代碼。
例子:
上一節的@performance只能打印秒,請給 @performace 增加一個參數,允許傳入‘s‘或‘ms‘:
@performance(‘ms‘) def factorial(n): return reduce(lambda x,y: x*y, range(1, n+1))
要實現帶參數的@performance,就需要實現:
my_func = performance(‘ms‘)(my_func)
需要3層嵌套的decorator來實現。
參考代碼:
import time def performance(unit): def perf_decorator(f): def wrapper(*args,**kw): t1=time.time(); r=f(*args,**kw) t2=time.time() t=(t2-t1)*1000 if unit==‘ms‘ else (t2-t1) print "call%s() in %f%s" %(f.__name__,t,unit) return r return wrapper return perf_decorator @performance(‘ms‘) def factorial(n): return reduce(lambda x,y: x*y, range(1, n+1)) print factorial(10)
python中完善decorator
@decorator可以動態實現函數功能的增加,但是,經過@decorator“改造”後的函數,和原函數相比,除了功能多一點外,有沒有其它不同的地方?
在沒有decorator的情況下,打印函數名:
def f1(x): pass print f1.__name__
輸出: f1
有decorator的情況下,再打印函數名:
def log(f): def wrapper(*args, **kw): print ‘call...‘ return f(*args, **kw) return wrapper @log def f2(x): pass print f2.__name__
輸出: wrapper
可見,由於decorator返回的新函數函數名已經不是‘f2‘,而是@log內部定義的‘wrapper‘。這對於那些依賴函數名的代碼就會失效。decorator還改變了函數的__doc__等其它屬性。如果要讓調用者看不出一個函數經過了@decorator的“改造”,就需要把原函數的一些屬性復制到新函數中:
def log(f): def wrapper(*args, **kw): print ‘call...‘ return f(*args, **kw) wrapper.__name__ = f.__name__ wrapper.__doc__ = f.__doc__ return wrapper
這樣寫decorator很不方便,因為我們也很難把原函數的所有必要屬性都一個一個復制到新函數上,所以Python內置的functools可以用來自動化完成這個“復制”的任務:
import functools def log(f): @functools.wraps(f) def wrapper(*args, **kw): print ‘call...‘ return f(*args, **kw) return wrapper
最後需要指出,由於我們把原函數簽名改成了(*args, **kw),因此,無法獲得原函數的原始參數信息。即便我們采用固定參數來裝飾只有一個參數的函數:
def log(f): @functools.wraps(f) def wrapper(x): print ‘call...‘ return f(x) return wrapper
也可能改變原函數的參數名,因為新函數的參數名始終是 ‘x‘,原函數定義的參數名不一定叫 ‘x‘。
例子:
請思考帶參數的@decorator,@functools.wraps應該放置在哪:
def performance(unit): def perf_decorator(f): def wrapper(*args, **kw): ??? return wrapper return perf_decorator
註意@functools.wraps應該作用在返回的新函數上。
參考代碼:
import time, functools def performance(unit): def perf_decorator(f): @functools.wraps(f) #@functools.wraps應該作用在返回的新函數上 def wrapper(*args,**kw): t1=time.time() r=f(*args,**kw) t2=time.time() t=(t2-t1)*1000 if unit==‘ms‘ else (t2-t1) print ‘call%s() in %f%s‘%(f.__name__,t,unit) return r return wrapper return perf_decorator @performance(‘ms‘) def factorial(n): return reduce(lambda x,y: x*y, range(1, n+1)) print factorial.__name__
Python進階之函數式編程