Skip to content

Fluent Python 讀書筆記(二)

一級函式

  • 在 Python 所有函式都是一級物件:
    • 可在執行階段建立
    • 可以指派給變數,或資料結構內的元素
    • 可以當成引數傳給函式
    • 可以當成函式的結果回傳
  • 如果一個函式的引數是包含函式,或回傳的物件是函式,它就是高階函式 (higher-order function),經典的例子是 mapfilterreduce
  • listcomp、genexp 可作 mapfilter 的工作,且更容易閱讀,後兩者已經沒有那麼重要了
  • 除了用來處理高階函式的引數外,匿名函式在 Python 並沒什麼其他用處,且通常難以閱讀
  • Python 的七種 callable
    1. User-defined functions: deflambda
    2. Built-in functions:以 C 寫成的函式,如 lentime.strftime
    3. Built-in methods:以 C 寫成的方法,如 dict.get
    4. Methods
    5. Classes: 透過 __new__ 建立,再經 __init__ 初始化
    6. Class instances:須實作 __call__(任何物件都能有函式的行為)
    7. Generator functions
  • 如同自訂類別的實例,函式會使用 __dict__ 儲存特定的使用者屬性

  • 幾個重要的函式專用特殊方法:`
    • __annotations__:參數與回傳註解
    • __closure__:綁定自由變數(free variables)的空間
    • __code__:中繼資料、及編碼後的函式內文
    • __defaults__:以 tuple 儲存正式參數的預設值
    • __kwdefaults__:以 dict 儲存限關鍵字的正式參數的預設值
  • 要知道函式需要什麼參數、以及有沒有預設值,使用 inspect 模組會比較方便。因為 __(kw)defaults__ 雖然儲存了預設值,但參數名稱卻是放在 __code__ 裡面,必須由後往前掃描一次,才能將每一個值與各自的參數連結

  • inspect.Signature 物件有一個 bind 方法可以拿來測試傳入的參數組合
  • 函式註釋(Function Annotations)常見的型態是類別,如 strint,或字串如 int > 0,註釋不會處理任何工作,會被保存在 __annotation__ 屬性裡
  • 對解譯器來說,註釋沒有意義,它只們是可能會被工具所使用的中繼資料
  • Guido 清楚地表示不想讓 Python 成為 Funtional Programming 語言(但是因為有 operatorfunctools 模組,可以善加運用在 FP 風格上)
  • 列出一些有用的 FP 工具:operator.itemgetteroperator.attrgetteroperator.methodcallerfunctools.partial
  • 在 Python 中廣泛採用 FP 語法的最大障礙,就是缺乏尾部遞迴消除 (tail-recursion elimination) 的最佳化功能
  • 「所有匿名函式都有一個嚴重的缺點:它們沒有名字」

一級函式的 Design Pattern

  • 你使用什麼語言,會決定哪些設計模式可用
  • 在一級函式的語言,可多參考的模式有: StrategyCommandTemplate MethodVisitor
  • 將設計模式與語言功能配對並不是一件精準的科學

Strategy

三大要素:
* Context:負責指派一些演算的工作給外部元件(可替換的)
* Strategy: 抽象類別,可視為多種演算法元件的共用界面
* Concrete Strategy:具體的子類別

重點整理:
* 個人覺得這模式頗類似 duck typing —— 只要你會滑水、會呱呱叫,你就可以是一隻鴨子
* 在抽象類別上,可以指定 metaclass=abc.ABCMeta 讓這個模式更加明確
* 以 FP 導向來看,其實「函式」就可以充當那個抽象類別,因為「任何函式都是可以呼叫的」(假設相關的具體函式都吃一樣的參數的話)
* Strategy 物件通常是很好共用的,可同時在多種情境下使用

Command

  • 藉由將函式當成引數傳遞,來進行簡化的設計模式
  • 目的是將負責呼叫的物件 (Invoker) 跟實作它的提供者物件 (Receiver) 解耦合,將 Command 物件放在兩者之間,實作一個內含一個方法的界面,執行它之後,它會呼叫 Receiver 的一些方法,而 Invoker 不需要知道 Receiver 是怎麼實作的
  • Command 可以單純只是一個函式,也可以是一個子類別的實體,只它要可以被特定方式呼叫(所以基本上跟 Strategy 模式是差不多的概念)
  • 在 Python 中,使用 closure 就可做到類似的事情了
  • 個人覺得這個模式跟 Ptthon 的 WSGI 的設計理念很像

Decorator 及 Closure

  • 嚴格來說,修飾器只是糖衣語法,因為它基本上就是將被修飾的函式換成不同的函式: target = decorate(target)
  • 可讓我們標記函式,來改善函式的行為(透過把函式對象當成引數傳入)
  • 一定要先懂 closure
  • 修飾器會在函式對象被「定義」(或模組載入時)時馬上執行;相反地,被裝飾的函式對象只是一般函式,只有在被明確呼叫時才會執行
  • Python 解譯器會假設在函式內文中被賦值的變數是區域變數,若該變數不是區域變數,須用 globalnonlocal 標記之

附上解釋 Clousure 的原文,翻譯的實在太難讀

a closure is a function with an extended scope that encompasses nonglobal
variables referenced in the body of the function but not defined there

較好的理解應該是:Closure 是一種函式,有自己額外的變數範圍,且這個變數範圍經過某種「疊加/繫結」,能把外部的「非全域變數」也一起納入進來——即使這些變數不存在於函式本文,在函式內還是能夠存取它們,而這種疊加的前提是函式與函式之間要有相互嵌套的關係

舉個例子來說:

  • 對 make_avg 來說, nums 是區域變數
  • 對 avg 來說,nums 是自由變數 (free variable),代表該變數不會被綁死在區域範圍內

特別記一下通用函式 (generic fucntion) 的實作範例:使用 functools.singledispatch,根據第一個引數的型態來採取不同作法

  • 使用 numbers.Integralabc.MutableSequence 等 ABC,而不是 intlist 等具體的實作,可以讓你的程式支援更多種相容的型態

對於帶有複雜邏輯的修飾器,最好是用內含 __call__ 的自訂類別來代替一般函式,以下附上 Class Decorator 實作的範例:

若需要額外引述的作法:


  • 對自由變數的求值,可分成:
    1. 動態範圍 (dynamic scope):只看「呼叫函式」的環境,實作上較簡單但也較危險
    2. 語彙範圍 (lexical scope):考慮函式被定義的環境來取值,須要支援 closure,較複雜但已是當今泛函語言的常態
  • 有一種特定的 DP 叫做 Decorator,在實作層面上,Python 的 decorator 並不像這種設計模式

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *