30分鐘掌握python的函數?
函數式模
?
在命令式模型中,執行程序的方式是給計算機一系列指令讓它執行。執行過程中計算機會改變狀態。例如,比如 A 的初始值是 5,後來改變了 A 的值。那麽 A 就是個變量,而變量的意思就是包含的值會改變。
而在函數式模式中,你不需要告訴計算機做什麽,而是告訴計算機是什麽。比如數字的最大公約數是什麽,1 到 n 的乘積是什麽等等。歡迎加入python學習交流q群250933691,分享我精心準備的Python學習資料,0基礎到進階!希望你們在學習Python道路上少走彎路!加油!
因此,變量是不能被改變的。變量一旦被設置,就永遠保持同一個值(註意在純粹的函數式語言中,它們不叫變量)。因此,在函數式模型中,函數沒有副作用。副作用就是函數對函數外的世界做出的改變。來看看下面這段Python代碼的例子:
a?=?3
def?some_func():
????global?a
????a?=?5
some_func()
print(a)
代碼的輸出是 5。在函數式模型中,改變變量的值是完全不允許的,讓函數影響函數外的世界也是不允許的。函數唯一能做的就是做一些計算然後返回一個值。
你可能會想:“沒有變量也沒有副作用?這有什麽好的?”好問題。
如果函數使用同樣的參數調用兩次,那麽我們可以保證它會返回同樣的結果。如果你學過數學函數,你肯定知道這樣做的好。這叫做引用透明性(referential transparency)。由於函數沒有副作用,那麽我們可以加速計算某個東西的程序。比如,如果程序知道 func(2)返回 3,那麽可以將這個值保存在表中,這樣就不需要重復運行我們早已知道結果的函數了。
通常,函數式編程不使用循環,而是使用遞歸。遞歸是個數學概念,通常的意思是“把結果作為自己的輸入”。使用遞歸函數,函數可以反復調用自己。下面就是個使用Python定義的遞歸函數的例子:
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)
函數式編程語言也是懶惰的。懶惰的意思是,除非到最後一刻,否則它們不會執行計算或做任何操作。如果代碼要求計算2+2,那麽函數式程序只有在真正用到計算結果的時候才會去計算。我們馬上就會介紹Python中的這種懶惰。
映射
?
要理解映射(map),首先需要理解什麽是可叠代對象。可叠代對象(iterable)指任何可以叠代的東西。通常是列表或數組,Python 還有許多其他可叠代對象。甚至可以自定義對象,通過實現特定的魔術方法使其變成可叠代對象。魔術方法就像 API 一樣,能讓對象更有 Python 風格。要讓對象變成可叠代對象,需要實現以下兩個魔術方法:
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
第一個魔術方法“iter”(雙下劃線iter)返回叠代子,通常在循環開始時調用。next則返回叠代的下一個對象。
可以打開命令行試一下下面的代碼:
for?c?in?Counter(3,?8):
????print(c)
這段代碼將會輸出:
3
4
5
6
7
8
在 Python 中,叠代器就是只實現了iter魔術方法的對象。也就是說,你可以訪問對象中都包含的位置,但無法遍歷整個對象。一些對象實現了next魔術方法,但沒有實現iter魔術方法,比如集合(本文稍後會討論)。在本文中,我們假設涉及到的一切對象都是可叠代的對象。
現在我們知道了什麽是可叠代的對象,回過頭來討論下映射函數。映射可以對可叠代對象中的每個元素執行指定的函數。通常,我們對列表中的每個元素執行函數,但要知道映射其實可以針對絕大多數可叠代對象使用。
map(function,?iterable)
假設有一個列表由以下數字組成:
[1,?2,?3,?4,?5]
我們希望得到每個數字的平方,那麽代碼可以寫成這樣:
x?=?[1,?2,?3,?4,?5]
def?square(num):
????return?num*num
print(list(map(square,?x)))
Python中的函數式函數是懶惰的。如果我們不加“list()”,那麽函數只會將可叠代對象保存下來,而不會保存結果的列表。我們需要明確地告訴Python“把它轉換成列表”才能得到結果。
在Python中一下子從不懶惰的函數求值轉換到懶惰的函數似乎有點不適應。但如果你能用函數式的思維而不是過程式的思維,那麽最終會適應的。
這個“square(num)”的確不錯,但總覺得有點不對勁。難道為了僅使用一次的map就得定義整個函數嗎?其實我們可以使用lambda函數(匿名函數)。
Lambda 表達式
?
Lambda表達式就是只有一行的函數。比如下面這個lambda表達式可以求出給定數字的平方:
square?=?lambda?x:?x?*?x
運行下面的代碼:
>>?square(3)
9
你肯定在問:“參數去哪兒了?這究竟是啥意思?看起來根本不像函數啊?”
嗯,的確是不太容易懂……但還是應該能夠理解的。我們上面的代碼把什麽東西賦給了變量“square”。就是這個東西:
lambda?x:
它告訴Python這是個lambda函數,輸入的名字為x。冒號後面的一切都是對輸入的操作,然後它會自動返回操作的結果。
這樣我們的求平方的代碼可以簡化成一行:
x?=?[1,?2,?3,?4,?5]
print(list(map(lambda?num:?num?*?num,?x)))
有了lambda表達式,所有參數都放在左邊,操作都放在右邊。雖然看上去有點亂,但不能否認它的作用。實際上能寫出只有懂得函數式編程的人才能看懂的代碼還是有點小興奮的。而且把函數變成一行也非常酷。
歸納
?
歸納(reduce)是個函數,它把一個可叠代對象變成一個東西。通常,我們在列表上進行計算,將列表歸納成一個數字。歸納的代碼看起來長這樣:
reduce(function,?list)
上面的函數可以使用lambda表達式。
列表的乘積就是把所有數字乘到一起。可以這樣寫代碼:
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])
這樣能得到同樣的結果。這段代碼更短,而且借助函數式編程,這段代碼更簡潔。
過濾
?
過濾(filter)函數接收一個可叠代對象,然後過濾掉對象中一切不需要的東西。
通常過濾接收一個函數和一個列表。它會針對列表中的每個元素執行函數,如果函數返回True,則什麽都不做。如果函數返回False,則從列表中去掉那個元素。
語法如下:
filter(function,?list)
我們來看一個簡單的例子。沒有過濾,代碼要寫成這樣:
x?=?range(-5,?5)
new_list?=?[]
for?num?in?x:
????if?num?
????????new_list.append(num)
使用過濾可以寫成這樣:
x?=?range(-5,?5)
all_less_than_zero?=?list(filter(lambda?num:?num?
?
?
?
高階函數
?
?
高階函數接收函數作為參數,返回另一個函數。一個非常簡單的例子如下所示:
def?summation(nums):
????return?sum(nums)
def?action(func,?numbers):
????return?func(numbers)
print(action(summation,?[1,?2,?3]))
#?Output?is?6
或者更簡單“返回函數”的例子:
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()
還記得之前說過函數式編程語言沒有變量嗎?實際上高階函數能很容易做到這一點。如果你只需要在一系列函數中傳遞數據,那麽數據根本不需要保存到變量中。
Python 中的所有函數都是頂級對象。頂級對象是擁有一個或多個以下特征的對象:
在運行時生成
賦值給某個數據結構中的變量或元素
作為參數傳遞給函數
作為函數的結果返回
所以,所有 Python 中的函數都是對象,都可以用作高階函數。
?
?
?
?
部分函數
?
?
部分函數有點難懂,但非常酷。通過它,你不需要提供完整的參數就能調用函數。我們來看個例子。我們要創建一個函數,它接收兩個參數,一個是底,另一個是指數,然後返回底的指數次冪,代碼如下:
def?power(base,?exponent):
??return?base?**?exponent
現在我們需要一個求平方的函數,可以這麽寫:
def?square(base):
??return?power(base,?2)
這段代碼沒問題,但如果需要立方函數怎麽辦?或者四次方函數呢?是不是得一直定義新的函數?這樣做也行,但是程序員總是很懶的。如果需要經常重復一件事情,那就意味著一定有辦法提高速度,避免重復。我們可以用部分函數實現這一點。下面是使用部分函數求平方的例子:
from?functools?import?partial
square?=?partial(power,?exponent=2)
print(square(2))
#?output?is?4
這是不是很苦?我們事先告訴 Python 第二個參數,這樣只需要提供一個參數就能調用需要兩個參數的函數了。
還可以使用循環來生成直到能計算 1000 次方的所有函數。
from?functools?import?partial
powers?=?[]
for?x?in?range(2,?1001):
??powers.append(partial(power,?exponent?=?x))
print(powers0)
#?output?is?9
?
?
?
?
函數式編程不夠 Python
?
?
你也許註意到了,我們這裏許多函數式編程都用到了列表。除了歸納和部分函數之外,所有其他函數都生成列表。Guido(Python發明人)不喜歡在 Python 中使用函數式的東西,因為 Python 有自己的方法來生成列表。
在 Python IDLE 中敲“import this”,可以看到下面的內容:
>>?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風格。我們要指出的是這句話:
There should be one?—?and preferably only one?—?obvious way to do it.
(任何事情應該有一個且只有一個方法解決。)
在 Python 中,映射和過濾能做到的事情,列表解析式(稍後介紹)也能做到。這就打破了 Python 之禪,因此我們說函數式編程的部分不夠“Python”。
另一個常被提及的地方就是lambda。在Python中,lambda函數就是個普通的函數。lambda只是個語法糖。這兩者是等價的:
foo?=?lambda?a:?2
def?foo(a):
??return?2
普通的函數能做到一切 lambda 能做到的事情,但反過來卻不行。lambda 不能完成普通函數能完成的一切事情。
關於為何函數式編程不適合Python生態系統曾有過一次討論。你也許註意到,我之前提到了列表解析式,我們現在就來介紹下什麽是列表解析式。
?
列表解析式
之前我說過,任何能用映射或過濾完成的事情都可以用列表解析式完成。這就是我們要學的東西。
列表解析式是 Python 生成列表的方式。語法如下:
[function?for?item?in?iterable]
要想求列表中每個數字的平方,可以這麽寫:
print([x?*?x?for?x?in?[1,?2,?3,?4]])
可以看到,我們給列表中的每個元素應用了一個函數。那麽怎樣才能實現過濾呢?先來看看之前的這段代碼:
x?=?range(-5,?5)
all_less_than_zero?=?list(filter(lambda?num:?num?
print(all_less_than_zero)
可以將它轉換成下面這種使用列表解析式的方式:
x?=?range(-5,?5)
all_less_than_zero?=?[num?for?num?in?x?if?num?
像這樣,列表解析式支持 if 語句。這樣就不需要寫一堆函數來實現了。實際上,如果你需要生成某種列表,那麽很有可能使用列表解析式更方便、更簡潔。
如果想求所有小於 0 的數字的平方呢?使用 Lambda、映射和過濾可以寫成:
x?=?range(-5,?5)
all_less_than_zero?=?list(map(lambda?num:?num?*?num,?list(filter(lambda?num:?num?
看上去似乎很長,而且有點復雜。用列表解析式只需寫成:
x?=?range(-5,?5)
all_less_than_zero?=?[num?*?num?for?num?in?x?if?num?
不過列表解析式只能用於列表。映射和過濾能用於一切可叠代對象。那為什麽還要用列表解析式呢?其實,解析式可以用在任何可叠代的對象上。
其他解析式
可以在任何可叠代對象上使用解析式。
?
任何可叠代對象都可以用解析式生成。從 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?
{1:?‘UNITED?STATES‘,?7:?‘RUSSIA‘,?62:?‘INDONESIA‘,?55:?‘BRAZIL‘}
只要是可叠代對象,就可以用解析式生成。我們來看個集合的例子。如果你不知道集合是什麽,可以先讀讀這篇
集合是元素的列表,但列表中沒有重復的元素
元素的順序不重要
#?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非常聰明。它會查看你是否在大括號中提供了額外的值,來判斷是集合解析式還是字典解析式。如果想了解更多關於解析式的內容,可以看看這個可視化的指南
結論
?
函數式編程很美、很純凈。函數式代碼可以寫得非常幹凈,但也可以寫得很亂。一些 Python 程序員不喜歡在 Python 中使用函數式的模型,不過大家可以根據自己的喜好,記得用最好的工具完成工作。
歡迎加入python學習交流q群250933691,分享我精心準備的Python學習資料,0基礎到進階!希望你們在學習Python道路上少走彎路!加油!
30分鐘掌握python的函數?