Python函數式編程指南(一):概述

這大概算是Python最難啃的一塊骨頭吧。在我 Python生涯的這一年裏,我遇到了一些Pythoner,他們毫無例外地徹底不會使用函數式編程(有些人喜歡稱爲Pythonic),好比,歷來不會 傳遞函數,不知道lambda是什麼意思,知道列表展開但歷來不知道用在哪裏,對Python不提供經典for循環感到無所適從,言談之中表現出對函數式 風格的一種抗拒甚至厭惡。 html

我嘗試剖析這個問題,最終總結了這麼兩個緣由:一、不想改變,認爲現有的知識能夠完成任務;二、對小衆語言的歧視,Python目前在國內市場份額仍然很小很小,熟悉Python風格用處不大。 算法

然而我認爲,學習使用一種大相徑庭的風格能夠顛覆整個編程的思想。我會慢慢總結一個系列共4篇文字,篇幅都不大,輕鬆就能看完,但願對喜歡Python的人們有所幫助,由於我我的確實從中受益不淺。 編程

仍是那句老話,尊重做者的勞動,轉載請註明原做者和原地址:)數據結構

1. 函數式編程概述

1.1. 什麼是函數式編程?

函數式編程使用一系列的函數解決問題。函數僅接受輸入併產生輸出,不包含任何能影響產生輸出的內部狀態。任何狀況下,使用相同的參數調用函數始終能產生一樣的結果。 閉包

在 一個函數式的程序中,輸入的數據「流過」一系列的函數,每個函數根據它的輸入產生輸出。函數式風格避免編寫有「邊界效應」(side effects)的函數:修改內部狀態,或者是其餘沒法反應在輸出上的變化。徹底沒有邊界效應的函數被稱爲「純函數式的」(purely functional)。避免邊界效應意味着不使用在程序運行時可變的數據結構,輸出只依賴於輸入。 app

能夠認爲函數式編程恰好站在了面 向對象編程的對立面。對象一般包含內部狀態(字段),和許多能修改這些狀態的函數,程序則由不斷修改狀態構成;函數式編程則極力避免狀態改動,並經過在函 數間傳遞數據流進行工做。但這並非說沒法同時使用函數式編程和麪向對象編程,事實上,複雜的系統通常會採用面向對象技術建模,但混合使用函數式風格還能 讓你額外享受函數式風格的優勢。 編程語言

1.2. 爲何使用函數式編程?

函數式的風格一般被認爲有以下優勢: ide

  • 邏輯可證
    這是一個學術上的優勢:沒有邊界效應使得更容易從邏輯上證實程序是正確的(而不是經過測試)。
  • 模塊化
    函數式編程推崇簡單原則,一個函數只作一件事情,將大的功能拆分紅儘量小的模塊。小的函數更易於閱讀和檢查錯誤。
  • 組件化
    小的函數更容易加以組合造成新的功能。
  • 易於調試
    細化的、定義清晰的函數使得調試更加簡單。當程序不正常運行時,每個函數都是檢查數據是否正確的接口,能更快速地排除沒有問題的代碼,定位到出現問題的地方。
  • 易於測試
    不依賴於系統狀態的函數無須在測試前構造測試樁,使得編寫單元測試更加容易。
  • 更高的生產率
    函數式編程產生的代碼比其餘技術更少(每每是其餘技術的一半左右),而且更容易閱讀和維護。
1.3. 如何辨認函數式風格?

