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