【Python】十分鐘學(xué)會函數(shù)式編程
點擊上方“小白學(xué)視覺”,選擇加"星標"或“置頂”
重磅干貨,第一時間送達
本文轉(zhuǎn)自:深度學(xué)習(xí)這點小事

函數(shù)式編程到底是什么?本文將詳解其概念,同時分享怎樣在 Python 中使用函數(shù)式編程。主要內(nèi)容包括列表解析式和其他形式的解析式。
在命令式模型中,執(zhí)行程序的方式是給計算機一系列指令讓它執(zhí)行。執(zhí)行過程中計算機會改變狀態(tài)。例如,比如 A 的初始值是 5,后來改變了 A 的值。那么 A 就是個變量,而變量的意思就是包含的值會改變。
而在函數(shù)式模式中,你不需要告訴計算機做什么,而是告訴計算機是什么。比如數(shù)字的最大公約數(shù)是什么,1 到 n 的乘積是什么等等。
因此,變量是不能被改變的。變量一旦被設(shè)置,就永遠保持同一個值(注意在純粹的函數(shù)式語言中,它們不叫變量)。因此,在函數(shù)式模型中,函數(shù)沒有副作用。副作用就是函數(shù)對函數(shù)外的世界做出的改變。來看看下面這段Python代碼的例子:
a = 3
def some_func():
global a
a = 5
some_func()
print(a)
代碼的輸出是 5。在函數(shù)式模型中,改變變量的值是完全不允許的,讓函數(shù)影響函數(shù)外的世界也是不允許的。函數(shù)唯一能做的就是做一些計算然后返回一個值。
你可能會想:“沒有變量也沒有副作用?這有什么好的?”好問題。
如果函數(shù)使用同樣的參數(shù)調(diào)用兩次,那么我們可以保證它會返回同樣的結(jié)果。如果你學(xué)過數(shù)學(xué)函數(shù),你肯定知道這樣做的好。這叫做引用透明性(referential transparency)。由于函數(shù)沒有副作用,那么我們可以加速計算某個東西的程序。比如,如果程序知道 func(2)返回 3,那么可以將這個值保存在表中,這樣就不需要重復(fù)運行我們早已知道結(jié)果的函數(shù)了。
通常,函數(shù)式編程不使用循環(huán),而是使用遞歸。遞歸是個數(shù)學(xué)概念,通常的意思是“把結(jié)果作為自己的輸入”。使用遞歸函數(shù),函數(shù)可以反復(fù)調(diào)用自己。下面就是個使用Python定義的遞歸函數(shù)的例子:
def factorial_recursive(n):
# Base case: 1! = 1
if n == 1:
return 1
# Recursive case: n! = n * (n-1)!
else:
return n * factorial_recursive(n-1)
函數(shù)式編程語言也是懶惰的。懶惰的意思是,除非到最后一刻,否則它們不會執(zhí)行計算或做任何操作。如果代碼要求計算2+2,那么函數(shù)式程序只有在真正用到計算結(jié)果的時候才會去計算。我們馬上就會介紹Python中的這種懶惰。
要理解映射(map),首先需要理解什么是可迭代對象??傻鷮ο螅╥terable)指任何可以迭代的東西。通常是列表或數(shù)組,但 Python 還有許多其他可迭代對象。甚至可以自定義對象,通過實現(xiàn)特定的魔術(shù)方法使其變成可迭代對象。魔術(shù)方法就像 API 一樣,能讓對象更有 Python 風(fēng)格。要讓對象變成可迭代對象,需要實現(xiàn)以下兩個魔術(shù)方法:
class Counter:
def __init__(self, low, high):
# set class attributes inside the magic method __init__
# for "inistalise"
self.current = low
self.high = high
def __iter__(self):
# first magic method to make this object iterable
return self
def __next__(self):
# second magic method
if self.current > self.high:
raise StopIteration
else:
self.current += 1
return self.current - 1
第一個魔術(shù)方法“__iter__”(雙下劃線iter)返回迭代子,通常在循環(huán)開始時調(diào)用。__next__則返回迭代的下一個對象。
可以打開命令行試一下下面的代碼:
for c in Counter(3, 8):
print(c)
這段代碼將會輸出:
3
4
5
6
7
8
在 Python 中,迭代器就是只實現(xiàn)了__iter__魔術(shù)方法的對象。也就是說,你可以訪問對象中都包含的位置,但無法遍歷整個對象。一些對象實現(xiàn)了__next__魔術(shù)方法,但沒有實現(xiàn)__iter__魔術(shù)方法,比如集合(本文稍后會討論)。在本文中,我們假設(shè)涉及到的一切對象都是可迭代的對象。
現(xiàn)在我們知道了什么是可迭代的對象,回過頭來討論下映射函數(shù)。映射可以對可迭代對象中的每個元素執(zhí)行指定的函數(shù)。通常,我們對列表中的每個元素執(zhí)行函數(shù),但要知道映射其實可以針對絕大多數(shù)可迭代對象使用。
map(function, iterable)
假設(shè)有一個列表由以下數(shù)字組成:
[1, 2, 3, 4, 5]
我們希望得到每個數(shù)字的平方,那么代碼可以寫成這樣:
x = [1, 2, 3, 4, 5]
def square(num):
return num*num
print(list(map(square, x)))
Python中的函數(shù)式函數(shù)是懶惰的。如果我們不加“l(fā)ist()”,那么函數(shù)只會將可迭代對象保存下來,而不會保存結(jié)果的列表。我們需要明確地告訴Python“把它轉(zhuǎn)換成列表”才能得到結(jié)果。
在Python中一下子從不懶惰的函數(shù)求值轉(zhuǎn)換到懶惰的函數(shù)似乎有點不適應(yīng)。但如果你能用函數(shù)式的思維而不是過程式的思維,那么最終會適應(yīng)的。
這個“square(num)”的確不錯,但總覺得有點不對勁。難道為了僅使用一次的map就得定義整個函數(shù)嗎?其實我們可以使用lambda函數(shù)(匿名函數(shù))。
Lambda表達式就是只有一行的函數(shù)。比如下面這個lambda表達式可以求出給定數(shù)字的平方:
square = lambda x: x * x
運行下面的代碼:
>>> square(3)
9
你肯定在問:“參數(shù)去哪兒了?這究竟是啥意思?看起來根本不像函數(shù)???”
嗯,的確是不太容易懂……但還是應(yīng)該能夠理解的。我們上面的代碼把什么東西賦給了變量“square”。就是這個東西:
lambda x:
它告訴Python這是個lambda函數(shù),輸入的名字為x。冒號后面的一切都是對輸入的操作,然后它會自動返回操作的結(jié)果。
這樣我們的求平方的代碼可以簡化成一行:
x = [1, 2, 3, 4, 5]
print(list(map(lambda num: num * num, x)))
有了lambda表達式,所有參數(shù)都放在左邊,操作都放在右邊。雖然看上去有點亂,但不能否認它的作用。實際上能寫出只有懂得函數(shù)式編程的人才能看懂的代碼還是有點小興奮的。而且把函數(shù)變成一行也非???。
歸納(reduce)是個函數(shù),它把一個可迭代對象變成一個東西。通常,我們在列表上進行計算,將列表歸納成一個數(shù)字。歸納的代碼看起來長這樣:
reduce(function, list)
上面的函數(shù)可以使用lambda表達式。
列表的乘積就是把所有數(shù)字乘到一起??梢赃@樣寫代碼:
product = 1
x = [1, 2, 3, 4]
for num in x:
product = product * num
但使用歸納,可以寫成這樣:
from functools import reduce
product = reduce((lambda x, y: x * y),[1, 2, 3, 4])
這樣能得到同樣的結(jié)果。這段代碼更短,而且借助函數(shù)式編程,這段代碼更簡潔。
過濾(filter)函數(shù)接收一個可迭代對象,然后過濾掉對象中一切不需要的東西。
通常過濾接收一個函數(shù)和一個列表。它會針對列表中的每個元素執(zhí)行函數(shù),如果函數(shù)返回True,則什么都不做。如果函數(shù)返回False,則從列表中去掉那個元素。
語法如下:
filter(function, list)
我們來看一個簡單的例子。沒有過濾,代碼要寫成這樣:
x = range(-5, 5)
new_list = []
for num in x:
if num < 0:
new_list.append(num)
使用過濾可以寫成這樣:
x = range(-5, 5)
all_less_than_zero = list(filter(lambda num: num < 0, x))
高階函數(shù)接收函數(shù)作為參數(shù),返回另一個函數(shù)。一個非常簡單的例子如下所示:
def summation(nums):
return sum(nums)
def action(func, numbers):
return func(numbers)
print(action(summation, [1, 2, 3]))
# Output is 6
或者更簡單“返回函數(shù)”的例子:
def rtnBrandon():
return "brandon"
def rtnJohn():
return "john"
def rtnPerson():
age = int(input("What's your age?"))
if age == 21:
return rtnBrandon()
else:
return rtnJohn()
還記得之前說過函數(shù)式編程語言沒有變量嗎?實際上高階函數(shù)能很容易做到這一點。如果你只需要在一系列函數(shù)中傳遞數(shù)據(jù),那么數(shù)據(jù)根本不需要保存到變量中。
Python 中的所有函數(shù)都是頂級對象。頂級對象是擁有一個或多個以下特征的對象:
在運行時生成
賦值給某個數(shù)據(jù)結(jié)構(gòu)中的變量或元素
作為參數(shù)傳遞給函數(shù)
作為函數(shù)的結(jié)果返回
所以,所有 Python 中的函數(shù)都是對象,都可以用作高階函數(shù)。
部分函數(shù)有點難懂,但非??帷Mㄟ^它,你不需要提供完整的參數(shù)就能調(diào)用函數(shù)。我們來看個例子。我們要創(chuàng)建一個函數(shù),它接收兩個參數(shù),一個是底,另一個是指數(shù),然后返回底的指數(shù)次冪,代碼如下:
def power(base, exponent):
return base ** exponent
現(xiàn)在我們需要一個求平方的函數(shù),可以這么寫:
def square(base):
return power(base, 2)
這段代碼沒問題,但如果需要立方函數(shù)怎么辦?或者四次方函數(shù)呢?是不是得一直定義新的函數(shù)?這樣做也行,但是程序員總是很懶的。如果需要經(jīng)常重復(fù)一件事情,那就意味著一定有辦法提高速度,避免重復(fù)。我們可以用部分函數(shù)實現(xiàn)這一點。下面是使用部分函數(shù)求平方的例子:
from functools import partial
square = partial(power, exponent=2)
print(square(2))
# output is 4
這是不是很苦?我們事先告訴 Python 第二個參數(shù),這樣只需要提供一個參數(shù)就能調(diào)用需要兩個參數(shù)的函數(shù)了。
還可以使用循環(huán)來生成直到能計算 1000 次方的所有函數(shù)。
from functools import partial
powers = []
for x in range(2, 1001):
powers.append(partial(power, exponent = x))
print(powers[0](3))
# output is 9
你也許注意到了,我們這里許多函數(shù)式編程都用到了列表。除了歸納和部分函數(shù)之外,所有其他函數(shù)都生成列表。Guido(Python發(fā)明人)不喜歡在 Python 中使用函數(shù)式的東西,因為 Python 有自己的方法來生成列表。
在 Python IDLE 中敲“import this”,可以看到下面的內(nèi)容:
>>> import this
The Zen of Python, by Tim Peters
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren’t special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one — and preferably only one — obvious way to do it.
Although that way may not be obvious at first unless you’re Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it’s a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea — let’s do more of those!
這就是Python之禪。這首詩表明了什么叫做Python風(fēng)格。我們要指出的是這句話:
There should be one?—?and preferably only one?—?obvious way to do it.
(任何事情應(yīng)該有一個且只有一個方法解決。)
在 Python 中,映射和過濾能做到的事情,列表解析式(稍后介紹)也能做到。這就打破了 Python 之禪,因此我們說函數(shù)式編程的部分不夠“Python”。
另一個常被提及的地方就是lambda。在Python中,lambda函數(shù)就是個普通的函數(shù)。lambda只是個語法糖。這兩者是等價的:
foo = lambda a: 2
def foo(a):
return 2
普通的函數(shù)能做到一切 lambda 能做到的事情,但反過來卻不行。lambda 不能完成普通函數(shù)能完成的一切事情。
關(guān)于為何函數(shù)式編程不適合Python生態(tài)系統(tǒng)曾有過一次討論。你也許注意到,我之前提到了列表解析式,我們現(xiàn)在就來介紹下什么是列表解析式。
之前我說過,任何能用映射或過濾完成的事情都可以用列表解析式完成。這就是我們要學(xué)的東西。
列表解析式是 Python 生成列表的方式。語法如下:
[function for item in iterable]
要想求列表中每個數(shù)字的平方,可以這么寫:
print([x * x for x in [1, 2, 3, 4]])
可以看到,我們給列表中的每個元素應(yīng)用了一個函數(shù)。那么怎樣才能實現(xiàn)過濾呢?先來看看之前的這段代碼:
x = range(-5, 5)
all_less_than_zero = list(filter(lambda num: num < 0, x))
print(all_less_than_zero)
可以將它轉(zhuǎn)換成下面這種使用列表解析式的方式:
x = range(-5, 5)
all_less_than_zero = [num for num in x if num < 0]
像這樣,列表解析式支持 if 語句。這樣就不需要寫一堆函數(shù)來實現(xiàn)了。實際上,如果你需要生成某種列表,那么很有可能使用列表解析式更方便、更簡潔。
如果想求所有小于 0 的數(shù)字的平方呢?使用 Lambda、映射和過濾可以寫成:
x = range(-5, 5)
all_less_than_zero = list(map(lambda num: num * num, list(filter(lambda num: num < 0, x))))
看上去似乎很長,而且有點復(fù)雜。用列表解析式只需寫成:
x = range(-5, 5)
all_less_than_zero = [num * num for num in x if num < 0]
不過列表解析式只能用于列表。映射和過濾能用于一切可迭代對象。那為什么還要用列表解析式呢?其實,解析式可以用在任何可迭代的對象上。
可以在任何可迭代對象上使用解析式。
任何可迭代對象都可以用解析式生成。從 Python 2.7 開始,甚至可以用解析式生成字典(哈希表)。
# Taken from page 70 chapter 3 of Fluent Python by Luciano Ramalho
DIAL_CODES = [
(86, 'China'),
(91, 'India'),
(1, 'United States'),
(62, 'Indonesia'),
(55, 'Brazil'),
(92, 'Pakistan'),
(880, 'Bangladesh'),
(234, 'Nigeria'),
(7, 'Russia'),
(81, 'Japan'),
]
>>> country_code = {country: code for code, country in DIAL_CODES}
>>> country_code
{'Brazil': 55, 'Indonesia': 62, 'Pakistan': 92, 'Russia': 7, 'China': 86, 'United States': 1, 'Japan': 81, 'India': 91, 'Nigeria': 234, 'Bangladesh': 880}
>>> {code: country.upper() for country, code in country_code.items() if code < 66}
{1: 'UNITED STATES', 7: 'RUSSIA', 62: 'INDONESIA', 55: 'BRAZIL'}
只要是可迭代對象,就可以用解析式生成。我們來看個集合的例子。如果你不知道集合是什么,可以先讀讀這篇(https://medium.com/brandons-computer-science-notes/a-primer-on-set-theory-746cd0b13d13)文章。簡單來說就是:
集合是元素的列表,但列表中沒有重復(fù)的元素
元素的順序不重要
# taken from page 87, chapter 3 of Fluent Python by Luciano Ramalho
>>> from unicodedata import name
>>> {chr(i) for i in range(32, 256) if 'SIGN' in name(chr(i), '')}
{'×', '¥', '°', '£', '?', '#', '?', '%', 'μ', '>', '¤', '±', '?', '§', '<', '=', '?', '$', '÷', '¢', '+'}
可以看到,集合使用字典同樣的大括號。Python非常聰明。它會查看你是否在大括號中提供了額外的值,來判斷是集合解析式還是字典解析式。如果想了解更多關(guān)于解析式的內(nèi)容,可以看看這個可視化的指南(http://treyhunner.com/2015/12/python-list-comprehensions-now-in-color/)。如果想了解更多關(guān)于解析式和生成器的內(nèi)容,可以讀讀這篇文章(https://medium.freecodecamp.org/python-list-comprehensions-vs-generator-expressions-cef70ccb49db)。
函數(shù)式編程很美、很純凈。函數(shù)式代碼可以寫得非常干凈,但也可以寫得很亂。一些 Python 程序員不喜歡在 Python 中使用函數(shù)式的模型,不過大家可以根據(jù)自己的喜好,記得用最好的工具完成工作。
交流群
歡迎加入公眾號讀者群一起和同行交流,目前有SLAM、三維視覺、傳感器、自動駕駛、計算攝影、檢測、分割、識別、醫(yī)學(xué)影像、GAN、算法競賽等微信群(以后會逐漸細分),請掃描下面微信號加群,備注:”昵稱+學(xué)校/公司+研究方向“,例如:”張三 + 上海交大 + 視覺SLAM“。請按照格式備注,否則不予通過。添加成功后會根據(jù)研究方向邀請進入相關(guān)微信群。請勿在群內(nèi)發(fā)送廣告,否則會請出群,謝謝理解~