支持函數式編程的語言一般具備以下特徵,大量使用這些特徵的代碼便可被認爲是函數式的: 模塊化

  • 函數是一等公民
    函數能做爲參數傳遞,或者是做爲返回值返回。這個特性使得模板方法模式很是易於編寫,這也促使了這個模式被更頻繁地使用。
    以一個簡單的集合排序爲例,假設lst是一個數集,並擁有一個排序方法sort須要將如何肯定順序做爲參數。
    若是函數不能做爲參數,那麼lst的sort方法只能接受普通對象做爲參數。這樣一來咱們須要首先定義一個接口,而後定義一個實現該接口的類,最後將該類的一個實例傳給sort方法,由sort調用這個實例的compare方法,就像這樣:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #僞代碼
    interface Comparator {
         compare(o1, o2)
    }
    lst = list ( range ( 5 ))
    lst.sort(Comparator() {
         compare(o1, o2) {
             return o2 - o1 / / 逆序
    })
    可見,咱們定義了一個新的接口、新的類型(這裏是一個匿名類),並new了一個新的對象只爲了調用一個方法。若是這個方法能夠直接做爲參數傳遞會怎樣呢?看起來應該像這樣:

    1
    2
    3
    4
    def compare(o1, o2):
         return o2 - o1 #逆序
    lst = list ( range ( 5 ))
    lst.sort(compare)
    請注意,前一段代碼已經使用了匿名類技巧從而省下了很多代碼,但仍然不如直接傳遞函數簡單、天然。
  • 匿名函數(lambda)
    lambda提供了快速編寫簡單函數的能力。對於偶爾爲之的行爲,lambda讓你再也不須要在編碼時跳轉到其餘位置去編寫函數。
    lambda表達式定義一個匿名的函數,若是這個函數僅在編碼的位置使用到,你能夠現場定義、直接使用:

    1
    lst.sort( lambda o1, o2: o1.compareTo(o2))
    相信從這個小小的例子你也能感覺到強大的生產效率:)
  • 封裝控制結構的內置模板函數
    爲了避開邊界效應,函數式風格儘可能避免使用變量,而僅僅爲了控制流程而定義的循環變量和流程中產生的臨時變量無疑是最須要避免的。
    假如咱們須要對剛纔的數集進行過濾獲得全部的正數,使用指令式風格的代碼應該像是這樣:
    ?
    1
    2
    3
    4
    lst2 = list ()
    for i in range ( len (lst)): #模擬經典for循環
         if lst[i] > 0 :
             lst2.append(lst[i])
    這段代碼把從建立新列表、循環、取出元素、判斷、添加至新列表的整個流程完整的展現了出來,儼然把解釋器當成了須要手把手指導的傻瓜。然而,「過濾」這個動做是很常見的,爲何解釋器不能掌握過濾的流程,而咱們只須要告訴它過濾規則呢?
    在Python裏,過濾由一個名爲filter的內置函數實現。有了這個函數,解釋器就學會了如何「過濾」,而咱們只須要把規則告訴它:

    1
    lst2 = filter ( lambda n: n > 0 , lst)
    這個函數帶來的好處不只僅是少寫了幾行代碼這麼簡單。
    封裝控制結構後,代碼中就只須要描述功能而不是作法,這樣的代碼更清晰,更可讀。由於避開了控制結構的干擾,第二段代碼顯然能讓你更容易瞭解它的意圖。
    另外,由於避開了索引,使得代碼中不太可能觸發下標越界這種異常,除非你手動製造一個。
    函數式編程語言一般封裝了數個相似「過濾」這樣的常見動做做爲模板函數。惟一的缺點是這些函數須要少許的學習成本,但這絕對不能掩蓋使用它們帶來的好處。
  • 閉包(closure)
    閉包是綁定了外部做用域的變量(但不是全局變量)的函數。大部分狀況下外部做用域指的是外部函數。
    閉包包含了自身函數體和所需外部函數中的「變量名的引用」。引用變量名意味着綁定的是變量名,而不是變量實際指向的對象;若是給變量從新賦值,閉包中能訪問到的將是新的值。
    閉包使函數更加靈活和強大。即便程序運行至離開外部函數,若是閉包仍然可見,則被綁定的變量仍然有效;每次運行至外部函數,都會從新建立閉包,綁定的變量是不一樣的,不須要擔憂在舊的閉包中綁定的變量會被新的值覆蓋。
    回到剛纔過濾數集的例子。假設過濾條件中的 0 這個邊界值再也不是固定的,而是由用戶控制。若是沒有閉包,那麼代碼必須修改成:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class greater_than_helper:
         def __init__( self , minval):
             self .minval = minval
         def is_greater_than( self , val):
             return val > self .minval
     
    def my_filter(lst, minval):
         helper = greater_than_helper(minval)
         return filter (helper.is_greater_than, lst)

    請注意咱們如今已經爲過濾功能編寫了一個函數my_filter。如你所見,咱們須要在別的地方(此例中是類greater_than_helper)持有另外一個操做數minval。
    若是支持閉包,由於閉包能夠直接使用外部做用域的變量,咱們就再也不須要greater_than_helper了:

    1
    2
    def my_filter(lst, minval):
         return filter ( lambda n: n > minval, lst)

    可見,閉包在不影響可讀性的同時也省下了很多代碼量。
    函數式編程語言都提供了對閉包的不一樣程度的支持。在Python 2.x中,閉包沒法修改綁定變量的值,全部修改綁定變量的行爲都被當作新建了一個同名的局部變量並將綁定變量隱藏。Python 3.x中新加入了一個關鍵字 nonlocal 以支持修改綁定變量。但無論支持程度如何,你始終能夠訪問(讀取)綁定變量。
  • 內置的不可變數據結構
    爲了避開邊界效應,不可變的數據結構是函數式編程中不可或缺的部分。不可變的數據結構保證數據的一致性,極大地下降了排查問題的難度。
    例如,Python中的元組(tuple)就是不可變的,全部對元組的操做都不能改變元組的內容,全部試圖修改元組內容的操做都會產生一個異常。
    函數式編程語言通常會提供數據結構的兩種版本(可變和不可變),並推薦使用不可變的版本。
  • 遞歸
    遞歸是另外一種取代循環的方法。遞歸實際上是函數式編程很常見的形式,常常能夠在一些算法中見到。但之因此放到最後,是由於實際上咱們通常很 少用到遞歸。若是一個遞歸沒法被編譯器或解釋器優化,很容易就會產生棧溢出;另外一方面複雜的遞歸每每讓人感受迷惑,不如循環清晰,因此衆多最佳實踐均指出 使用循環而非遞歸。
    這一系列短文中都不會關注遞歸的使用。
相關文章
相關標籤/搜索