想要一個三列三欄、每格都是 0 的二維串列,用乘號寫可以漂亮的寫一行,印出來的結果也跟預期一樣:
grid = [[0] * 3] * 3
print(grid)
# [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
接著把第一列中間那格改成 5:
grid[0][1] = 5
print(grid)
# [[0, 5, 0], [0, 5, 0], [0, 5, 0]]
程式只改了 grid[0][1],第二列和第三列的中間卻也一起變成 5。到底是發生甚麼事情呢?(文中的輸出都是在 Python 3.14.3 上執行的結果。)
三列是不是同一個串列
先比對三列本身。is 比對的是是否是同一個物件(參考之前寫的Python 的 is 和 == 差在哪),id() 則會回傳物件的識別碼:
print(grid[0] is grid[1], grid[1] is grid[2])
# True True
print([id(row) for row in grid])
# [3132406798336, 3132406798336, 3132406798336]
兩次比對都是 True,三個 id 也完全一樣。外層串列雖然有三個位置,放的卻是同一個內層串列。不管從 grid[0]、grid[1] 還是 grid[2] 進去改,改到的都是同一份資料,print 則是把同一份資料印了三次。所以一開始每格都是 0,印三次也看不出差別,要等改了其中一格才會發現。
Python 官方 FAQ 在「怎麼建立多維串列」說明,用 * 複製串列不會建立副本,只會建立指向原本物件的參考(replicating a list with * doesn’t create copies, it only creates references to the existing objects)。
那 [0] * 3 這為什麼沒事?三個位置指向的其實也是同一個整數 0,但 grid[0][1] = 5 是把那個位置換成另一個物件,0 本身沒有被修改,所以同一列的第一格和第三格還是 0。只是被換掉的位置就在大家共用的內層串列裡。
改用 list comprehension
FAQ 建議的做法是讓每一列各自建立一次:先建立長度足夠的外層串列,再用迴圈把新建立的串列填寫進去,另外也可以用 list comprehension。下面用的是 list comprehension,一行就寫得完:
grid = [[0] * 3 for _ in range(3)]
grid[0][1] = 5
print(grid)
# [[0, 5, 0], [0, 0, 0], [0, 0, 0]]
print(grid[0] is grid[1])
# False
[0] * 3 放進 comprehension 裡之後,迴圈每跑一次就會重新執行一次,三列是三個分別建立的串列,這次就只有第一列改變,is 的比對也變成 False。
那什麼時候可以用乘號
乘號重複的也可能是空串列。內建型別文件談 s * n 時舉的例子就是 [[]] * 3,三個元素都是同一個空串列,對其中一個呼叫 append時都會看到。
所以看到用乘號重複的寫法,可以先找出重複的是哪一個物件,再看後面的程式會不會直接修改這個物件,例如改其中某一格、呼叫 append,或是對串列使用 +=。+= 看起來像是把整個位置換掉,但用在串列上其實是直接修改原本的串列,[[]] * 3 的第一個位置做完 += [1],印出來一樣是 [[1], [1], [1]]。
如果會修改,就換成每個位置各自建立一次的寫法;如果被重複的是 0 這種不會被修改的值,或是之後只會用 grid[0] = [1, 2, 3] 這樣的寫法把整個位置換成別的物件,用乘號就不會遇到這個情況。
修正後的寫法裡還留著 [0] * 3,FAQ 建議的迴圈第一行也是 A = [None] * 3,所以乘號本身還是可以放心使用。這篇就當作小提醒,如果有遇過其他類似的情況,也歡迎討論。
