在翻閱 Python 的函式庫時常常會看到定義參數的地方放了 *args**kwargs 這樣的東西,這究竟是什麼呢?讓我們先談談函式參數的定義。

函式參數語法基礎

預設參數

一般的定義方法就不多說了,直接來看有預設值的參數:

def plus(a, b, c=None):
    res = a + b + (c if c else 0)
    return res

預設參數的用處通常是實作函式重載用的,可以使一個函式在接受引數時更有彈性,而要注意的語法問題是:預設參數在函式定義時一定要放在非預設參數的後面。

但如果我們想實作無限版的 plus() 函式呢?總不可能一直增加預設參數吧! 這時候我們可以用「*」來將引數收集到一個 tuple 中。

* - 收集至 Tuple

先來看看範例:

def plus(*nums):
    res = 0
    for i in nums:
        res += i
    return res

透過 * 收集的引數會被放到一個 tuple 中,所以我們可以使用 for 來對它進行迭代。

這樣就可以理解為什麼要使用 *args 這個參數了,但是 **kwargs 又是什麼呢?我們要先從關鍵字引數來說起:

關鍵字引數 Keyword Argument

在呼叫 print() 時,我們有時會指定 sep 參數做為分隔輸出的字元,或是使用 end 參數來更改最後的換行字元。像這樣不用理會參數的真正順序,而只要給定名字然後指定值的情況,就是在使用關鍵字引數。

若是直接傳入函式而沒有寫明參數名的話,則稱為位置引數(Positional Argument)。

如果我們要指定的參數太多而造成版面不簡潔的話,可以考慮使用「**」來拆解一個裝有參數名與值的 dict。

** 第一招 - 拆解 Dict

原諒我使用這麼中二的小標題XDD
直接看實例應該就能懂了:

dt = {'sep': ' # ', 'end': '\n\n'}
print('hello', 'world', **dt)
# 等同於 print('hello', 'world', sep=' # ', end='\n\n')

雖然這不算真的發揮到 ** 的長處,因為我們要指定的參數不多,但就足以展現他的功用了。

上面是在處理呼叫時引數太多的問題,但如果在定義函式時,參數就太多了呢?

** 第二招 - 收集至 Dict

雖然我們可以用上面的單星號來收集到一個 tuple 中,但這樣很難知道第幾個元素代表什麼、也無法隨心所欲的選擇參數傳入了。這時我們就可以再次利用 ** 以及 dict「具名」的性質來定義函式:

def fun(**_settings):
    print(_settings)

fun(name='Sky', attack=100, hp=500)
# {'name': 'Sky', 'attack': 100, 'hp': 500}

可以看到,傳入的引數被收集成一個 dict 了,那我們要怎麼利用這個 dict 呢?可以如下:

def fun(**settings):
    settings.setdefault('attack', 50)
    settings.setdefault('defense', 0)
    print(settings)

fun(name='Sky', attack=100, hp=500)
# {'name': 'Sky', 'attack': 100, 'defense': 0, 'hp': 500}

注意第 2、3 行,我們還可以順便給定預設值,這不就跟一開始的預設參數一樣了嗎?

集大成- *** 雙管齊下

*** 都很方便,但用了 * 就不能指名;而用了 ** 就一定要指名,好像有點美中不足。其實我們可以將這兩個合併起來使用,就如同我們常看到的一樣,可以接受任意引數:

def fun(*args, **kwargs):
    print(args, kwargs, sep='\n')

唯一要注意的是,* 一定要在 ** 的前面,而呼叫函式時有名字的也一定要在沒名字的後面。這種集大成的寫法通常會在裝飾器時使用,讓裝飾器可以接受參數數量不同的函式。

再談 * 的其它用法

我們可以在傳入引數時使用 ** 來拆解 dict,那就不能用 * 來拆解 tuple 嗎?其實是可以的,只是我覺得這個沒那麼難理解,就沒有寫出來了。

另外,在 Python 3 裡,可以在定義函式時使用單獨的 * 來做為非指名參數和指名參數(唯-關鍵字引數,Keyword-Only Arguments)的區隔,底下這個範例結合了本文最上面的預設參數:

def fun(a, b=20, *, kw1, kw2=40):
    print(a, b, kw1, kw2)

fun(1, 2, kw1=3, kw2=4)  # 1 2 3 4
fun(10, kw1=30)  # 10 20 30 40
# 在傳入引數時,在 * 後面的(kw1 和 kw2)一定要以關鍵字引數(指名)傳入

這個寫法可以限制使用者一定要指名傳入引數,而不是依賴原本的順序。

在 Python 3.8 中,引入了唯-位置參數(Positional-only Parameters), 使用方式是以一個斜線 / 來表示在這之前的參數都不能指名,而只能依靠位置來傳入。

有興趣的人可以參考官方文件PEP 570

超級集大成

我們可以將 *args、分隔用的 *、以及 **kwargs 一起使用:

def fun(a, *args, kw1, **kwargs):
    print(a, args, kw1, kwargs, sep=' # ')

fun(1, 2, 3, 4, 5, kw1=6, g=7, f=8, m=9)
# 1 # (2, 3, 4, 5) # 6 # {'g': 7, 'f': 8, 'm': 9}

可以看到這裡的 *args 同時扮演了原本和分隔的角色。
好啦,我覺得這個部分可能已經不是像我這樣的新手能好好利用的了,所以就僅止於介紹而已。

這次就到這邊了!謝謝大家的閱讀 m(_ _)m,如有疑慮或指正歡迎留言提出。


參考資料:

  1. stackoverflow 中的相關問題

後記:
  其實那個什麼「唯-關鍵字引數」是我亂翻的XDD(看也知道
  另外我也終於把這個 * 的用法給搞懂了~ 希望這篇文不會太難懂