[Python] *args 和 **kwargs 是什麼?一次搞懂它們!

在翻閱 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 參數來更改最後的換行字元。像這樣不用理會參數的真正順序,而只要給定名字然後指定值的情況,就是在使用關鍵字引數。
  如果我們要指定的參數太多而造成版面不簡潔的話,可以考慮使用「**」來拆解一個裝有參數名與值的 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 呢?可以如下:(2019/7/15 補:將這部份的程式碼改得更精簡)
def fun(**settings):
    settings.setdefault('name', 'Hello')
    settings.setdefault('attack', 50)
    settings.setdefault('defense', 0)
    settings.setdefault('hp', 150)
    print(settings)

fun(name='Sky', attack=100, hp=500)
# {'name': 'Sky', 'attack': 100, 'defense': 0, 'hp': 500}
  注意第 2~5 行,我們還可以順便給定預設值,這不就跟一開始的預設參數一樣了嗎?

  • 集大成- *** 雙管齊下
  * 和 ** 都很方便,但用了 * 就不能指名;而用了 ** 就一定要指名,好像有點美中不足。其實我們可以將這兩個合併起來使用,就如同我們常看到的一樣,可以接受任意引數:
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)一定要以關鍵字引數(指名)傳入
  這個寫法可以限制使用者一定要指名傳入引數,而不是依賴原本的順序。
    • 超級集大成
  我們可以將 *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, l=9)
# 1 # (2, 3, 4, 5) # 6 # {'g': 7, 'f': 8, 'l': 9}
  可以看到這裡的 *args 同時扮演了原本和分隔的角色。
  好啦,我覺得這個部分可能已經不是像我這樣的新手能好好利用的了,所以就僅止於介紹而已。 

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

參考資料:
  1) stackoverflow 中的相關問題

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

留言

  1. 好文~
    想請問你的blogspot樣式是怎麼調的?
    也想用用這種形式

    回覆刪除
    回覆
    1. 先謝謝你的讚賞!

      如果是指整個blogger的主題的話,可以到 Blogger後台 > 主題,就會看到下面有不少的範本可以隨意取用,若是想要微調特色之類的話可以在剛剛那個頁面選擇「自訂」或「編輯HTML」。

      至於程式碼的部分,可以到這個網站 : https://prismjs.com/ 選擇你喜歡的主題。
      :)

      刪除
  2. 感謝分享!! 終於看懂怎麼用了QQ

    回覆刪除
  3. 喜歡您的解說!! 大感謝!!

    回覆刪除
  4. 清晰易懂喔~ 感謝你的分享

    回覆刪除
  5. 寫得很清楚!!
    不過 *args 不算是放入 tuple
    比較類似 iterator

    回覆刪除
    回覆
    1. 謝謝~
      不過 *args 的話是 tuple 沒錯喔,python 的 language reference 有寫說是放到 tuple 裡:

      `If the form “*identifier” is present, it is initialized to a tuple receiving any excess positional parameters, defaulting to the empty tuple.`

      -- 8.7. Function definitions (https://docs.python.org/3/reference/compound_stmts.html#function-definitions)

      另外,實際測試也是這樣:

      >>> def f(*args):
      ... print(type(args))
      ...
      >>> f()

      >>> f(3, 4, 5)

      >>>

      刪除
    2. 啊 角括號被過濾掉了
      最後的兩行程式都會印出 class 'tuple'

      刪除

張貼留言

這個網誌中的熱門文章

[C] 每天來點字串用法 (2) - strcpy()、strncpy()

[C] 每天來點字串用法 (5) - strcat()、strncat()