###### tags: `Python` # 你的參數預設值不是你想的預設值 撰寫函式的時候, 幫參數加上預設值是個便利的作法, 使用函式時就不一定要傳上一大堆的參數, 不過在使用參數的預設值時你可能沒有注意到預設值產生的時間點, 導致實際叫用函式時傳入了奇怪的參數值, 執行後得到異常的結果。 ## 參數預設值只會在定義函式時產生 1 次 舉個例子來說, 如果我想要設計一個幫我以 hh\:mm\:ss 格式顯示時間的函式, 大概會這樣設計: ```python >>> def print_time(t): ... t_struct = time.localtime(t) ... print(F'{t_struct[3]:02d}:{t_struct[4]:02d}:{t_struct[5]:02d}') >>> ``` 實際使用時就像是這樣: ```python >>> print_time(time.time()) 22:09:19 >>> print_time(time.time()) 22:09:22 >>> print_time(time.time()) 22:09:24 >>> ``` 但是我發現使用這個函式時最常傳入的就是現在的時間, 因此就幫參數 `t` 加上預設值如下: ```python >>> def print_time(t = time.time()): ... t_struct = time.localtime(t) ... print(F'{t_struct[3]:02d}:{t_struct[4]:02d}:{t_struct[5]:02d}') >>> ``` 執行看看: ```python >>> print_time(time.time()) 22:05:44 >>> print_time() 22:05:33 >>> print_time() 22:05:33 >>> print_time() 22:05:33 >>> ``` 直接傳入參數沒問題, 但是使用參數預設值時怎麼怪怪的, 雖然是在傳入參數叫用後才執行, 但時間點怎麼會比較早?而且不管叫用幾次, 參數的預設值都不會變? 這主要的問題就是參數的預設值是在定義函式的時候產生, 而且只會產生 1 次, 因此上例中雖然參數的預設值是叫用 `time.time()` 的傳回值, 但這只會執行 1 次, 因此之後不論在什麼時候叫用函式, 參數的預設值都會一樣。以剛剛的例子來說, `t` 的預設值就是定義函式時叫用 `time.time()` 傳回的時間點, 之後叫用函式時都不會再重新叫用 `time.time()`, 因此 `t` 的預設值都是同一個時間。 ## 檢查是否有傳入參數再計算預設值 如果你的參數預設值需要隨叫用的時間點而變化, 那麼最好改成在函式中檢查是否真的有傳入參數, 然後再計算該時間點的預設值, 例如: ```python >>> def print_time(t = None): ... if t == None: ... t = time.time() ... t_struct = time.localtime(t) ... print(F'{t_struct[3]:02d}:{t_struct[4]:02d}:{t_struct[5]:02d}') >>> ``` 執行後就會正確了: ```python >>> print_time() 22:16:52 >>> print_time() 22:16:54 >>> print_time(time.time()) 22:17:03 >>> ``` ## 小心使用可變的資料當預設值 由於參數的預設值是在定義函式時產生, 若是以可變的資料當成預設值, 而且會在函式內變更資料內容, 下一次叫用時的預設值就會是變更後的內容, 例如: ```python >>> def add_list(l = []): ... print(l) ... sum = 0; ... for i in l: ... sum += i ... print(sum) ... l.append(1) >>> ``` 這個函式會把傳入的串列內容印出後加總, 並在傳入的串列尾端加入新的項目 1, 所以執行結果會是這樣: ```python >>> add_list([1, 2, 3]) [1, 2, 3] 6 >>> ``` 如果不傳入參數使用預設值, 就會是這樣: ```python >>> add_list() [] 0 >>> add_list() [1] 1 >>> add_list() [1, 1] 2 >>> add_list() [1, 1, 1] 3 >>> ``` 你會發現每次叫用得到的加總值都會增加 1, 這是因為作為預設值的串列會在每次叫用時在尾端新增項目 1, 所以下次叫用時加總值就會多 1。 如果你希望每次叫用時預設值都要是空的串列, 那就一樣可以比照前面的作法, 先檢查是否有傳入參數, 再設定參數的預設值。 ## 小結 Python 雖然有許多便利的語法, 但是如果沒有注意相關的細節, 往往會在遇到異常結果時不知所措, 但只要瞭解實際的運作方式, 就可以用正確的方式撰寫程式。