不要用 list、dict、set 這類可變物件當函式的預設參數值。改成 target=None,進函式後再 if target is None: target = []。原本的寫法會讓所有沒傳這個參數的呼叫共用同一個物件,資料就跨呼叫累積下來。

在 Python 裡,用可變物件當預設參數值,會讓資料在多次呼叫之間累積。先看這段程式:

def append_to(x, target=[]):
    target.append(x)
    return target

append_to(1)   # -> [1]
append_to(2)   # -> [1, 2]

第二次呼叫 append_to(2) 並沒有傳 target,照直覺應該從空串列開始、回傳 [2]。實際回傳的是 [1, 2],第一次呼叫留下的 1 還在。

原因在 def 這一行的行為。Python 描述函式定義時寫:「Default parameter values are evaluated from left to right when the function definition is executed.」預設值的計算時機是函式被定義的那一刻,也就是直譯器執行到 def 這行時,就從左到右算好,此後每次呼叫都沿用這個結果、不再重算。文件接著補一句:「This means that the expression is evaluated once, when the function is defined, and that the same “pre-computed” value is used for each call.」那個 [] 只被建立一次,之後每次呼叫都沿用同一個串列。

函式物件本身就把這個預設值存著,可以直接印出來看。預先算好的預設值放在函式的 __defaults__,那是一個 tuple。把前面的例子改一下,在每次呼叫時比對 target__defaults__ 裡的第一個元素是不是同一個物件:

def append_to(x, target=[]):
    print(target is append_to.__defaults__[0])  # True:每次都是同一個物件
    target.append(x)
    return target

append_to(1)   # -> [1],印出 True
append_to(2)   # -> [1, 2],印出 True
print(append_to.__defaults__)   # -> ([1, 2],)

is 這個運算子比的是身分,也就是兩個名字是不是指向記憶體裡同一個物件(== 則是比值,兩者差別見 Python 的 is 和 == 差在哪)。每次呼叫都印出 True,表示函式收到的 target 和掛在 append_to.__defaults__[0] 的是同一個串列。呼叫累積內容的同時,__defaults__ 裡那個串列也跟著變長,最後 __defaults__ 印出來是 ([1, 2],)

官方 FAQ 針對這題的說法是同一件事:「Default values are created exactly once, when the function is defined. If that object is changed, like the dictionary in this example, subsequent calls to the function will refer to this changed object.」用 dict 當預設值也會這樣,任何可變物件都適用。

修法是把可變物件從預設值裡拿掉,改用 None 當哨兵,真正的空串列到函式內部才建立:

def append_ok(x, target=None):
    if target is None:
        target = []
    target.append(x)
    return target

None 是不可變的,每次呼叫拿到的都是它,不會被改動。呼叫者沒傳 target 時,target = [] 這行會在該次呼叫的當下跑一次,每次都是一個新的空串列,彼此不共用;呼叫者有傳自己的串列時,None 檢查不成立,就照他傳進來的用。

這個累積行為就是 def 當下把預設值求值一次這條規則的結果。實務上就是換掉可變預設值:改用 None 哨兵,把空串列的建立搬進函式內部。想省事不必每次自己盯,Ruff 與 flake8-bugbear 都有一條 B006 規則,專門抓函式簽名裡的可變預設值,在寫進去的當下就會提醒。