22 KiB
sources, brief
| sources | brief | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
Python 可变对象可原地修改;赋值和传参只共享引用,不会自动复制对象。 |
Python 可变对象
本页边界
本页专注列表、字典、集合等对象的原地修改和共享引用副作用。变量重新赋值的基础机制见 concepts/变量绑定;函数调用中的影响见 concepts/Python-参数传递;浅拷贝和深拷贝的复制策略见 concepts/浅拷贝与深拷贝。
Python 可变对象是指对象创建之后,其内部内容仍然可以被修改,而不必创建一个全新的对象。summaries/05_Lists 中介绍的列表是 Python 中最常见、最重要的可变对象之一;summaries/01_Datatypes 说明字典也是典型的可变对象,而元组和字符串则是与之相对的不可变对象;summaries/02_More_functions 强调函数参数传递的是对象引用而不是对象副本;summaries/07_Objects 进一步从 Python 对象模型角度说明:赋值不会复制对象,多个名字或容器元素可能引用同一个可变对象。
理解可变对象,核心不是记住某个方法会不会改变列表,而是理解三件事:
- 变量名只是名字,不是内存位置。
- 赋值、传参、放入容器通常只是复制引用,不复制对象。
- 如果共享的是可变对象,任何一处原地修改都会被所有引用看到。
相关主题包括 concepts/Python-对象模型、concepts/可变性与引用、concepts/Python-拷贝语义、concepts/Python-参数传递、concepts/变量绑定、Python列表、字典、元组。
核心定义
如果一个对象支持“原地修改”,它就是可变对象。所谓原地修改,是指变量仍然引用同一个对象,但对象内部的数据发生了变化。
以列表为例:
names = ['Elwood', 'Jake', 'Curtis']
names[1] = 'Joliet Jake'
names
# ['Elwood', 'Joliet Jake', 'Curtis']
这里没有创建一个新的列表变量,而是直接修改了原列表中索引为 1 的元素。
以字典为例:
s = {
'name': 'GOOG',
'shares': 100,
'price': 490.1
}
s['shares'] = 75
s['date'] = '6/6/2007'
这里 s 仍然引用同一个字典对象,但其中 'shares' 对应的值被修改,并且新增了 'date' 字段。这种通过键直接改变内容的能力,是字典可变性的核心体现。相关内容见 字典 和 Python数据结构。
名字、对象与引用
summaries/07_Objects 用一个关键原则概括 Python 的对象模型:变量是名字,不是内存位置。
a = [1, 2, 3]
这行代码让名字 a 绑定到一个列表对象。之后如果执行:
a.append(4)
列表对象本身被修改,a 仍然指向同一个列表。
但如果执行:
a = [4, 5, 6]
这不是修改原来的列表,而是让名字 a 重新绑定到另一个新列表。旧列表是否还存在,取决于是否还有其他名字引用它。
这一区别与 concepts/变量绑定、concepts/Python-参数传递、concepts/Python-对象模型 密切相关。
赋值不会复制对象
Python 中很多操作本质上都是“赋值”或“存储引用”:
a = value
s[n] = value
s.append(value)
d['key'] = value
summaries/07_Objects 特别强调:这些操作不会复制被赋的值,只是复制对象引用。
例如:
a = [1, 2, 3]
b = a
c = [a, b]
这里实际上只有一个列表对象 [1, 2, 3],但有多个引用指向它:a、b、c[0]、c[1]。
如果修改这个列表:
a.append(999)
那么所有引用都会看到变化:
a
# [1, 2, 3, 999]
b
# [1, 2, 3, 999]
c
# [[1, 2, 3, 999], [1, 2, 3, 999]]
这就是 可变性与引用 中最常见、也最容易出错的情况:以为自己持有的是独立数据,实际却和其他代码共享同一个对象。
修改对象 vs 重新绑定变量
可变对象最容易造成混淆的地方,是“修改对象”和“重新绑定变量名”看起来都像改变,但含义完全不同。
原地修改会影响共享对象
def foo(items):
items.append(42)
a = [1, 2, 3]
foo(a)
print(a)
# [1, 2, 3, 42]
append() 是原地修改。函数内部的 items 和外部的 a 引用同一个列表对象,所以外部的 a 会看到变化。
重新绑定只改变名字指向
def bar(items):
items = [4, 5, 6]
b = [1, 2, 3]
bar(b)
print(b)
# [1, 2, 3]
这里 items = [4, 5, 6] 只是让函数内部的局部变量 items 指向一个新列表。它没有修改 b 原来引用的列表,因此函数外部的 b 不变。
同理:
a = [1, 2, 3]
b = a
a = [4, 5, 6]
print(a) # [4, 5, 6]
print(b) # [1, 2, 3]
重新赋值 a 不会覆盖旧列表对象;它只是让 a 这个名字指向新列表。b 仍然引用旧列表。
对象身份、is 与 ==
理解可变对象时,经常需要区分“是不是同一个对象”和“内容是否相等”。
is 用于判断两个名字是否引用同一个对象:
a = [1, 2, 3]
b = a
a is b
# True
对象身份可以用 id() 查看:
id(a)
id(b)
如果两个名字引用同一个对象,它们的 id() 相同。
但通常比较值时应该使用 ==:
a = [1, 2, 3]
b = a
c = [1, 2, 3]
a is b # True
a is c # False
a == c # True
a 和 c 内容相同,但它们是两个不同的列表对象。对可变对象来说,这个区别非常重要:如果 a is c 为 False,修改 a 通常不会影响 c;如果 a is b 为 True,修改其中一个名字引用的对象,另一个名字会看到同样的变化。
列表作为可变对象
在 summaries/05_Lists 中,列表被介绍为 Python 保存有序值集合的主要数据类型。列表的很多操作都体现了它的可变性。
修改单个元素
列表元素可以通过索引直接重新赋值:
symlist[2] = 'AIG'
这会改变列表中指定位置的值。
添加元素
列表可以使用 append() 在末尾添加元素:
symlist.append('RHT')
也可以使用 insert() 在指定位置插入元素:
symlist.insert(1, 'AA')
这些操作都会直接改变原列表。
删除元素
列表支持按值删除或按索引删除:
symlist.remove('MSFT')
del symlist[1]
删除后,列表不会留下空洞,后面的元素会自动向前移动。
切片赋值
列表还可以通过切片一次性替换一部分内容:
symlist[-2:] = ['GOOG']
切片赋值会根据右侧列表的长度自动调整左侧列表的大小。这是列表可变性的一个重要体现。
字典作为可变对象
summaries/01_Datatypes 中介绍的字典是一种键到值的映射,也叫哈希表或关联数组。字典通过键访问和修改数据:
d = {
'name': 'AA',
'shares': 100,
'price': 32.2
}
cost = d['shares'] * d['price']
与元组中使用数字索引不同,字典使用具名键:
d['price']
这通常比下面这种基于位置的访问更清晰:
t[2]
修改、添加与删除字段
字典可以自由修改已有键对应的值:
d['shares'] = 75
也可以添加新键值对:
d['date'] = (6, 11, 2007)
d['account'] = 12345
还可以删除键值对:
del d['account']
这些操作都体现了字典的可变性:字典对象本身没有被替换,但其内部映射关系发生了变化。
字典适合可变记录
字典常用于表示字段较多、字段名称重要、并且可能随程序运行而变化的数据记录。例如股票持仓可以表示为:
d = {
'name': 'AA',
'shares': 75,
'price': 32.2,
'date': (6, 11, 2007)
}
这种结构比元组更容易读懂,也更方便逐步添加属性。因此,在需要频繁修改或扩展字段时,字典通常比元组更合适。
函数参数传递中的可变对象
summaries/02_More_functions 强调:调用函数时,参数变量只是引用传入对象的名字,函数不会自动获得输入对象的副本。summaries/07_Objects 从赋值语义角度说明了同一个事实:赋值和传参都是引用共享。
如果把列表、字典等可变对象传入函数,函数内部可以原地修改该对象,调用者会看到这种变化:
def foo(items):
items.append(42)
a = [1, 2, 3]
foo(a)
print(a)
# [1, 2, 3, 42]
这对程序设计非常重要:
- 如果函数只是读取数据,通常不应修改传入的可变对象。
- 如果函数会修改传入对象,应在函数名、文档或调用方式中明确表达。
- 如果不希望影响原对象,应显式创建副本。
相关主题:Python函数设计、Python参数传递、函数抽象。
原地操作与新对象操作
理解可变对象时,一个关键点是区分“修改原对象”和“创建新对象”。
原地排序:sort()
列表的 sort() 方法会直接修改原列表:
symlist.sort()
排序后,symlist 本身的顺序发生变化,没有生成新的列表。这类操作称为原地操作。
创建新列表:sorted()
如果希望保留原列表不变,可以使用 sorted():
t = sorted(symlist)
这里 symlist 保持不变,排序后的结果保存在新列表 t 中。
这一区别与 Python列表、Python序列 密切相关。
浅拷贝与深拷贝
因为赋值不会复制对象,所以如果需要独立数据,必须显式拷贝。summaries/07_Objects 特别区分了浅拷贝和深拷贝,这是理解嵌套可变对象的关键。
浅拷贝
列表和字典可以创建浅拷贝。例如:
a = [2, 3, [100, 101], 4]
b = list(a)
a is b
# False
a 和 b 是两个不同的外层列表,但它们的内部元素仍然可能共享:
a[2].append(102)
b[2]
# [100, 101, 102]
a[2] is b[2]
# True
这里外层列表被复制了,但内部列表 [100, 101] 没有被复制。两个外层列表仍然引用同一个内部列表。这就是浅拷贝。
深拷贝
如果需要复制对象以及它包含的所有嵌套对象,可以使用 copy.deepcopy():
import copy
a = [2, 3, [100, 101], 4]
b = copy.deepcopy(a)
a[2].append(102)
b[2]
# [100, 101]
a[2] is b[2]
# False
深拷贝适用于需要完全隔离嵌套可变结构的场景。相关主题见 拷贝语义。
可变对象与不可变对象的对比
可变对象的关键特征是“内容可改”。这与不可变对象相对。
summaries/01_Datatypes 中的元组就是典型的不可变对象:
t = ('AA', 100, 32.2)
t[1] = 75
# TypeError: 'tuple' object does not support item assignment
如果想“改变”元组中的股数,不能原地修改,只能创建一个新元组并重新绑定变量名:
t = (t[0], 75, t[2])
这看起来像修改了 t,但实际发生的是:旧元组保持不变,变量 t 被重新绑定到一个新元组。
这与字典形成鲜明对比:
d['shares'] = 75
这里字典本身被原地修改。
字符串通常也被视为不可变对象。可以通过字符串方法生成新字符串或通过 split() 得到列表,但不能像列表那样直接修改字符串中的某个字符。
summaries/07_Objects 还指出,基础类型如 int、float、str 的不可变性有助于避免共享引用带来的意外污染:不可变对象即使被多个变量共享,也不会被某个引用原地改坏。
相关主题包括:
- Python字符串
- Python字符串处理
- Python序列
- Python列表
- 元组
- 字典
可变对象与局部变量
函数内部的变量通常是局部变量。局部变量名在函数调用结束后不可访问,但这并不意味着函数内部对可变对象的修改会自动撤销。
例如:
def add_symbol(symbols):
symbols.append('IBM')
portfolio_symbols = ['AA', 'MSFT']
add_symbol(portfolio_symbols)
函数调用结束后,局部变量 symbols 消失,但它曾经引用的列表对象仍然存在,并且已经被修改。外部变量 portfolio_symbols 也引用同一个列表,所以会看到变化。
这说明“局部变量不可访问”和“对象未被修改”是两回事。局部作用域限制的是名字的可见性,不是对象本身的可变性。相关内容见 Python作用域。
可变对象与全局状态
可变对象还容易与全局状态产生联系。函数可以读取全局变量,如果全局变量引用的是可变对象,即使不使用 global,函数也可能通过原地操作修改该对象:
items = []
def add_item(x):
items.append(x)
这里没有对 items 重新赋值,而是调用列表的 append() 方法修改列表对象本身。因此这种写法可能改变全局状态。
如果函数内部写的是:
def reset_items():
items = []
这只是创建了一个局部变量 items,不会修改全局变量。若要重新绑定全局变量,需要 global 声明,但 summaries/02_More_functions 建议尽量避免 global,更好的设计通常是把状态封装到类或显式传递的数据结构中。
相关主题:全局变量、状态管理。
可变对象与索引结构
因为列表是有序集合,所以它既具有序列特征,也具有可变特征:
- 可以通过整数索引访问元素。
- 可以使用负索引从末尾访问元素。
- 可以使用切片提取子列表。
- 可以通过索引或切片修改内容。
例如:
symlist[0]
symlist[-1]
symlist[0:3]
symlist[-2:] = ['GOOG']
其中访问操作不改变列表,而赋值操作会改变列表。
字典不是通过整数位置访问,而是通过键访问:
d['name']
d['shares']
因此,列表的可变性主要体现为“按位置修改有序集合”,字典的可变性主要体现为“按键修改映射关系”。
可变对象与重复值
列表允许包含重复值:
symlist.append('YHOO')
可以使用 count() 统计某个值出现的次数:
symlist.count('YHOO')
但使用 remove() 删除时,只会删除第一个匹配项:
symlist.remove('YHOO')
这说明对可变对象进行修改时,需要清楚方法的具体行为,尤其是在存在重复值的情况下。
字典则不允许同一个键同时对应多个值。对已有键赋值会覆盖旧值:
d['shares'] = 100
d['shares'] = 75
最终 'shares' 只会对应 75。这也是字典作为键值映射结构的重要特征。
可变对象与嵌套结构
列表可以包含任意类型的对象,包括其他列表:
nums = [101, 102, 103]
items = ['spam', symlist, nums]
可以通过多重索引访问嵌套列表中的元素:
items[1][1]
items[2][1]
字典也可以包含复杂对象,例如把日期表示为一个元组:
d['date'] = (6, 11, 2007)
这里字典本身是可变的,可以添加或删除 'date' 这个键;但作为值保存的日期元组是不可变的,不能原地修改其内部元素。
嵌套结构的风险在于:外层对象和内层对象可能分别被共享。例如浅拷贝列表后,外层列表不同,但内层列表仍可能相同。复杂嵌套结构可能导致代码难以理解。summaries/05_Lists 建议通常应让列表保持简单,最好让一个列表保存同一种类型的值。
字典视图与可变性
字典的可变性还体现在 keys() 和 items() 返回的视图对象上。
keys = d.keys()
keys 不是一份静态拷贝,而是原字典键集合的动态视图。如果字典发生变化,视图会反映最新状态:
del d['account']
keys
# dict_keys(['name', 'shares', 'price', 'date'])
类似地,items() 返回键值对视图:
for k, v in d.items():
print(k, '=', v)
每个键值对可以看作一个 (key, value) 元组,因此这里也结合了 元组 的解包机制。
这说明在处理可变字典时,需要注意:某些对象看起来像“结果”,但实际上仍然与原字典保持动态关联。
与 CSV 数据处理的关系
在 summaries/01_Datatypes 的练习中,从 portfolio.csv 读取出的原始行是字符串列表:
row = ['AA', '100', '32.20']
直接计算会失败,因为数字字段仍是字符串:
cost = row[1] * row[2]
# TypeError
一种做法是转换为元组:
t = (row[0], int(row[1]), float(row[2]))
这种结构适合固定字段的简单记录,但不可变。
另一种做法是转换为字典:
d = {
'name': row[0],
'shares': int(row[1]),
'price': float(row[2])
}
这种结构不仅能用于计算:
cost = d['shares'] * d['price']
还方便后续修改和扩展:
d['shares'] = 75
d['date'] = (6, 11, 2007)
在 summaries/02_More_functions 的 parse_csv() 练习中,CSV 文件可以被解析为字典列表:
portfolio = parse_csv('Data/portfolio.csv', types=[str, int, float])
summaries/07_Objects 进一步展示了为什么可以把 str、int、float 放入列表:Python 中函数和类型也是对象,属于 一等对象。因此可以写出通用转换逻辑:
types = [str, int, float]
converted = [func(val) for func, val in zip(types, row)]
record = dict(zip(headers, converted))
这种结果本身包含多层可变结构:外层是列表,内部每条记录是字典。外层列表可以追加、删除或重新排序记录;内层字典可以修改某条记录的字段值。因此,在 CSV数据处理 中,选择元组、字典或字典列表,不仅是语法选择,也是关于数据是否需要可变、字段是否需要具名访问、函数是否可能修改输入数据的设计选择。
类型与可变性
Python 中变量名本身没有类型,类型属于对象值。可以用 type() 查看对象类型:
a = 42
b = 'Hello World'
type(a) # int
type(b) # str
也可以用 isinstance() 做类型检查:
if isinstance(a, list):
print('a is a list')
或检查多个可能类型:
if isinstance(a, (list, tuple)):
print('a is a list or tuple')
不过 summaries/07_Objects 提醒,不应过度类型检查。可变对象的正确使用通常更依赖清晰的接口约定:函数是否会修改输入?调用者是否需要传入可变容器?返回值是否共享内部状态?这些问题往往比单纯判断类型更重要。
实践意义
理解 Python 可变对象非常重要,因为它影响程序的行为和设计方式:
-
原地修改会改变原数据
使用append()、insert()、remove()、sort()、字典赋值、del等操作时,原对象会直接变化。 -
赋值不会复制对象
b = a不会创建新列表,只是让b和a引用同一个对象。 -
函数不会自动复制参数
把列表或字典传入函数后,函数内部的原地修改会影响调用者持有的对象。 -
重新赋值不同于修改对象
items = [4, 5, 6]只是重新绑定局部名字;items.append(42)才是修改原列表。 -
is和==含义不同
is比较对象身份,==比较对象内容。对共享可变对象进行调试时,这个区别尤其重要。 -
浅拷贝可能仍然共享内部对象
list(a)只复制外层列表;嵌套列表等内部对象仍可能共享。 -
深拷贝用于隔离嵌套可变结构
copy.deepcopy()可以递归复制内部对象,但也可能带来额外开销和复杂性。 -
有些操作返回新对象
例如sorted()会创建新列表;修改元组时也必须创建新元组,而不是原地改变。 -
列表和字典适合不同的可变场景
列表适合按顺序维护一组元素;字典适合按名称维护一组字段或属性。 -
复杂嵌套结构需要谨慎使用
一个容器可能是可变的,但其中的元素可能是不可变的;也可能多个层级都可变,导致修改影响更难追踪。 -
可变对象适合逐步构建数据
例如先创建空列表,再不断append()新元素:mysyms = [] mysyms.append('GOOG')或者先创建基础字典,再逐步添加字段:
d = {} d['name'] = 'AA' d['shares'] = 100 d['price'] = 32.2
小结
Python 可变对象是可以在创建后继续修改内部内容的对象。列表是最典型的例子,它支持元素重新赋值、追加、插入、删除、切片赋值和原地排序;字典同样是典型可变对象,它支持通过键修改、添加和删除值。与之相对,元组、字符串以及数字等对象不能原地修改,若要改变内容,必须创建新对象并重新绑定名字。
从对象模型角度看,理解可变对象的关键是:变量只是名字,赋值不会复制对象,传参也不会自动复制对象。多个名字可能引用同一个可变对象,因此任何原地修改都会被所有引用看到。需要独立数据时,应显式创建浅拷贝或深拷贝,并注意嵌套对象是否仍被共享。
掌握这一点,有助于理解 summaries/05_Lists 中列表操作的本质,也有助于吸收 summaries/01_Datatypes 中关于元组、字典和数据建模的区别,并为 summaries/02_More_functions 中的函数设计和 CSV 解析练习,以及 summaries/07_Objects 中的 Python 对象模型、身份比较和拷贝语义打下基础。
See also: summaries/00_Overview
See also: summaries/02_Containers
See also: summaries/04_Sequences
See also: summaries/02_More_functions
See also: summaries/01_Class
See also: summaries/01_Dicts_revisited
See also: summaries/07_Objects