官术网_书友最值得收藏!

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的烏龜。

更底層的操作系統和硬件有它們各自的烏龜塔,而且與我們要處理的問題無關。

主站蜘蛛池模板: 灵寿县| 齐河县| 温泉县| 平果县| 麻阳| 永新县| 德化县| 万宁市| 惠东县| 伊吾县| 西丰县| 福建省| 永顺县| 威海市| 平顺县| 资阳市| 高淳县| 南郑县| 厦门市| 洛浦县| 辽阳县| 潼南县| 武陟县| 土默特左旗| 孙吴县| 综艺| 瑞金市| 丰原市| 得荣县| 应用必备| 湖北省| 乌什县| 常宁市| 福州市| 叶城县| 浦城县| 华阴市| 翁牛特旗| 平潭县| 平山县| 闻喜县|