Python 的參考傳遞是什麼?不可變與可變對象的差異與活用方法

1. 傳值與傳參考的差異

在 Python 中,將引數傳遞給函式時,可以分為「傳值」與「傳參考」兩種方式。

  • 傳值:將引數的複製值傳給函式,即使在函式內更改該值,也不會影響原本的變數。
  • 傳參考:將變數的參考(記憶體位址)傳給函式,因此在函式內進行的修改也會影響到原本的變數。

在 Python 中,根據物件的特性,這些行為會有所不同。尤其是「傳參考」在處理可變資料型別時會產生明顯影響,因此正確理解這個概念非常重要。

2. Python 中物件的特性

在 Python 裡,所有資料都是物件,根據物件的特性,可以分為不可變(Immutable)可變(Mutable)兩種類型。這個特性會影響當資料作為引數傳遞時的行為。

  • 不可變物件:建立後無法修改的物件。常見例子包括整數(int)、浮點數(float)、字串(str)、元組(tuple)等。由於這些資料型別無法被修改,即使是傳參考,在函式內的操作也不會影響到外部的變數。
  • 可變物件:建立後可以被修改的物件。常見例子包括串列(list)、字典(dict)、集合(set)等。這些型別在函式內被更動時,原本的變數也會同步受到影響,因此特別受到 Python 傳參考行為的影響。

3. Python 中引數傳遞的機制

在 Python 中,函式接收的是物件的參考。因此這種行為有時也被稱為「傳值的參考」。接下來,我們將透過具體的範例說明不可變與可變物件在這種機制下的不同表現。

3.1 當傳入不可變物件時

將不可變物件傳入函式時,如果在函式內試圖賦予新值,會建立新的物件,而原本的物件不會受到影響。

範例:傳遞不可變整數作為引數

def modify_number(num):
    num = num * 2
    print("函式內的數值:", num)

original_num = 5
modify_number(original_num)
print("函式外的數值:", original_num)

輸出結果:

函式內的數值: 10
函式外的數值: 5

說明
在函式 modify_number 中,變數 num 被乘以 2,但這個操作實際上是產生了新的物件。由於整數是不可變物件,原始的 original_num 不會受到任何影響。

3.2 當傳入可變物件時

相對地,若將可變物件傳入函式,該物件的參考會在函式內使用,因此函式內的修改也會反映到原始物件上。

範例:傳遞可變串列作為引數

def add_item(my_list):
    my_list.append("新增的元素")
    print("函式內的串列:", my_list)

original_list = ["元素1", "元素2"]
add_item(original_list)
print("函式外的串列:", original_list)

輸出結果:

函式內的串列: ['元素1', '元素2', '新增的元素']
函式外的串列: ['元素1', '元素2', '新增的元素']

說明
由於串列是可變物件,在 add_item 函式中所進行的修改會直接影響到外部的 original_list。這說明了可變物件在傳參考時,內部與外部會共用同一個物件。

4. 有關傳參考的注意事項與對策

在 Python 中,將可變物件作為引數傳入函式時,若不加注意,可能會產生預期之外的行為。以下介紹一些避免此類問題的實用對策。

4.1 使用物件的複製

如果希望在函式內修改物件但不影響原始資料,可以先複製一份再進行操作。可使用 Python 的 copy 模組來建立淺層複製(copy.copy)或深層複製(copy.deepcopy)。

範例:使用深層複製來避免修改原始物件

import copy

def add_item(my_list):
    my_list_copy = copy.deepcopy(my_list)
    my_list_copy.append("新增的元素")
    print("函式內的串列(複製):", my_list_copy)

original_list = ["元素1", "元素2"]
add_item(original_list)
print("函式外的串列:", original_list)

輸出結果:

函式內的串列(複製): ['元素1', '元素2', '新增的元素']
函式外的串列: ['元素1', '元素2']

說明
透過 deepcopy,函式內會產生一個新的物件,因此不會改動原本的串列。這樣可以讓函式內外的變更彼此獨立,避免不小心更動原始資料。

4.2 預設引數使用 None

在定義函式時,避免使用可變物件作為預設引數。建議使用 None 作為預設值,並在函式內初始化新物件。

範例:使用 None 作為預設引數的安全寫法

def add_item(my_list=None):
    if my_list is None:
        my_list = []
    my_list.append("新元素")
    print("函式內的串列:", my_list)
    return my_list

# 不傳入串列
result = add_item()
print("回傳的串列:", result)

# 傳入既有串列
existing_list = ["元素1", "元素2"]
result = add_item(existing_list)
print("回傳的串列:", result)
print("原始串列:", existing_list)

輸出結果:

函式內的串列: ['新元素']
回傳的串列: ['新元素']
函式內的串列: ['元素1', '元素2', '新元素']
回傳的串列: ['元素1', '元素2', '新元素']
原始串列: ['元素1', '元素2', '新元素']

說明
透過將 None 設為預設引數,可以在函式內判斷是否需要初始化新串列。這樣的寫法能有效避免修改外部傳入的原始物件。

RUNTEQ(ランテック)|超実戦型エンジニア育成スクール

5. 實戰範例:加深對傳參考的理解

讓我們透過更實際的範例來驗證前面學到的知識。

修改字典的鍵與值

字典同樣是可變物件,因此在函式內的操作也會影響到外部的資料。以下範例展示如何在函式中修改字典內容。

def modify_dict(my_dict):
    my_dict["新鍵"] = "新值"
    print("函式內的字典:", my_dict)

original_dict = {"鍵1": "值1", "鍵2": "值2"}
modify_dict(original_dict)
print("函式外的字典:", original_dict)

輸出結果:

函式內的字典: {'鍵1': '值1', '鍵2': '值2', '新鍵': '新值'}
函式外的字典: {'鍵1': '值1', '鍵2': '值2', '新鍵': '新值'}

說明
由於字典是可變的,函式內新增的鍵值對會直接反映到原始的 original_dict 上。這再次說明了可變物件透過傳參考傳遞時的特性。

6. 結語

在 Python 中,「傳參考」的行為會根據物件的特性而有所不同。為了提升程式碼的可靠性,理解這些行為是非常重要的。特別是在處理可變物件時,建議採取如使用複製或預設引數為 None 等對策,來避免預期外的錯誤。

重點整理

  • 傳值與傳參考的差異:在 Python 中,根據物件的特性不同,函式內的操作是否影響原變數也會有所不同。
  • 不可變與可變物件:不可變物件在函式內的變動不會影響外部;而可變物件因為是傳參考,所以會在函式內外共享狀態。
  • 對策方法:為了避免可變物件導致的副作用,可以使用 copy.deepcopy 或將預設引數設為 None,在函式內自行初始化。

希望透過本文,你能更清楚理解 Python 中的傳參考行為,並在實作中寫出更有效率、更少錯誤的程式碼。