2.0. 對象的類型:可變 (Mutable) 與不可變 (Immutable)?
在Python中,理解對象的可變性 (mutability) 是至關重要的,它影響著變量如何被修改、函數參數如何傳遞以及數據結構的行為。
-
不可變對象 (Immutable Objects):
-
大白話定義:“不可變對象就像一塊已經刻好字的石頭。一旦石頭上的字刻好了(對象創建并賦值后),這些字(對象的狀態或內容)就再也不能改變了。如果你想改變這些字,你實際上是得到了一塊全新的、刻著新字的石頭,原來的石頭并沒有變。”
-
技術定義:一個對象在創建之后,其內部狀態(值)不能被修改。任何看起來像是修改不可變對象的操作,實際上都會創建一個新的對象。
-
常見的不可變類型:
- 數字類型: int?, float?, bool?, complex?
- ?str? (字符串)
- ?tuple? (元組)
- ?frozenset? (不可變集合)
-
示例:Python
x = 10 print(f"x = {x}, id(x) = {id(x)}") # x = 10, id(x) = ... x = x + 5 # 看起來像是修改了x print(f"x = {x}, id(x) = {id(x)}") # x = 15, id(x) = ... (id變了,是新對象)s = "hello" print(f"s = '{s}', id(s) = {id(s)}") # s = 'hello', id(s) = ... s = s + " world" # 字符串拼接 print(f"s = '{s}', id(s) = {id(s)}") # s = 'hello world', id(s) = ... (id變了,是新字符串對象)my_tuple = (1, 2, 3) # my_tuple[0] = 10 # 這會引發 TypeError: 'tuple' object does not support item assignment
-
-
可變對象 (Mutable Objects):
-
大白話定義:“可變對象就像一塊可以隨時擦寫的白板。你可以在白板上寫字、擦掉字、修改字(對象的狀態或內容可以被改變),而白板本身還是那塊白板(對象的身份標識不變)。”
-
技術定義:一個對象在創建之后,其內部狀態(值)可以被修改,而對象的身份標識 (id) 保持不變。
-
常見的可變類型:
- ?list? (列表)
- ?dict? (字典)
- ?set? (集合)
- ?bytearray? (字節數組)
- 大多數用戶自定義的類實例 (默認情況下)
-
示例:Python
my_list = [1, 2, 3] print(f"my_list = {my_list}, id(my_list) = {id(my_list)}") # my_list = [1, 2, 3], id = ... my_list.append(4) # 原地修改列表 print(f"my_list = {my_list}, id(my_list) = {id(my_list)}") # my_list = [1, 2, 3, 4], id = ... (id沒變)my_dict = {'name': 'Alice'} print(f"my_dict = {my_dict}, id(my_dict) = {id(my_dict)}") my_dict['age'] = 30 # 原地修改字典 print(f"my_dict = {my_dict}, id(my_dict) = {id(my_dict)}") # id沒變
-
-
為什么區分可變與不可變很重要?
- 函數參數傳遞: 當你將一個可變對象傳遞給函數時,如果在函數內部修改了這個對象,那么這個修改會反映到原始對象上(因為函數操作的是同一個對象)。如果傳遞的是不可變對象,函數內部的“修改”實際上是創建了一個新的局部對象,不會影響原始對象。
- 默認參數值: 函數的默認參數值只在函數定義時計算一次。如果默認值是一個可變對象(如列表),并且在函數調用中修改了這個可變對象,那么后續不傳遞該參數的調用將會共享這個被修改過的對象,這通常不是期望的行為(詳見IV.2 函數默認參數值部分)。
- 字典的鍵: 字典的鍵必須是不可變類型(或更準確地說,是可哈希的,而所有不可變類型都是可哈希的)。這是因為字典需要依賴鍵的哈希值在其生命周期內保持不變來進行高效查找。
- 數據共享與副作用: 操作可變對象時需要注意可能產生的副作用,尤其是在多處代碼引用同一個可變對象時。
- 拷貝行為: 理解可變性是理解淺拷貝與深拷貝差異的前提(詳見VIII. 數據拷貝的深與淺)。
- 性能: 有時,不可變對象因為其狀態固定,可能在某些操作(如作為字典鍵)或并發場景下有優勢。
-
基本類型速覽
-
?int? (整數) :
- Python中整數類型可以表示任意大小的整數,其精度僅受限于可用內存。
- vs. Java: Java有固定大小的整數類型 (byte?, short?, int?, long?),超出范圍會溢出或需要使用 BigInteger? 類。 Python int? 自動處理大數,無需擔心。
-
?float?: 雙精度浮點數 (似Java double?)。
-
?bool?: True? / False? (首字母大寫 vs. Java true?/false?)。
-
?str? (字符串) :
- 用于表示文本數據,由一個或多個字符組成的序列。
- 定義: 可以用單引號 ''?、雙引號 ""? 或三引號 ''''''?/""""""? 包裹。 單雙引號等效。 三引號用于定義多行字符串,會保留字符串中的換行和空格。
- 不可變性 (Immutable) : 與Java的 String? 對象一樣,Python的字符串也是不可變的。 任何看起來像是修改字符串的操作(如拼接、替換)實際上都會創建一個新的字符串對象。
- 無獨立 char? 類型: Python沒有單獨的字符類型,單個字符也被視為長度為1的字符串。
- vs. Java: Java使用雙引號定義字符串,單引號定義字符。 Python的引號使用更靈活,三引號對多行文本非常方便。
-
?None?: 特殊空值對象 (似Java null?)。
-
-
核心容器對比
-
?list? (列表)
-
Python定義與特性:
- ?my_list = [1, "a", True]? 或 my_list = list()?/empty_list = []
- 有序序列,元素可變,允許重復,可存儲混合類型。
-
Java近似等價物:
- ?java.util.ArrayList? (泛型化后通常類型統一)。
-
關鍵Pythonic操作/特性 (Java對比) :
-
索引/切片: my_list[i]?, my_list[start:stop:step]? (比Java get(i)? 和 subList()? 更強大和靈活)。
-
修改:
- ?append(element)?: 末尾添加 (類似Java add(element)?)。
- ?extend(iterable)?: 末尾逐個添加可迭代對象中的元素 (類似Java addAll(collection)?)。
- ?insert(index, element)?: 指定位置插入 (類似Java add(index, element)?)。
- ?pop(index?)?: 移除并返回指定索引元素 (默認最后一個) (類似Java remove(index)?)。
- ?remove(value)?: 刪除第一個匹配的值 (Java remove(Object)? 返回布爾值)。
- ?sort(key=None, reverse=False)?: 原地排序。 key?參數接收一個函數,該函數作用于列表中的每個元素,排序將基于這些函數的返回值進行。 這類似Java中的 List.sort(Comparator)?,但Python的 key? 參數常與 lambda? 表達式結合,對于很多場景其定義比Java的 Comparator? 更為簡潔直觀。例如:my_list.sort(key=lambda x: x.age)? 按對象的 age? 屬性排序。
-
推導式: new_list = [expression for item in iterable if condition]? (比Java Stream API更簡潔)。
-
-
-
?tuple? (元組) 固定序列
-
Python定義與特性:
- ?my_tuple = (1, "a", True)? 或 my_tuple = tuple()?/empty_tuple = ()? 或 my_tuple = 1, "a"? (逗號創建)。
- 有序序列,元素不可變,允許重復,可存儲混合類型。
- 特別注意: 定義只包含一個元素的元組,必須在元素后加逗號: single_item_tuple = (1,)?。
-
Java近似等價物:
- Java沒有直接對應的內置元組類型。 可視為輕量級的不可變List? (通過Collections.unmodifiableList()?) 或一個固定結構的小對象/數組。
-
關鍵Pythonic操作/特性 (Java對比) :
-
用途:
- 常用于函數返回多個值 (會自動打包成元組,調用時可直接解包 x, y = func()?)。
- 作為字典的鍵 (因為元組及其元素若都不可變,則是可哈希的)。
- 格式化字符串時的參數傳遞。
-
操作:
- 支持索引和切片 (操作方式與列表、字符串相同),返回的是新元組或元素。
- 沒有修改自身內容的方法 (如 append?, remove?, sort? 等,因為不可變)。
- 主要方法有 count(element)? 和 index(element)?。
-
-
-
?dict? (字典) 鍵值對集合
-
Python定義與特性:
- ?my_dict = {'key1': val1, 'key2': val2}? 或 my_dict = dict()?/empty_dict = {}。
- 存儲鍵值對 (key-value pairs)。
- 鍵 (Keys) : 必須是唯一的且不可變類型 (如字符串, 數字, 元組——前提是元組內元素也都不可變)。
- 值 (Values) : 可以是任意類型,且可以重復。
- 順序: Python 3.7+ 版本保證插入順序有序。 CPython 3.6+ 也可能有序(作為實現細節)。 早期版本通常是無序的。
- 可變類型。
-
Java近似等價物:
- ?java.util.HashMap? (通常無序)。
- ?java.util.LinkedHashMap? (保持插入順序)。
-
關鍵Pythonic操作/特性 (Java對比) :
-
訪問/修改:
- ?my_dict[key]? 獲取值 (若key?不存在則拋出 KeyError?)。
- ?my_dict.get(key, default_value=None)? 獲取值 (若key?不存在則返回 default_value?,更安全,不會報錯)。
- ?my_dict[key] = value? (用于新增鍵值對或修改已存在鍵的值)。
- vs. Java: Java Map.get(key)? 找不到返回 null?;Map.put(key, value)? 用于新增/修改。
-
視圖對象 (可迭代) :
- ?my_dict.keys()?: 返回所有鍵。
- ?my_dict.values()?: 返回所有值。
- ?my_dict.items()?: 返回所有 (鍵, 值)? 元組對。 遍歷字典時非常常用 (for k, v in my_dict.items(): ...?)。
- vs. Java: 對應 keySet()?, values()?, entrySet()?。 Python的 items()? 結合解包遍歷更簡潔。
-
推導式: new_dict = {k_expr: v_expr for item in iterable if condition}?。
-
-
-
?set? (集合) 唯一元素容器
-
Python定義與特性:
- ?my_set = {1, "a", True}? 或 my_set = set()? (創建空集合必須用set()?,因為 {}? 創建的是空字典)。
- 無序 (通常,迭代順序不保證,但CPython 3.7+實現上可能有序)。
- 元素唯一 (自動去重) 且必須為不可變類型。
- 集合本身是可變的。
- 主要用途:快速去重 (unique_list = list(set(my_list_with_duplicates))? 注意可能打亂順序)、高效的成員測試 (element in my_set? 通常比列表快)、以及進行數學集合運算 (交集 &? 或 .intersection()?, 并集 |? 或 .union()?, 差集 -? 或 .difference()?, 對稱差集 ^? 或 .symmetric_difference()?)。
-
Java近似等價物:
- ?java.util.HashSet? (無序, 唯一)。
- ?java.util.LinkedHashSet? (插入有序, 唯一)。
-
關鍵Pythonic操作/特性 (Java對比) :
- 修改: add(element)?, remove(element)? (若元素不存在拋KeyError?), discard(element)? (若不存在不報錯), pop()? (移除并返回任意元素), clear()?。
- 推導式: new_set = {expression for item in iterable if condition}?。
-
-
-
字符串 (str? ) 特色操作
-
格式化:
- f-string (Formatted String Literals) (Python 3.6+): name = "Python"; print(f"Hello, {name} {3 + 4}!")?。 強烈推薦,簡潔、可讀、高效。
- ?str.format()? 方法: "Hello, {} {}".format(name, version)?。
- ?%? 操作符 (舊式): "Hello, %s %d" % (name, version)?。
- vs. Java: f-string遠比Java的 String.format()? 或 +? 拼接直觀。
-
常用方法:
- 大小寫: lower()?, upper()?, capitalize()? (首詞首字母大寫), title()? (每詞首字母大寫), swapcase()? (大小寫互換)。
- 判斷類: startswith(prefix, start?, end?)?, endswith(suffix, start?, end?)?, isalpha()? (全字母?), isalnum()? (字母或數字?), isdigit()? (全數字?), isspace()? (全空白?), isupper()? (全大寫?), islower()? (全小寫?)。
- 查找與替換: find(sub, start?, end?)? (查找子串,找不到返-1), index(sub, start?, end?)? (找不到拋ValueError?), rfind()? (從右開始找), rindex()?, replace(old, new, count?)? (替換子串,count?指定最大替換次數)。
- 分割與連接: split(sep=None, maxsplit=-1)? (按sep?分割成列表,默認按空白字符分割,maxsplit?指定最大分割次數), partition(sep)? (按首次出現的sep?分割成三元組(head, sep, tail)?), sep.join(iterable_of_strings)? (用sep?連接字符串序列中的元素)。
- 去除空白: strip(chars?)? (去除首尾指定字符集chars?,默認空白), lstrip(chars?)? (左去除), rstrip(chars?)? (右去除)。
- 對齊: center(width, fillchar=' ')? (居中,fillchar?填充), ljust(width, fillchar=' ')? (左對齊), rjust(width, fillchar=' ')? (右對齊), zfill(width)? (右對齊,左邊用0填充,主要用于數字)。
-
切片 (Slicing) : my_string[start:stop:step]?,極其強大。 my_string[::-1]? 可反轉字符串。
-
-
容器公共操作
-
?len(c)? (vs. Java .size()?, .length()?)。
-
?in? / not in? (成員測試): 適用于所有主要的容器類型(對字典默認檢查鍵)。 Python的 in? / not in? 比Java中針對不同集合類型的 .contains()? / .containsKey()? 等方法更為統一和通用。
-
?+? (合并序列: str?, list?, tuple?),創建并返回一個新的合并后的序列。
-
?*? (重復序列: str?, list?, tuple?),sequence * n? 將序列內容重復n次形成新序列。
- 注意: 對包含可變對象的列表進行 *? 復制時,是淺拷貝,內部的可變對象引用會被復制,修改一個會導致所有副本中對應元素改變。 (詳見VIII. 數據拷貝的深與淺)
-
?enumerate(iterable, start=0)? (同時獲取索引和值)。
-
?zip(iter1, iter2, ...)? (并行迭代多個序列,以最短的為準)。
-
?min(iterable, key=None, default=...)?, max(iterable, key=None, default=...)?: 返回可迭代對象中的最小/最大元素 (元素間需可比較,key?指定比較函數,default?為iterable為空時返回值)。
-
?sum(iterable, start=0)?: 對可迭代對象中的數值元素求和 (從 start? 值開始累加)。
-
?sorted(iterable, key=None, reverse=False)?: 返回一個新的已排序列表,不修改原可迭代對象。 key? 和 reverse? 參數與 list.sort()? 相同。
-
?del container[index_or_key]? 或 del container?: 刪除元素或整個對象。
-
-
切片補充
理解為什么有些Python容器類型支持切片(Slicing)而有些不支持,關鍵在于理解它們各自數據結構的核心特性:是否有序。
核心原因:“序”- 切片 (Slicing) 操作的本質是從一個序列中提取出一段連續的子序列。 這個操作依賴于元素在容器中的固定位置和順序。 你需要能夠告訴Python:“從第X個位置開始,到第Y個位置結束,以Z的步長取元素”。
支持切片的容器類型 (都是序列類型 - Sequences):
- ?list? (列表) : 列表中的元素是按照它們被添加的順序存儲的,每個元素都有一個明確的、固定的索引(位置)。 因此,你可以指定一個索引范圍來進行切片。
- ?tuple? (元組) : 與列表類似,元組也是有序的,元素有固定的索引。 所以也支持切片。
- ?str? (字符串) : 字符串可以看作是字符的有序序列,每個字符都有其位置。 因此,字符串也支持切片。
不支持切片的容器類型 (通常是無序或基于鍵的):
-
?dict? (字典) :
- 核心特性: 字典存儲的是鍵值對(key-value pairs)。 它設計的核心是通過鍵來快速查找、插入和刪除值,而不是通過元素的位置。
- 順序問題: 在Python 3.7之前,字典是無序的(元素的迭代順序不確定)。 從Python 3.7+開始,字典會記住元素的插入順序,但這主要是為了迭代時保持一致性,其根本的訪問方式仍然是基于鍵。
- 為何不支持切片: "從第X個元素切到第Y個元素"這種操作對于基于鍵的結構沒有直觀意義。 字典的“順序”是插入順序,而不是一個可以用來進行范圍訪問的數字索引序列。 你想訪問的是特定的一些鍵,而不是一個位置范圍。
-
?set? (集合) :
- 核心特性: 集合是一個無序的、包含不重復元素的容器。
- 為何不支持切片: 因為集合中的元素沒有固定的順序或索引,所以無法通過位置范圍來“切”出一部分。 你無法說“給我集合中的第1到第3個元素”,因為它們的排列順序是不確定的。 集合的操作主要是成員測試、去重以及數學上的集合運算(交、并、差等)。
總結 (言簡意賅):
-
支持切片: 因為它們是有序序列 (ordered sequences) ,元素有明確的、可預測的數字索引。
- ?list?
- ?tuple?
- ?str?
-
不支持切片: 因為它們要么是無序的 (unordered) ,要么是基于鍵 (key-based) 訪問的,而不是基于數字位置索引。
- ?dict?
- ?set?