文章目錄
- 函數式編程的一個特點就是,允許把函數本身作爲參數傳入另一個函數,還允許返回一個函數!
1. 高階函數
- 英文名:Higher-order function
1.1 變量可以指向函數
- 以內置的求絕對值函數
abs()
爲例:
>>> abs(-10)
10
# 如果只寫abs呢
>>> abs
<build-in function abs>
# 要獲得函數調用結果,可以把結果賦值給變量
>>> x = abs(-10)
>>> x
10
# 如果把函數本身賦值給變量呢
>>> f = abs
>>> f
<build-in function abs> # 可見變量可以指向函數
# 同時調用函數和調用變量是一樣的
>>> f = abs
>>> f(-10)
10
1.2 傳入函數
- 既然變量可以指向函數,函數的參數可以接受變量,那麼一個函數就可以接受另外一個函數作爲變量,這種函數就叫做高階函數;
# 一個簡單的高階函數
def add(x, y, f):
return f(x) + f(y)
# 調用此高階函數
>>> add(-5, 6, abs)
11
1.3 map/reduce
1.3.1 map()
map()
函數接受兩個參數,一個是函數,另外一個是Iterable
,map
將傳入的函數依次作用到序列的每個元素,並把結果作爲新的Iterator
返回;- 我們有一個函數,要把這個函數作用在一個list
[1, 2, 3, 4, 5, 6, 7, 8, 9]
:
# 用map()方法實現
>>> def f(x):
... return x * x
...
>>> r = map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> list(r) # 由於r是一個Iterator,是惰性序列,故通過list()函數把整個序列算出來,並返回一個list
[1, 4, 9, 16, 25, 36, 49, 64, 81]
# 同時也可以用for循環方法實現
>>> L = []
>>> for n in [1, 2, 3, 4, 5, 6, 7, 8, 9]:
L.append(f(n))
print(L)
# 這種方法比較麻煩,而map一行代碼就可以實現
map()
函數還可以計算複雜的函數,比如,將這個list所有數字轉爲字符串:
>>> list(map(str, [1, 2, 3, 4, 5, 6, 7, 8, 9]))
['1', '2', '3', '4', '5', '6', '7', '8', '9']
1.3.2 reduce()
reduce()
把一個函數作用在一個序列[x1, x2, x3, ...]
上,這個函數必須接受兩個參數,reduce
繼續把結果和序列的下一個元素做累積計算,其效果就是:
reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4)
- 比方說對一個序列求和:
>>> from functools import reduce
>>> def add(x, y):
... return x + y
...
>>> reduce(add, [1, 3, 5, 7, 9])
25
- 當然求和可以用
sum()
函數,沒必要reduce()
,但將序列[1, 3, 5, 7, 9]
變換成整數13579
,就需要reduce
函數:
>>> from functools import reduce
>>> def fn(x, y):
... return x * 10 + y
...
>>> reduce(fn, [1, 3, 5, 7, 9])
13579
- 如果考慮到字符串
str
也是一個序列,對上面稍加修改,配合map()
,就可以寫出把str
轉爲int
的函數:
>>> from functools import reduce
>>> def fn(x, y):
... return x * 10 + y
...
>>> def char2num(s):
... digits = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}
... return digits[s]
...
>>> reduce(fn, map(char2num, '13579'))
13579
- 整理成一個
str2int
函數:
from functools import reduce
DIGITS = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}
def str2int(s):
def fn(x, y):
return x * 10 + y
def char2num(s):
return DIGITS[s]
return reduce(fn, map(char2num, s))
- 還可以用
lambda
函數進一步簡化:
from functools import reduce
DIGITS = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}
def char2num(s):
return DIGITS[s]
def str2int(s):
return reduce(lambda x, y: x * 10 + y, map(char2num, s))
# lambda 函數的用法在後面介紹
1.3.3 練習1
- 利用
map()
函數,把用戶輸入的不規範的英文名字,變爲首字母大寫,其他小寫的規範名字。輸入:['adam', 'LISA', 'barT']
,輸出:['Adam', 'Lisa', 'Bart']
:
>>> def normalize(name):
... return name[0].upper() + name[1:].lower()
>>> L1 = ['adam', 'LISA', 'barT']
>>> L2 = list(map(normalize, L1))
>>> print(L2)
['Adam', 'Lisa', 'Bart']
1.3.4 練習2
- Python提供的
sum()
函數可以接受一個list並求和,請編寫一個prod()
函數,可以接受一個list並利用reduce()
求積:
>>> from functools import reduce
>>> def prod(L):
... def fn(x, y):
... return x * y
... return reduce(fn, L)
>>> prod([1, 3, 5, 7, 9])
945
1.3.5 練習3
- 利用
map
和reduce
編寫一個str2float
函數,把字符串'123.456'
轉換成浮點數123.456
:
>>> from functools import reduce
# 方法一
>>> from math import pow
>>> def str2float(s):
... def fn(x, y):
... length = len(y)
... return int(x) + int(y)/pow(10, length)
... return reduce(fn, list(map(str, s.split('.'))))
...
>>> print(str2float('123.456'))
123.456
# 方法二
>>> def str2float(s):
... def fn(x, y):
... return float(x + '.' + y)
... return reduce(fn, list(map(str, s.split('.'))))
...
>>> print(str2float('123.456'))
123.456
1.4 filter
- python內建的
filter()
函數用來過濾數列,和map()
一樣,filter()
也接受一個函數和一個數列,只不過不用的是,filter()
把傳入的函數依次作用於每個元素,根據返回值是True
還是False
決定保留還是丟棄該元素;
# 在一個list中刪除偶數,只保留奇數
>>> def is_odd(n):
... return n % 2 == 1
...
>>> list(filter(is_odd, [1, 2, 4, 5, 6, 9, 10, 15]))
[1, 5, 9, 15]
# 把一個序列中的空字符串刪掉
>>> def not_empty(s):
... return s and s.strip()
...
>>> list(filter(not_empty, ['A', '', 'B', None, 'C', ' ']))
['A', 'B', 'C']
filter()
函數返回的是一個Iterator
,惰性序列,所以需要list()
函數獲得所有結果並返回list
1.4.1 用filter()
求素數
- 計算素數的一個方法是埃氏篩法,算法說明如下:
- 列出從
2
開始的所有自然數,構成一個序列:
2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, … - 取序列的第一個數
2
,它一定是素數,然後用2
把序列的2
的倍數篩掉:
3, 5, 7, 9, 11, 13, 15, 17, 19, … - 取新序列的第一個數
3
,它一定是素數,然後用3
把序列的3
的倍數篩掉:
5, 7, 11, 13, 17, 19, … - 取新序列的第一個數
5
,然後用5
把序列的5
的倍數篩掉:
7, 11, 13, 17, 19, … - 不斷篩選下去,就可以得到所有的素數,下面用Python來實現這個算法:
- 列出從
# 構造一個從3開始的奇數序列,注意這是一個生成器,並且是一個無限序列;
>>> def _odd_iter():
... n = 1
... while True:
... n = n + 2
... yield n
...
# 定義一個篩選函數;
>>> def _not_divisible(n):
... return lambda x: x % n > 0
...
# 定義一個生成器,不斷返回下一個素數;
>>> def primes():
... yield 2 # 生成器先返回第一個素數2,然後,利用filter()不斷產生篩選後的新的序列;
... it = _odd_iter()
... while True:
... n = next(it)
... yield n
... it = filter(_not_divisible(n), it)
...
# 由於primes()也是一個無限序列,所以調用時需要設置一個退出循環的條件;
>>> for n in primes():
... if n < 1000:
... print(n)
... else:
... break
...
1.4.2 練習
- 回數是指從左向右讀和從右向左讀都是一樣的數,例如
12321
,909
。請利用filter()
篩選出回數:
>>> def is_palindrome(n):
... return str(n) == str(n)[::-1]
...
>>> output = filter(is_palindrome, range(1, 1000))
>>> print('1-1000:', list(output))
1-1000: [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 22, 33, 44, 55, 66, 77, 88, 99, 101, 111, 121, 131, 141, 151, 161, 171, 181, 191, 202, 212, 222, 232, 242, 252, 262, 272, 282, 292, 303, 313, 323, 333, 343, 353, 363, 373, 383, 393, 404, 414, 424, 434, 444, 454, 464, 474, 484, 494, 505, 515, 525, 535, 545, 555, 565, 575, 585, 595, 606, 616, 626, 636, 646, 656, 666, 676, 686, 696, 707, 717, 727, 737, 747, 757, 767, 777, 787, 797, 808, 818, 828, 838, 848, 858, 868, 878, 888, 898, 909, 919, 929, 939, 949, 959, 969, 979, 989, 999]
1.5 sorted
- 無論是冒泡排序還是快速排序,排序的核心是比較兩個元素的大小,如果是數字,可以直接比較大小,但如果是字符串或者兩個dict呢,python內置的
sorted()
函數就可以對list就行排序:
# 對list進行排序:
>>> sorted([36, 5, -12, -24])
[-24, -12, 5, 36]
# 同時也是一個高階函數,可以接受一個key函數來實現自定義的排序,例如按絕對值大小進行排序:
>>> sorted([36, 5, -12, -24], key = abs)
[-24, -12, 5, 36]
- 字符串排序的例子:
# 默認情況下,是根據ASCII的大小進行排序,由於'Z'<'a',所以:
>>> sorted(['bob', 'about', 'Zoo', 'Credit'])
['Credit', 'Zoo', 'about', 'bob']
# 通過將所有字符串都變成大寫或者小寫,來實現忽略大小寫的排序:
>>> sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower)
['about', 'bob', 'Credit', 'Zoo']
# 進行反向排序,傳入reverse=True即可:
>>> sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower, reverse=True)
['Zoo', 'Credit', 'bob', 'about']
練習1
- 假設我們用一組tuple表示學生名字和成績:
L = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)]
請用sorted()
對上述列表分別按名字排序:
>>> L = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)
>>> def by_name(t):
... return t[0]
...
>>> L2 = sorted(L, key=by_name)
>>> print(L2)
[('Adam', 92), ('Bart', 66), ('Bob', 75), ('Lisa', 88)]
練習2
- 再按成績從高到低排序:
>>> L = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)]
>>> def by_score(t):
... return -t[1]
...
>>> L2 = sorted(L, key=by_score)
>>> print(L2)
[('Adam', 92), ('Lisa', 88), ('Bob', 75), ('Bart', 66)]
2. 返回函數
2.1 函數作爲返回值
- 高階函數除了可以接受函數作爲參數外,還可以將函數作爲結果值返回;
- 例如實現一個可變參數的求和:
# 通常情況下,求和函數這樣定義:
>>> def calc_sum(*args):
... ax = 0
... for n in args:
... ax = ax + n
... return ax
# 但如果不立刻求和,而在後面的代碼中,根據需要計算,這樣就需要返回求和的函數:
>>> def lazy_sum(*args):
... def sum():
... ax = 0
... for n in args:
... ax = ax + n
... return ax
... return sum
...
>>> f = lazy_sum(1, 2, 3) # 當調用lazy_sum時,返回的並不是求和結果,而是求和函數;
>>> f
<function lazy_sum.<locals>.sum at 0x0000022D10582598>
>>> f() # 調用函數f時,才真正計算求和的結果;
- 此例中,在函數
lazy_sum
中又定義了函數sum
,並且,內部函數sum
可以引用外部函數lazy_sum
的參數和局部變量,當lazy_sum
返回函數sum
時,相關參數和變量都保存在返回的函數中,這種稱爲“閉包(Closure)”的程序結構擁有極大的威力。 - 注意:當我們每次調用
lazy_sum
函數時,都會返回一個新的函數,即使傳入相同的函數:
>>> f1 = lazy_sum(1, 3, 5, 7, 9)
>>> f2 = lazy_sum(1, 3, 5, 7, 9)
>>> f1 = f2 # f1()和f2()的調用結果互不影響;
False
2.2 閉包
- 注意到返回的函數在其定義內部引用了局部變量
args
,當一個函數返回了另外一個函數後,其內部的局部變量還被新函數引用; - 另外注意返回的函數並沒有立刻執行,而是直到調用了
f()
後才執行,例如:
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()
的結果是1
,4
,9
,但實際結果是:
>>> f1()
9
>>> f2()
9
>>> f3()
9
- 全部都是
9
!原因就在於返回的函數引用了變量i
,但它並非立刻執行。等到3個函數都返回時,它們所引用的變量i
已經變成了3
,因此最終結果爲9
。 - 返回閉包時牢記:返回函數不要引用任何循環變量,或者後續會變化的變量
- 如何引用循環變量呢,方法就是再創建一個函數,用該函數的參數綁定當前循環變量的值,無論該循環變量後期如何變化,已綁定到函數參數的值不變:
def count():
def f(j):
def g():
return j * j
return g
fs = []
for i in range(1, 4):
fs.append(f(i)) # f(i)立刻被執行,因此i的當前值被傳入f()
return fs
>>> f1, f2, f3 = count()
>>> f1()
1
>>> f2()
4
>>> f3()
9
- 缺點是代碼較長,可利用
lambda
函數縮短代碼。
2.3 練習
- 利用閉包返回一個計數器函數,每次調用它返回遞增整數:
def createCounter():
i = [0]
def counter():
i[0] = i[0] + 1
return i[0]
return counter
counterA = createCounter()
print(counterA(), counterA(), counterA(), counterA(), counterA()) # 1 2 3 4 5
counterB = createCounter()
if [counterB(), counterB(), counterB(), counterB()] == [1, 2, 3, 4]:
print('測試通過!')
else:
print('測試失敗!')
Run
3. 匿名函數
- 當傳入函數時,有時候並不需要顯式的定義函數,直接傳入匿名函數更加方便;
# 例如在map()函數中,在計算平方時,除了定義一個f(x)函數外,還可以直接傳入匿名函數
>>> list(map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7]))
[1, 4, 9, 16, 25, 36, 49]
- 可以看出匿名函數
lambda x: x * x
實際上就是:
def f(x):
return x * x
- 關鍵字
lambda
表示匿名函數,冒號前面的x
表示函數參數; - 匿名函數有個限制,就是只能有一個表達式,不用寫
return
,返回值就是該表達式的結果; - 同時,匿名函數也是一個函數對象,也可以把匿名函數賦值給一個變量,再利用變量來調用該函數;
>>> f = lambda x: x * x
>>> f
<function <lambda> at 0x101c6ef28>
>>> f(5)
25
- 同時也可將匿名函數作爲返回值返回;
def build(x, y):
return lambda: x * x + y * y
練習
- 請使用匿名函數改造下面的代碼:
def is_odd(n):
return n % 2 == 1
L = list(filter(is_odd, range(1, 20)))
print(L)
[1, 3, 5, 7, 9, 11, 13, 15, 17, 19]
>>> L = list(filter(lambda n: n % 2 ==1, range(1, 20)))
>>> print(L)
[1, 3, 5, 7, 9, 11, 13, 15, 17, 19]
4. 裝飾器
- 函數對象可以被賦值給變量,所以,通過變量也可以調用該函數:
>>> def now():
... print('2018-11-9')
...
>>> f = now
>>> f()
2018-11-9
函數對象有一個__name__
屬性,可以拿到函數的名字:
>>> now.__name__
'now'
>>> f.__name__
'now'
- 加入我們現在想增強
now()
函數的功能,例如,在函數調用前後自動打印日誌,但又不希望修改now()
函數的定義,這種在代碼運行期間動態增加功能的定義的方式,稱之爲‘裝飾器(Decorator)’。
# 實質上,decorator就是一個返回函數的高階函數,故定義一個要打印日誌的decorator,如下:
def log(func):
def wrapper(*args, **kw):
print('call %s():' % func.__name__)
return func(*args, **kw)
return wrapper
# log()函數就是一個decorator,所以接受一個函數作爲參數,並返回一個函數,藉助python中@的語法,將decorator置於函數的定義處:
>>> @log
... def now():
... print('2018')
# 調用now()函數,不僅會運行函數本身,還會在運行now()函數前打印一行日誌:
>>> now()
call now():
2018
# 把@log放到now()函數的定義處,相當於執行了語句:
now = log(now)
-
由於
log()
是一個decorator,返回一個函數,故,原來的now()
函數任然存在,只是現在同名的now
變量指向了新的函數,即在log()
函數中返回的wrapper()
函數; -
wrapper()
函數的參數定義是(*args, **kw)
,(*args 是非關鍵字參數,用於元組,**kw 是關鍵字參數,用於字典)
,因此,wrapper()
函數可以接受任意參數的調用,在wrapper()
函數內,首先打印日誌,再緊接着調用原始函數; -
如果decorator本身需要傳入參數,那就需要寫一個返回decorator的高階函數,寫出來更加複雜。比如,要自定義log的文本:
def log(text):
def decorator(func):
def wrapper(*args, **kw):
print('%s %s():' % (text, func.__name__))
return func(*args, **kw)
return wrapper
return decorator
# 這三層嵌套的decorator用法如下:
@log('execute')
def now():
print('2018')
# 執行結果:
>>> now()
execute now():
2018
- 和兩層嵌套的
decorator
相比,3層嵌套的結果是這樣的:
>>> now = log('execute')(now)
- 上面的語句,首先執行
log('execute')
,返回的是decorator
函數,再調用返回的函數,參數是now()
函數,返回值最終是wrapper
函數; - 前面也講了函數也是對象,他也有
__name__
屬性,但看經decorator
裝飾過的函數:
>>> now.__name__ # 已經由原來的'now'變爲現在的'wrapper';
'wrappper'
- 因爲最後返回到
wrapper
函數名字就是wrapper
,所以需要把原先的__name__
等屬性複製到wrapper
函數中,否則有些依賴函數簽名的代碼執行就容易出錯: - python內置的
functools.wraps
可以完成這個功能,所以一個完整的decorator
函數寫法如下:
import functools
def log(text):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print('%s %s():' % (text, func.__name__))
return func(*args, **kw)
return wrapper
return decorator
練習
- 設計一個decorator,他可作用於任何函數上,並打印該函數的執行時間:
import time, functools
def metric(fn):
@functools.wraps(fn)
def wrapper(*args, **kw):
start = time.time()
fn(*args, **kw)
end = time.time()
print('%s execute in %s ms' %(fn.__name__, end - start))
return fn(%args, **kw)
return wrapper
5. 偏函數
- 這裏的偏函數(partial function)和數學意義上的偏函數不一樣
- 在介紹函數參數的時候,我們講到,通過設定函數參數的默認值,可以降低函數調用的難度,而偏函數也可以做到這一點;
# int()函數可以把字符串轉換爲整數,當僅傳入字符串時,int()函數可以按照默認值進行十進制轉換;
>>> int('123456')
123456
# 但int()函數同時還提供額外的base參數,默認值爲10,傳入base參數,可以做N進制的轉換;
>>> int('123456', base = 8)
42798
>>> int('123456', 8)
42798
# 如果要轉換大量的二進制字符串,每次都傳入int(x, base = 2)很麻煩,於是,我們可以定義一個int2()的函數,默認把base=2傳進去;
def int2(x, base = 2):
return int(x, base)
# 這樣轉換二進制就非常方便了
>>> int2('1000000')
64
>>> int2('1010101')
85
functools.partial
就是幫助我們創建一個偏函數的,不需要我們自定義int2()
,可以直接用一行代碼創建一個新的函數int2()
;
>>> import functools
>>> int2 = functools.partial(int, base=2)
>>> int2('100000')
32
functools.partial
函數的功能就是把一個函數的某些參數給固定住(就是設置默認值),返回一個新的函數,調用這個新的函數會非常方便;- 注意到上面的
int2
函數,僅僅把base
參數默認值設定爲2
,但是在調用時依然可以傳入其他參數值:
>>> int2('100000', base=10)
100000
- 最後在創建偏函數時,實際上可以接收函數對象,
*args
和**kw
這3個參數,當傳入:
int2 = functools.partials(int, base=2)
- 實際上固定了int()函數的關鍵字參數
base
,也就是:
int2('10010')
# 相當於
kw = {'base': 2}
int('10010', **kw)
- 當傳入:
max2 = functools.partial(max, 10)
# 實際上會把10作爲*args的一部分自動加到左邊,也就是
max2(5, 6, 7)
# 相當於:
args = (10, 5, 6, 7)
max(*args)
- 結果爲
10