一級函式
- 在 Python 所有函式都是一級物件:
- 可在執行階段建立
- 可以指派給變數,或資料結構內的元素
- 可以當成引數傳給函式
- 可以當成函式的結果回傳
- 如果一個函式的引數是包含函式,或回傳的物件是函式,它就是高階函式 (higher-order function),經典的例子是
map
、filter
、reduce
- listcomp、genexp 可作
map
與filter
的工作,且更容易閱讀,後兩者已經沒有那麼重要了 - 除了用來處理高階函式的引數外,匿名函式在 Python 並沒什麼其他用處,且通常難以閱讀
- Python 的七種 callable
- User-defined functions:
def
或lambda
- Built-in functions:以 C 寫成的函式,如
len
、time.strftime
- Built-in methods:以 C 寫成的方法,如
dict.get
- Methods
- Classes: 透過
__new__
建立,再經__init__
初始化 - Class instances:須實作
__call__
(任何物件都能有函式的行為) - Generator functions
- User-defined functions:
- 如同自訂類別的實例,函式會使用
__dict__
儲存特定的使用者屬性 - 幾個重要的函式專用特殊方法:`
__annotations__
:參數與回傳註解__closure__
:綁定自由變數(free variables)的空間__code__
:中繼資料、及編碼後的函式內文__defaults__
:以 tuple 儲存正式參數的預設值__kwdefaults__
:以 dict 儲存限關鍵字的正式參數的預設值
- 要知道函式需要什麼參數、以及有沒有預設值,使用
inspect
模組會比較方便。因為__(kw)defaults__
雖然儲存了預設值,但參數名稱卻是放在__code__
裡面,必須由後往前掃描一次,才能將每一個值與各自的參數連結
1 2 3 4 5 6 7 8 9 10 11 |
>>> from inspect import signature >>> def tag(name, *content, cls=None, **attrs): pass >>> sig = signature(tag) >>> for name, param in sig.parameters.items(): ... print(param.kind, ":", name, "=", param.default) ... POSITIONAL_OR_KEYWORD : name = <class 'inspect._empty'> VAR_POSITIONAL : content = <class 'inspect._empty'> KEYWORD_ONLY : cls = None VAR_KEYWORD : attrs = <class 'inspect._empty'> |
inspect.Signature
物件有一個 bind 方法可以拿來測試傳入的參數組合- 函式註釋(Function Annotations)常見的型態是類別,如
str
、int
,或字串如int > 0
,註釋不會處理任何工作,會被保存在__annotation__
屬性裡 - 對解譯器來說,註釋沒有意義,它只們是可能會被工具所使用的中繼資料
- Guido 清楚地表示不想讓 Python 成為 Funtional Programming 語言(但是因為有
operator
、functools
模組,可以善加運用在 FP 風格上) - 列出一些有用的 FP 工具:
operator.itemgetter
、operator.attrgetter
、operator.methodcaller
、functools.partial
- 在 Python 中廣泛採用 FP 語法的最大障礙,就是缺乏尾部遞迴消除 (tail-recursion elimination) 的最佳化功能
- 「所有匿名函式都有一個嚴重的缺點:它們沒有名字」
一級函式的 Design Pattern
- 你使用什麼語言,會決定哪些設計模式可用
- 在一級函式的語言,可多參考的模式有:
Strategy
、Command
、Template Method
、Visitor
- 將設計模式與語言功能配對並不是一件精準的科學
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 解譯器會假設在函式內文中被賦值的變數是區域變數,若該變數不是區域變數,須用
global
或nonlocal
標記之
附上解釋 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 是一種函式,有自己額外的變數範圍,且這個變數範圍經過某種「疊加/繫結」,能把外部的「非全域變數」也一起納入進來——即使這些變數不存在於函式本文,在函式內還是能夠存取它們,而這種疊加的前提是函式與函式之間要有相互嵌套的關係
舉個例子來說:
1 2 3 4 5 6 7 8 9 |
def make_avg(): nums = [] def avg(num): nums.append(num) return sum(nums) / len(nums) return avg |
- 對 make_avg 來說, nums 是區域變數
- 對 avg 來說,nums 是自由變數 (free variable),代表該變數不會被綁死在區域範圍內
特別記一下通用函式 (generic fucntion) 的實作範例:使用 functools.singledispatch
,根據第一個引數的型態來採取不同作法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
@functools.singledispatch def htmlize(obj): pass @htmlize.register(str) def _(text): pass @htmlize.register(numbers.Intergal) def _(num): pass @htmlize.register(tuple) @htmlize.register(abc.MutableSequence) def _(seq): pass |
- 使用
numbers.Integral
、abc.MutableSequence
等 ABC,而不是int
與list
等具體的實作,可以讓你的程式支援更多種相容的型態
對於帶有複雜邏輯的修飾器,最好是用內含 __call__
的自訂類別來代替一般函式,以下附上 Class Decorator 實作的範例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Logger: def __init__(self, func): self.func = func def __call__(self, *args, **kwargs): print(f"Execute {self.func.__name__}.") return self.func(*args, **kwargs) @Logger def f1(): pass f1() # 輸出 "Execute f1." |
若需要額外引述的作法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
from datetime import datetime class Logger: def __init__(self, show_dt=False): self.show_dt = show_dt def __call__(self, func): def decorate(*args, **kwargs): if self.show_dt: print(datetime.now(), end=' : ') print(f"Execute {func.__name__}.") return func(*args, **kwargs) return decorate @Logger(show_dt=True) def f2(): pass f2() # 輸出為 "2022-02-23 21:19:51.207595 : Execute f2." |
- 對自由變數的求值,可分成:
- 動態範圍 (dynamic scope):只看「呼叫函式」的環境,實作上較簡單但也較危險
- 語彙範圍 (lexical scope):考慮函式被定義的環境來取值,須要支援 closure,較複雜但已是當今泛函語言的常態
- 有一種特定的 DP 叫做 Decorator,在實作層面上,Python 的 decorator 並不像這種設計模式