本文 AI 產出,尚未審核
Python 資料結構 – 清單(list)
複製與淺拷貝、深拷貝
簡介
在日常開發中,我們常會需要 複製 既有的 list,無論是為了暫存、做變更前的快照,或是在多執行緒環境下避免資料競爭。
看似簡單的「把清單丟給另一個變數」卻暗藏「引用」的概念:兩個變數其實指向同一塊記憶體,對其中一個的變更會直接影響另一個。
因此,了解 淺拷貝(shallow copy) 與 深拷貝(deep copy) 的差異,才能在程式設計時避免不預期的副作用,寫出更安全、可維護的程式碼。本文將從概念說明、實作範例、常見陷阱與最佳實踐,帶你完整掌握 Python 清單的複製技巧。
核心概念
1. 為什麼直接指派不是「複製」?
a = [1, 2, 3]
b = a # 直接指派
b[0] = 99
print(a) # 輸出: [99, 2, 3],a 也被改變了
b = a只是把a的 引用 複製給b,兩者共享同一個物件。- 這在需要 獨立 操作的情境(如資料快照、測試)會造成問題。
2. 淺拷貝(shallow copy)
定義:建立一個新清單,容器本身 被複製,但裡面的元素仍是原本物件的引用。
適用於 清單內只含不可變物件(如 int, float, str)的情況。
常見取得淺拷貝的方法
| 方法 | 說明 |
|---|---|
list.copy() |
Python 3.3+ 內建的淺拷貝方法 |
切片 [:] |
簡潔且廣為人知 |
list() 建構子 |
以另一個可迭代物件建立新清單 |
copy.copy() |
copy 模組的函式,功能相同 |
範例 1:使用 list.copy() 與切片
original = [10, 20, 30]
shallow1 = original.copy() # 方法 1
shallow2 = original[:] # 方法 2
shallow1[0] = 999
print(original) # [10, 20, 30] 原本未受影響
print(shallow1) # [999, 20, 30]
範例 2:淺拷貝與內嵌可變物件
original = [[1, 2], [3, 4]]
shallow = original.copy() # 只複製外層 list
shallow[0][0] = 99 # 改變內層 list 的元素
print(original) # [[99, 2], [3, 4]] 內層仍被改動
print(shallow) # [[99, 2], [3, 4]]
重點:當清單中包含 可變物件(如子清單、字典、物件實例)時,淺拷貝只能保護外層結構,內層仍共享同一個實體。
3. 深拷貝(deep copy)
定義:不僅複製容器本身,還會遞迴地 複製所有子物件,產生一個與原物件完全獨立的結構。
適用於任意深度、任意型別的嵌套資料。
使用 copy.deepcopy()
import copy
original = [[1, 2], [3, 4]]
deep = copy.deepcopy(original)
deep[0][0] = 777
print(original) # [[1, 2], [3, 4]] 完全不受影響
print(deep) # [[777, 2], [3, 4]]
範例 3:深拷貝自訂類別
import copy
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
p1 = Point(5, 8)
container = [p1, [10, 20]]
deep_clone = copy.deepcopy(container)
# 修改內層物件
deep_clone[0].x = 999
deep_clone[1][0] = 777
print(container[0].x) # 5 原始物件未被改動
print(container[1][0]) # 10 同上
註:
deepcopy會呼叫物件的__deepcopy__方法(若有實作),否則使用預設的遞迴複製機制。
4. 何時選擇淺拷貝或深拷貝?
| 情境 | 建議使用 |
|---|---|
清單只包含不可變資料(int, float, str) |
淺拷貝(更快、佔用較少記憶體) |
| 清單內有子清單、字典、或自訂可變物件 | 深拷貝(確保資料完全獨立) |
| 只需要 暫時 讀取或遍歷而不改變內容 | 直接 指派 或 迭代器(不需複製) |
| 大型資料結構且效能是關鍵 | 盡量 避免不必要的深拷貝,改用寫時複製(copy‑on‑write) 的設計模式 |
常見陷阱與最佳實踐
1. 誤以為切片 [:] 能深拷貝
切片只做 淺層 複製,對於嵌套結構仍會共用內部物件。
解決方案:對於多層結構,使用 copy.deepcopy()。
2. 在迴圈中不小心共享同一個子清單
matrix = [[0]*3]*3 # 錯誤寫法
matrix[0][0] = 1
print(matrix) # [[1, 0, 0], [1, 0, 0], [1, 0, 0]]
- 這裡產生的是同一個子清單的多個引用。
- 正確寫法:
matrix = [[0]*3 for _ in range(3)] # 正確
matrix[0][0] = 1
print(matrix) # [[1, 0, 0], [0, 0, 0], [0, 0, 0]]
3. 深拷貝的效能成本
deepcopy 會遍歷整個物件圖,對大型資料結構可能非常慢且佔用大量記憶體。
最佳實踐:
- 只在確實需要完全隔離時才使用。
- 若只需要「部分」深拷貝,可手動複製關鍵子結構(例如
list(map(list, outer))只複製兩層)。 - 使用 不可變 資料(如
tuple、frozenset)減少拷貝需求。
4. 自訂類別的拷貝行為
若類別內部包含大量資源(檔案、網路連線),直接 deepcopy 可能導致錯誤或資源浪費。
做法:
- 實作
__copy__與__deepcopy__,在拷貝時只複製必要屬性,或返回同一個實例(視需求而定)。
class LargeResource:
def __init__(self, path):
self.file = open(path, 'r')
def __deepcopy__(self, memo):
# 只拷貝檔案路徑,避免重新開檔
new_obj = type(self)(self.file.name)
memo[id(self)] = new_obj
return new_obj
實際應用場景
| 場景 | 為何需要拷貝 | 建議方法 |
|---|---|---|
| 資料快照(Versioning) | 在修改前保留原始狀態,以便回溯或比較 | 使用 淺拷貝(若資料為不可變)或 深拷貝(若有嵌套) |
| 多執行緒/多程序共享資料 | 防止一個執行緒改變另一個執行緒的資料 | 以 深拷貝 建立獨立副本,或使用 Queue 內建的資料傳遞 |
| 函式的預設參數 | 避免預設值被意外改變 | 在函式內部使用 param = param.copy()(淺拷貝) |
| 測試環境的資料隔離 | 測試案例需要在同一基礎資料上多次執行,且不互相影響 | 以 deepcopy 為測試前的「重置」步驟 |
| 資料序列化前的防護 | 在 json.dumps 前,確保不會因為序列化過程修改原始物件 |
先 deepcopy,再對副本進行清理或轉型 |
總結
- 指派 只會複製 引用,不會產生新物件。
- 淺拷貝 複製外層容器,內層仍共享;適合不可變或單層結構。
- 深拷貝 會遞迴複製所有子物件,產生完全獨立的資料結構;適用於多層嵌套或可變物件。
- 使用
list.copy()、切片[:]、list()可快速取得淺拷貝;copy.deepcopy()才是深拷貝的標準做法。 - 在實務開發中,先評估資料結構的深度與可變性,再選擇最合適的拷貝方式,才能兼顧效能與正確性。
掌握了這些概念與技巧,你就能在 Python 程式中安全地操作清單,避免常見的「不小心改到別人的資料」的坑洞,寫出更穩定、更易於維護的程式碼。祝你在資料結構的旅程中玩得開心! 🎉