- Python函數式編程(第2版)
- (美)史蒂文·洛特
- 3195字
- 2020-04-22 12:28:27
1.2 細分過程范式
命令式編程可以再細分為多個子類別,本節簡單介紹過程式編程和面向對象編程的區別,并重點講解面向對象屬于命令式編程的原因。過程式編程和面向對象編程雖然有區別,但它們與函數式編程的差別更大。
下面通過代碼示例解釋這些概念。有些人覺得這么做是在重新造輪子,然而這其實是抽象概念的具體表現。
對于某些計算過程,完全可以忽略Python的面向對象特點,只使用簡單的數值計算。例如用下面的方法可以得到一組屬性相同的數。
s = 0 for n in range(1, 10): if n % 3 == 0 or n % 5 == 0: s += n print(s)
和m僅包括3或5的倍數。以上程序是嚴格過程式的,避免使用Python的任何面向對象特征。程序的狀態由變量s和n定義,變量n的取值范圍是1~10,每次循環中n的值依次增加,可以確定n == 10時循環結束。使用類似的原始數據結構,完全可以用C或者Java編寫出功能相同的程序。
下面利用Python的面向對象編程(object-oriented programming, OOP)特征編寫一段類似的代碼。
m = list() for n in range(1, 10): if n % 3 == 0 or n % 5 == 0: m.append(n) print(sum(m))
程序運行結果與前面的結果相同,但它內部維護了一個狀態可變的集合對象m,計算狀態由m和n定義。
m.append(n)和sum(m)令人費解的語法讓一些開發者誤以為Python不是純粹的面向對象語言:它混合了function()和object.method()兩種語法。然而事實上Python是純粹的面向對象語言,一些語言(例如C++)允許使用非對象的原始數據類型,例如int、float和long。Python中沒有原始數據類型,前綴的語法形式不會改變語言的本質。
嚴格地說,完全可以采用純粹的面向對象風格,基于list類生成一個包含sum方法的子類。
class Summable_List(list): def sum(self): s = 0 for v in self: s += v return s
接下來使用Summable_List()類代替list()方法初始化變量m,就可以用m.sum()方法代替sum(m)方法來對m求和了。該示例可以證明Python是純粹的面向對象語言,前綴的使用僅是語法糖而已。
前面3個例子都基于變量值顯式確定程序的狀態,使用賦值語句改變變量值,推動計算前進。我們可以在程序中插入assert語句,確保程序狀態完全按照要求變化。
關鍵之處不是命令式編程存在某種缺陷,而是函數式編程是一種思維方式的轉變,這種改變適用于許多場景。下面介紹如何用函數式方法編寫同一個算法,你會發現函數式編程并沒有使算法顯著變短或變快。
1.2.1 使用函數式范式
在函數式編程中,求3或5的倍數可分為兩部分。
? 對一系列數值求和。
? 生成一個滿足某個條件的序列,例如3或5的倍數組成的序列。
一個列表的和的遞歸形式定義如下。
def sumr(seq): if len(seq) == 0: return 0 return seq[0] + sumr(seq[1:])
可以把序列的和分為兩種情況。基礎形式:一個長度為0的序列,和為0。遞歸形式:序列的和等于序列中的第一個元素加上序列中后續元素的和。
由于遞歸形式的序列長度小于原序列,所以任何長度有限的序列最終都會退化為基礎形式。
該函數運行示例如下。
>>> sumr([7, 11]) 18 >>> 7+sumr([11]) 18 >>> 18+sumr([]) 0
第一個例子計算了包含多個值的列表之和。第二個例子演示了遞歸規則將第一個值seq[0]和后續所有值的和seq[1:]相加。最后一個計算包含了對空列表求和,其值定義為0。
這個例子中,代碼最后一行的+運算符和初始值0表明其為求和。如果將運算符從+改為*,將初始值從0改為1,則表明其為序列乘積。后面會詳細介紹這種抽象化方法。
對于一列值,可以用類似的方法遞歸,定義如下。
def until(n, filter_func, v): if v == n: return [] if filter_func(v): return [v] + until(n, filter_func, v+1) else: return until(n, filter_func, v+1)
該函數的基礎形式為:給定一個值v和一個上限n,如果v達到上限,則返回一個空列表。
根據filter_func()函數的不同返回值,遞歸形式有兩種情況。如果v通過了filter_func()函數的測試,返回一個序列,則該序列的第一個元素是v,后續元素由until()作用于后續序列的返回值組成。如果v沒有通過filter_func()函數的測試,將忽略該值,返回值由函數作用于剩余元素得到的值組成。
可以看到v在每次遞歸中遞增,直到達到上限n,也就是基礎形式。
下面介紹如何使用until()函數生成3或5的倍數。首先定義一個用于篩選數值的lambda對象。
mult_3_5 = lambda x: x % 3 == 0 or x % 5 == 0
(這里使用lambda定義簡單函數是為了保持簡潔。如果函數比較復雜,多于一行代碼,請使用def語句。)
從命令提示符界面觀察lambda的行為,如下所示。
>>> mult_3_5(3) True >>> mult_3_5(4) False >>> mult_3_5(5) True
結合until()函數,它可以生成一系列3或5的倍數。
使用until()函數生成一系列值,如下所示。
>>> until(10, lambda x: x % 3 == 0 or x % 5 == 0, 0) [0, 3, 5, 6, 9]
然后可以使用之前定義的遞歸版sum()函數計算一系列數值的和了。這里將用到的所有函數,包括sum()、until()和mult_3_5(),都定義為遞歸的,計算時不需要使用臨時變量保存計算狀態。
之后還會多次用到這種純粹遞歸風格的函數來定義思想。請注意,許多函數式語言的編譯器可以優化此類簡單的遞歸函數,但Python不會進行此類優化。
1.2.2 使用混合范式
下面介紹如何用函數式編碼實現前面計算3或5的倍數的例子。混合型函數的實現代碼如下所示。
print(sum(n for n in range(1, 10) if n % 3 == 0 or n % 5 == 0))
這里使用了嵌入式生成器表達式迭代數值集合,并計算它們的和。range(1, 10)方法是可迭代的,所以它是一種生成器表達式,返回一個數值序列{n|1≤n<10}。n for n in range(1, 10) if n % 3 == 0 or n % 5 == 0稍復雜一些,但它也是可迭代表達式,返回一個數值集合{n|1≤n<10∧(n mod 3=0∨n mod 5=0)}。變量n與集合中的每個值綁定,表示集合的內容,而非當前的計算狀態。sum()方法的輸入值是一個可迭代表達式,輸出最終計算結果:對象23。
綁定變量僅存在于生成器表達式內部,上述程序中,變量n在生成器表達式之外是不可見的。
可以將表達式中的if從句看作獨立的函數,便于用其他函數替換它??梢允褂靡粋€名為filter()的高階函數代替上面生成器表達式中的if從句,第5章會詳細介紹高階函數。
這個例子中的變量n不同于前面兩個命令式實現中的變量n,生成器表達式之外的for語句在本地命名空間中創建變量,而生成器表達式并不創建for語句式的變量。
>>> sum(n for n in range(1, 10) if n % 3 == 0 or n % 5 == 0) 23 >>> n Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'n' is not defined
生成器表達式綁定的范圍外不存在變量n,即它并不定義計算狀態。
1.2.3 對象的創建過程
在某些情況下,觀察中間對象有助于理解計算過程,但請注意,計算的路徑并不是唯一的。當函數滿足交換律和結合律的時候,改變求值順序會創建出不同的中間對象。通過這種方式,可以在保證計算正確性的同時提升計算性能。
以下面這個表達式為例:
>>> 1+2+3+4 10
下面講解不同的計算過程是如何得到相同的計算結果的。由于+運算符滿足交換律和結合律,有許多條計算路徑都能得到相同結果。
根據選擇待計算值順序的不同,有以下兩種主要的計算路徑。
>>> ((1+2)+3)+4 10 >>> 1+(2+(3+4)) 10
第一種情形是從左向右結合并求值,此時對象3和6作為求值過程的中間對象被創建出來。
第二種情形則是從右向左結合并求值,中間對象是7和9。在這個簡單的整數算術運算中,兩種方式的表現相同,優化無助于提升性能。
涉及數組的追加操作時,改變結合方式可能會提升性能。
示例如下。
>>> import timeit >>> timeit.timeit("((([]+[1])+[2])+[3])+[4]") 0.8846941249794327 >>> timeit.timeit("[]+([1]+([2]+([3]+[4])))") 1.0207440659869462
可以看到,從左向右計算性能更佳。
對于函數式編程的設計,以任意順序使用+運算符(或者add()函數),結果不變,即+運算符不影響使用方式。
1.2.4 烏龜塔
嚴格意義上,Python的函數式編程并非函數式的,Python不是Haskell、OCaml或Erlang。請注意,真正完成計算過程的處理器硬件本身就不是函數式的,甚至嚴格意義上不是面向對象的,CPU實際上是過程式的。
所有的編程語言都基于抽象、庫、框架和虛擬機,這里的抽象又基于更底層的抽象、庫、框架和虛擬機。有個很形象的比喻:整個世界被一只大烏龜馱在背上,這只大烏龜又被另外一只更大的烏龜馱在背上,這只更大的烏龜又被一只比它還大的烏龜馱在背上······
一言以蔽之:全是烏龜。
——佚名
抽象形成的層是無盡的。
更重要的是,這種抽象和虛擬機并不會影響通過Python的函數式特性設計軟件。
即使在函數式語言內部,也存在更純粹的語言和不太純粹的語言。有些語言經常使用monads處理像文件系統輸入、輸出這樣有狀態的事務,另外一些語言則使用類似于Python的混合型環境,通過仔細地隔離含有狀態的過程式動作來設計函數式的軟件。
本書的函數式Python編程基于以下3層抽象。
? 應用由函數組成,直到層層分解碰到對象。
? 支撐函數式編程的Python運行時環境是由對象組成的,直到層層分解碰到庫。
? 支撐Python運行的庫就是馱著Python的烏龜。
更底層的操作系統和硬件有它們各自的烏龜塔,而且與我們要處理的問題無關。