Python中的變量、賦值及函數的參數傳遞概要
python中的變量、賦值
python中的變量不是盒子。
python中的變量無法用“變量是盒子”做解釋。圖說明了在 Python 中為什么不能使用盒子比喻,而便利貼則指出了變量的正確工作方式。
如果把變量想象為盒子,那么無法解釋 Python 中的賦值;應該把變量視作便利貼,這樣示例中的行為就好解釋了
注意:
對引用式變量來說,說把變量分配給對象更合理,反過來說就有問題。畢竟,對象在賦值之前就創建了。
上面這一段是《流暢的Python》說法。
在 Python 中,一切皆為對象。對象分為不可變對象和可變對象。每個對象都有各自的 id、type 和 value。
Id:當一個對象被創建后,在對象生命周期內它的 id 就不會在改變,在 CPython 實現中,id 通常等于對象的內存地址,可以使用 id() 去查看對象在內存中地址。不可對象(如元組)可能包含可變子對象,子對象的修改不會影響容器的 id。
Type:對象類型(如 int, str, list),決定了支持的操作,通過 type() 獲取。
Value:對象存儲的具體數據。
可變性的本質
??? 可變性取決于對象是否允許 原地修改(即不改變 id 的情況下修改 value)。
??? 不可變對象看似“修改”時,實際上是創建新對象并重新賦值給變量。(不可變對象的“不可變”是指 value 不可原地修改,而非完全不可變。任何對不可變對象的“修改”都會生成新對象。)
# 不可變對象示例(整數)
a = 10
print(id(a)) # 輸出初始 id
a += 5
print(id(a)) # id 改變,因為創建了新對象# 可變對象示例(列表)
b = [1, 2]
print(id(b)) # 輸出初始 id
b.append(3)
print(id(b)) # id 不變,原地修改
學過C/C++可知,python與C/C++不一樣,它的變量使用有自己的特點。下面,就進行必要的展開介紹。
在Python中,變量賦值的機制涉及到對象的引用和對象的可變性。變量名只是對象的引用,而不是直接存儲數據。因此,變量a和b賦值后的行為差異,取決于兩個因素:
在Python中,數據對象被明確劃分為兩大陣營:可變(Mutable)與不可變(Immutable)。
不可變數據類型
??? 數字類型(int/float/complex)
??? 字符串(str)
??? 元組(tuple)
??? 不可變集合/凍結集合(frozenset)
??? 布爾值(bool)
不可變數據類型的核心特性
1.內存機制:當嘗試修改不可變對象時,Python不會改變原對象,而是創建一個新對象。例如:
a = 10
b = a
a += 5 # a指向新對象15,不可變對象的重新賦值,無法原地修改,必須生成新對象
print(id(a), id(b)) # 輸出不同內存地址
print(id(a) == id(b)) # 輸出 False
2. 內存效率優化
對象復用:Python對部分不可變對象(如小整數范圍-5到256、部分短字符串)會進行緩存優化,相同值的對象可能復用同一內存地址。
垃圾回收:不可變對象更易被識別為垃圾,提升回收效率。
3. 哈希性能提升
快速查找:不可變對象的哈希值在創建時確定且不可變,適合作為字典鍵。字典查找時間復雜度O(1)。
安全保障:哈希值穩定性防止字典鍵沖突導致的邏輯錯誤。
4. 線程安全保證
不可變對象天然線程安全,因為無法被修改;可變對象在多線程環境中需使用鎖(如 threading.Lock)來保證數據一致性。
不可變 vs 可變
特性 | 不可變類型 | 可變類型 |
內存占用 | 低(注:不可變類型的低內存占用僅適用于可復用的對象如小整數,大對象可能仍會獨立存儲) | 高(獨立副本) |
修改成本 | 高(需創建新對象) | 低(原地修改) |
線程安全 | 是 | 否(需加鎖) |
哈希支持 | 是 | 否 |
適用場景 | 字典鍵、線程共享數據 | 頻繁修改的數據集合 |
☆不可變對象的值一旦創建,就不能修改其內容
1) 修改變量的新賦值
a = 5 # a指向整數對象5
b = a # b也指向同一個整數對象5
a = 6 # 此時a指向新的整數對象6,但b仍指向5
print(b) # 輸出5
解釋原因:
賦值操作 b = a 時,僅復制引用,使得 a 和 b 都指向同一個對象(初始是5)。
當 a = 6 時,a 的引用被更新為指向新對象6,而 b 仍然指向原對象
2)不可變對象無法被修改內容,只能重新創建新對象,嘗試直接修改對象內容(無效)
a = "hello" # a指向字符串"hello"
b = a # b也指向相同的字符串對象
# 嘗試修改字符串內容(但不可變對象不允許此操作)
a[0] = "H" # 報錯:'str' object does not support item assignment
☆可變對象的值可以在創建后被修改
1) 直接修改對象內容
a = [1, 2] # a指向列表[1, 2]
b = a # b指向同一個列表對象
a.append(3) # 修改列表內容,原列表變為[1, 2, 3]
print(b) # 輸出[1, 2, 3]
解釋原因:
a 和 b 指向同一個列表對象。
append() 方法直接修改了該對象的內部內容,因此兩個變量看到的都是同一個被修改后的對象。
可變對象(如列表)在修改時可能觸發動態擴容,導致內存地址變化(如 a = a + [3] 會創建新列表,內存地址改變,而 a.append(3) 是原地修改,內存地址不變)
2) 修改變量的引用(不影響對方)
a = [1, 2]
b = a
a = [3, 4] # a的引用被更新為新列表[3,4],但b仍指向原列表[1,2]
print(b) # 輸出[1, 2]
解釋原因:
a = [3,4] 是一個新的賦值操作,直接改變了a的引用,使其指向新對象,而b未被修改,依然指向原列表。
python函數的參數傳遞
python函數的參數傳遞,也有自己的特點。當傳過來的是可變類型(list、dict)時,我們在函數內部修改就會影響函數外部的變量。而傳入的是不可變類型時在函數內部修改改變量并不會影響函數外部的變量,因為修改的時候會先復制一份再修改。
Python的參數傳遞,有人建議表述為:Python的參數傳遞是按對象引用傳遞(pass by object reference),即函數接收的是對象的引用副本,而非對象本身的副本。
官方術語,參數傳遞使用按值調用(call by value)的方式(其中的值始終是對象的引用,而不是對象的值)。實際上,Python函數參數傳遞的始終對象的引用,而不是對象的值。這方面的內容,因一些人和資料介紹的比較混亂,特地附注。
【附注(有關官方文檔節選——同時給出python官方的英文和中文兩種說法節選和鏈接——供參考):
The actual parameters (arguments) to a function call are introduced in the local symbol table of the called function when it is called; thus, arguments are passed using call by value (where the value is always an object reference, not the value of the object). [1] When a function calls another function, or calls itself recursively, a new local symbol table is created for that call.
https://docs.python.org/3/tutorial/controlflow.html#defining-functions
在調用函數時會將實際參數(實參)引入到被調用函數的局部符號表中;因此,實參是使用 按值調用 來傳遞的(其中的 值 始終是對象的 引用 而不是對象的值)。 [1] 當一個函數調用另外一個函數時,會為該調用創建一個新的局部符號表。
https://docs.python.org/zh-cn/3/tutorial/controlflow.html#defining-functions
Remember that arguments are passed by assignment in Python. Since assignment just creates references to objects, there’s no alias between an argument name in the caller and callee, and so no call-by-reference per se.
https://docs.python.org/3/faq/programming.html#how-do-i-write-a-function-with-output-parameters-call-by-reference
請記住,Python 中的實參是通過賦值傳遞的。由于賦值只是創建了對象的引用,所以調用方和被調用方的參數名都不存在別名,本質上也就不存在按引用調用的方式。
https://docs.python.org/zh-cn/3/faq/programming.html#how-do-i-write-a-function-with-output-parameters-call-by-reference 】
這種機制,也有人稱為按對象引用調用(call by object reference),但機制本質不變。
對于不可變對象(如整數、字符串、元組),因為其不可變性,函數內對參數的任何修改不會影響到外部變量。對于可變對象(如列表、字典、集合),函數內對參數的修改會影響到外部變量。
如果傳入的參數是不可變類型(如數字、字符串、元組),那么在函數體內修改參數的值,并不會影響到原來的變量。因為不可變類型的變量實際上是值的引用,當試圖改變變量的值時,相當于是在創建新的對象。例如:
def change_number(num):num = 100x = 10
change_number(x)
print(x) # 輸出:10
在上面的例子中,盡管在函數內部num的值被改變了,但是原變量x的值并沒有改變。參見下圖:
如果傳入的參數是可變類型(如列表、字典),那么在函數體內修改參數的值,會影響到原來的變量。因為可變類型的變量存儲的是一個地址,當試圖改變變量的值時,實際上是在改變這個地址所指向的內容。例如:
def change_list(lst):lst.append(100)x = [1, 2, 3]
change_list(x)
print(x) # 輸出:[1, 2, 3, 100]
在上面的例子中,函數內部對參數lst的修改影響到了原變量x的值。參見下圖:
淺拷貝與深拷貝在函數參數傳遞中的作用
函數參數傳遞使用可變對象時,可能涉及淺拷貝問題(如傳入列表時,函數內修改會影響原對象),并建議使用 copy.deepcopy() 避免副作用。
- 淺拷貝:copy.copy()或切片操作(如lst[:]),僅復制頂層對象,嵌套對象仍共享引用。
- 深拷貝:copy.deepcopy(),遞歸復制所有嵌套對象,生成完全獨立的副本。
- 選擇依據:根據數據結構復雜度和是否需要隔離修改來決定。在涉及嵌套可變對象時,優先使用深拷貝。
通過合理使用copy.deepcopy(),可以確保函數對參數的修改不會意外影響外部數據。
假設有一個函數接收一個列表參數,并在內部修改該列表。由于Python的參數傳遞是對象引用傳遞,直接修改會影響原對象:
def modify_list(lst):lst.append(4) # 原地修改列表print("函數內列表:", lst)original_list = [1, 2, 3]
modify_list(original_list)
print("函數外列表:", original_list)
輸出:
函數內列表: [1, 2, 3, 4]
函數外列表: [1, 2, 3, 4] ?# 原列表被修改
如果希望避免修改原列表,可以使用淺拷貝(copy.copy()或切片操作),但對于嵌套的可變對象,淺拷貝可能不夠:
import copydef modify_list_shallow(lst):lst_copy = copy.copy(lst) # 淺拷貝lst_copy.append(4)print("函數內列表:", lst_copy)original_list = [1, 2, 3]
modify_list_shallow(original_list)
print("函數外列表:", original_list)
輸出:
函數內列表: [1, 2, 3, 4]
函數外列表: [1, 2, 3] ?# 原列表未被修改
但當列表包含其他可變對象時,淺拷貝仍可能影響原對象:
def modify_nested_list_shallow(lst):lst_copy = copy.copy(lst) # 淺拷貝lst_copy[0].append(100) # 修改嵌套列表print("函數內列表:", lst_copy)original_list = [[1, 2], [3, 4]]
modify_nested_list_shallow(original_list)
print("函數外列表:", original_list)
輸出:
函數內列表: [[1, 2, 100], [3, 4]]
函數外列表: [[1, 2, 100], [3, 4]] ?# 原嵌套列表被修改
使用深拷貝copy.deepcopy()創建完全獨立的副本,即使對象包含嵌套的可變對象:
import copydef modify_list_deep(lst):lst_copy = copy.deepcopy(lst) # 深拷貝lst_copy[0].append(100) # 修改嵌套列表print("函數內列表:", lst_copy)original_list = [[1, 2], [3, 4]]
modify_list_deep(original_list)
print("函數外列表:", original_list)
輸出:
函數內列表: [[1, 2, 100], [3, 4]]
函數外列表: [[1, 2], [3, 4]] ?# 原列表未被修改
何時使用深拷貝
- 需要完全獨立的副本:當函數需要修改傳入的可變對象,但又不希望影響外部變量時。
- 嵌套可變對象:當對象包含多層嵌套的可變結構(如列表的列表、字典的字典)時,必須使用深拷貝。
- 避免副作用:在并發編程或需要保持數據隔離的場景中,深拷貝能有效防止意外修改。
OK!?