函數(shù)式編程就是一種抽象程度很高的編程范式蕴茴,純粹的函數(shù)式編程語言編寫的函數(shù)沒有變量,因此蓝翰,任意一個函數(shù),只要輸入是確定的,輸出就是確定的杖玲,這種純函數(shù)我們稱之為沒有副作用顿仇。而允許使用變量的程序設(shè)計(jì)語言,由于函數(shù)內(nèi)部的變量狀態(tài)不確定,同樣的輸入臼闻,可能得到不同的輸出鸿吆,因此,這種函數(shù)是有副作用的述呐。
函數(shù)式編程的一個特點(diǎn)就是惩淳,允許把函數(shù)本身作為參數(shù)傳入另一個函數(shù),還允許返回一個函數(shù)乓搬!
Python對函數(shù)式編程提供部分支持思犁。由于Python允許使用變量,因此进肯,Python不是純函數(shù)式編程語言激蹲。
高階函數(shù)
- 變量可以指向函數(shù)
# 獲取絕對值
print(abs(-1))
# 可見,abs(-1)是函數(shù)調(diào)用江掩,而abs是函數(shù)本身学辱。
print(abs)
# 函數(shù)本身也可以賦值給變量,即:變量可以指向函數(shù)环形,變量f現(xiàn)在已經(jīng)指向了abs函數(shù)本身策泣。
# 直接調(diào)用abs()函數(shù)和調(diào)用變量f()完全相同。
f = abs
print(f(-1))
結(jié)果
1
<built-in function abs>
1
- 函數(shù)名也是變量
函數(shù)名其實(shí)就是指向函數(shù)的變量抬吟!對于abs()這個函數(shù)萨咕,完全可以把函數(shù)名abs看成變量,它指向一個可以計(jì)算絕對值的函數(shù)拗军!
abs = 10
print(abs)
print(abs(-1))
把a(bǔ)bs指向10后任洞,就無法通過abs(-10)調(diào)用該函數(shù)了!因?yàn)閍bs這個變量已經(jīng)不指向求絕對值函數(shù)而是指向一個整數(shù)10发侵!
當(dāng)然實(shí)際代碼絕對不能這么寫交掏,這里是為了說明函數(shù)名也是變量。要恢復(fù)abs函數(shù)刃鳄,請重啟Python交互環(huán)境盅弛。
注:由于abs函數(shù)實(shí)際上是定義在import builtins模塊中的,所以要讓修改abs變量的指向在其它模塊也生效叔锐,要用import builtins; builtins.abs = 10挪鹏。
- 高階函數(shù)定義
既然變量可以指向函數(shù),函數(shù)的參數(shù)能接收變量愉烙,那么一個函數(shù)就可以接收另一個函數(shù)作為參數(shù)讨盒,這種函數(shù)就稱之為高階函數(shù)。
# 以下就是高階函數(shù)
def test(x,y,f):
return f(x)*f(y)
print(test(2,-9,abs))
# 結(jié)果
x = 2
y = -9
f = abs
f(x) * f(y) ==> abs(-5) * abs(6) ==> 11
return 18
map/reduce
- map(fn,Iterable):map將傳入的函數(shù)依次作用到序列的每個元素步责,并把結(jié)果作為新的Iterator返回返顺。
def count(x):
return x*x
# map()傳入的第一個參數(shù)是f禀苦,即函數(shù)對象本身。所以它也是高階函數(shù)遂鹊,由于結(jié)果r是一個Iterator振乏,
# Iterator是惰性序列,因此通過list()函數(shù)讓它把整個序列都計(jì)算出來并返回一個list秉扑。
print(list(map(count,[1,2,3,4,5,6,7,8,9])))
[1, 4, 9, 16, 25, 36, 49, 64, 81]
- reduce(fn,Iterable):reduce把一個函數(shù)作用在一個序列[x1, x2, x3, ...]上慧邮,這個函數(shù)必須接收兩個參數(shù),reduce把結(jié)果繼續(xù)和序列的下一個元素做累積計(jì)算舟陆,其效果就是:reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4)误澳,如下例子,如何實(shí)現(xiàn)將一個str轉(zhuǎn)成int類型的函數(shù):
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]
# map將'14789'轉(zhuǎn)成了分開的int類型的集合吨娜,reduce將分開的int集合合成可14789
print(reduce(fn,map(char2num,'14789')))
整合成一個函數(shù)就是:
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))
print(str2Int('14789'))
filter
和map()類似脓匿,filter()也接收一個函數(shù)和一個序列。和map()不同的是宦赠,filter()把傳入的函數(shù)依次作用于每個元素陪毡,然后根據(jù)返回值是True還是False決定保留還是丟棄該元素。比如下面勾扭,我要刪除集合中的偶數(shù):
def dell(n):
return n % 2 == 1
# filter()函數(shù)返回的是一個Iterator毡琉,也就是一個惰性序列,所以要強(qiáng)迫filter()完成計(jì)算結(jié)果妙色,需要用list()函數(shù)獲得所有結(jié)果并返回list桅滋。
print(list(filter(dell, [1, 2, 3, 4, 5, 6, 7, 8, 9])))
以下是利用Python的filter函數(shù)實(shí)現(xiàn)篩選素?cái)?shù)的埃氏篩法:
# 構(gòu)建一個只會生成奇數(shù)的生成器,構(gòu)成的是無限序列
def _odd_iter():
n = 1
while True:
n = n + 2
yield n
# 構(gòu)建一個篩選函數(shù),用于去除序列第一個數(shù)身辨,然后把序列第一個數(shù)的倍數(shù)給去除
def _not_divisible(n):
return lambda x: x % n > 0
# 篩選素?cái)?shù)的生成器
def primes():
yield 2
it = _odd_iter() # 初始序列
while True:
n = next(it) # 返回序列的第一個數(shù)
yield n
it = filter(_not_divisible(n), it) # 篩選序列的素?cái)?shù)丐谋,然后構(gòu)造新序列
# 打印篩選出來的素?cái)?shù)
for n in primes():
if n<100:
print(n)
else:
break
sorted
說白了,就是給集合進(jìn)行排序的算法煌珊,看看以下幾個示例:
# 將集合按照絕對值排序
print(sorted([-1, 5, -3, 8, -37, -20, 10], key=abs))
# 默認(rèn)情況下号俐,字符串排序是按照首字母的ASCII的大小排序,小的在前定庵,大的在后
print(sorted(['hello', 'Case', 'about', 'Like']))
# 可以忽略首字母大小寫來排序
print(sorted(['hello', 'Case', 'about', 'Like'],key=str.lower))
# 將函數(shù)進(jìn)行反轉(zhuǎn)
print(sorted(['hello', 'Case', 'about', 'Like'], reverse=True))
返回函數(shù)
高階函數(shù)除了可以接受函數(shù)作為參數(shù)外吏饿,還可以把函數(shù)作為結(jié)果值返回,具體看示例:
# 定一個函數(shù)蔬浙,里面有一個求和函數(shù)猪落,并且將求和函數(shù)作為參數(shù)返回
def add(*agr):
def sum():
s = 0
for n in agr:
s = n + s
return s
return sum
# 調(diào)用add函數(shù),返回的是里面求和函數(shù)畴博,而不是求和結(jié)果
su = add(1,2,3,4,5,6)
# 這樣才是返回求和結(jié)果
print(su())
閉包
上面的返回函數(shù)的示例中笨忌,我們在函數(shù)add中又定義了函數(shù)sum,并且俱病,內(nèi)部函數(shù)sum可以引用外部函數(shù)add的參數(shù)和局部變量官疲,當(dāng)add返回函數(shù)sum時杂曲,相關(guān)參數(shù)和變量都保存在返回的函數(shù)中,這種就是稱為“閉包(Closure)”的程序結(jié)構(gòu)袁余。注意一點(diǎn),當(dāng)我們調(diào)用add()時咱揍,每次調(diào)用都會返回一個新的函數(shù)颖榜,即使傳入相同的參數(shù)。
def count():
fs = []
for i in range(1, 4):
def f():
return i * i
fs.append(f)
return fs
f1, f2, f3 = count()
print(f1(), f2(), f3())
9 9 9
為啥結(jié)果和預(yù)料不一樣煤裙?應(yīng)該是1掩完,4,9才對硼砰,原因就在于返回的函數(shù)引用了變量i且蓬,但它并非立刻執(zhí)行。等到3個函數(shù)都返回時题翰,它們所引用的變量i已經(jīng)變成了3恶阴,因此最終結(jié)果為9。
記妆稀:返回的函數(shù)在其定義內(nèi)部引用了局部變量args冯事,所以,當(dāng)一個函數(shù)返回了一個函數(shù)后血公,其內(nèi)部的局部變量還被新函數(shù)引用昵仅,所以,需要注意的是返回的函數(shù)并沒有立刻執(zhí)行累魔,而是直到調(diào)用了f()才執(zhí)行摔笤。
返回閉包時牢記一點(diǎn):返回函數(shù)不要引用任何循環(huán)變量,或者后續(xù)會發(fā)生變化的變量垦写。
如果一定要引用循環(huán)變量怎么辦吕世?方法是再創(chuàng)建一個函數(shù),用該函數(shù)的參數(shù)綁定循環(huán)變量當(dāng)前的值梯澜,無論該循環(huán)變量后續(xù)如何更改寞冯,已綁定到函數(shù)參數(shù)的值不變:
def count():
def f(j):
def g():
return j * j
return g
fs = []
for i in range(1, 4):
fs.append(f(i))
return fs
f1, f2, f3 = count()
print(f1(), f2(), f3())
lambda(匿名函數(shù))
當(dāng)我們在傳入函數(shù)時,有些時候晚伙,不需要顯式地定義函數(shù)吮龄,直接傳入匿名函數(shù)更方便。關(guān)鍵字lambda表示匿名函數(shù)咆疗,冒號前面的x表示函數(shù)參數(shù)漓帚。匿名函數(shù)有個限制,就是只能有一個表達(dá)式午磁,不用寫return尝抖,返回值就是該表達(dá)式的結(jié)果毡们。
# 定義可一個函數(shù)f
def f(x):
return x * x
# 傳入了一個匿名函數(shù),這個匿名函數(shù)等同于上面的f函數(shù)
print(list(map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9])))
# 同樣匿名函數(shù)也是一個函數(shù)對象,可以賦值
f = lambda x: x * x
print(f(15))
# 當(dāng)然昧辽,也可以作為函數(shù)返回值
def build(x, y):
return lambda: x * y
裝飾器
這個就好比裝飾設(shè)計(jì)模式衙熔,不改變原來函數(shù),但是給函數(shù)添加新的功能搅荞,而這種在代碼運(yùn)行期間動態(tài)增加功能的方式红氯,稱之為“裝飾器”(Decorator)。
比如我們定義一個簡單的打印hello的函數(shù):
def print_hello():
print('hello')
return
然后我們需要在不改變函數(shù)的代碼情況下咕痛,加上打印本函數(shù)名稱的方法痢甘,下面演示裝飾器的用法:
# 定義一個裝飾器decorator,本質(zhì)上,decorator就是一個返回函數(shù)的高階函數(shù)茉贡。所以塞栅,我們要定義一個能打印日志的decorator,可以定義如下
def log(func):
def wrapper(*args, **kw):
# func.__name__ 中__name__ 是函數(shù)對象的一個屬性腔丧,表示函數(shù)名字
print('call %s():' % func.__name__)
return func(*args, **kw)
return wrapper
# 我們要借助Python的@語法放椰,把decorator置于函數(shù)的定義處,
# 把@log()函數(shù)print_hello()的定義處愉粤,相當(dāng)于執(zhí)行了語句 print_hello = log(print_hello)
@log
def print_hello():
print('hello')
return
print_hello()
# 結(jié)果
call print_hello():
hello
由于log()是一個decorator庄敛,返回一個函數(shù),所以科汗,原來的print_hello()函數(shù)仍然存在藻烤,只是現(xiàn)在同名的print_hello變量指向了新的函數(shù),于是調(diào)用print_hello()將執(zhí)行新函數(shù)头滔,即在log()函數(shù)中返回的wrapper()函數(shù)怖亭。wrapper()函數(shù)的參數(shù)定義是(*args, **kw),因此坤检,wrapper()函數(shù)可以接受任意參數(shù)的調(diào)用兴猩。在wrapper()函數(shù)內(nèi),首先打印日志早歇,再緊接著調(diào)用原始函數(shù)倾芝。
如果decorator本身需要傳入?yún)?shù),那就需要編寫一個返回decorator的高階函數(shù)箭跳,可以這么寫:
def log(text):
def decorator(func):
def wrapper(*args, **kw):
print('%s %s():' % (text, func.__name__))
return func(*args, **kw)
return wrapper
return decorator
@log('執(zhí)行了')
def print_hello():
print('hello')
return
# 其實(shí)就是執(zhí)行了 print_hello = log('execute')(print_hello)
print_hello()
# 結(jié)果
執(zhí)行了 print_hello():
hello
首先執(zhí)行l(wèi)og('execute')晨另,返回的是decorator函數(shù),再調(diào)用返回的函數(shù)谱姓,參數(shù)是print_hello函數(shù)借尿,返回值最終是wrapper函數(shù)。以上兩種decorator的定義都沒有問題,但還差最后一步路翻。因?yàn)槲覀冎v了函數(shù)也是對象狈癞,它有name等屬性,但你去看經(jīng)過decorator裝飾之后的函數(shù)茂契,它們的name已經(jīng)從原來的'print_hello'變成了'wrapper'蝶桶。因?yàn)榉祷氐哪莻€wrapper()函數(shù)名字就是'wrapper',所以掉冶,需要把原始函數(shù)的name等屬性復(fù)制到wrapper()函數(shù)中莫瞬,否則,有些依賴函數(shù)簽名的代碼執(zhí)行就會出錯郭蕉。
如果你要解決這個問題,不需要編寫wrapper.name = func.name這樣的代碼喂江,Python內(nèi)置的functools.wraps就是干這個事的:
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
偏函數(shù)
偏函數(shù)的作用就是和我們講到召锈,通過設(shè)定參數(shù)的默認(rèn)值,可以降低函數(shù)調(diào)用的難度這一點(diǎn)作用差不多获询。比如如下函數(shù)涨岁,第二個參數(shù)默認(rèn)是10:
int('12345', base=8)
我們可以改變默認(rèn)參數(shù)來達(dá)到效果,但是如果我們要經(jīng)常調(diào)用int('12345', base=2)吉嚣,這樣感覺很麻煩梢薪,這個時候我們可以使用偏函數(shù)達(dá)到效果:
import functools
# functools.partial的作用就是,把一個函數(shù)的某些參數(shù)給固定壮⒍摺(也就是設(shè)置默認(rèn)值)秉撇,返回一個新的函數(shù),調(diào)用這個新函數(shù)會更簡單秋泄。
int2 = functools.partial(int, base=2)
print(int2('1010101'))
# 新的int2函數(shù)琐馆,僅僅是把base參數(shù)重新設(shè)定默認(rèn)值為2,但也可以在函數(shù)調(diào)用時傳入其他值
print(int2('1000000', base=10))
實(shí)際上可以接收函數(shù)對象恒序、args和kw這3個參數(shù)瘦麸。其實(shí)上面的操作,等同于改變*kw的操作歧胁。