Files

22 KiB
Raw Permalink Blame History

sources, brief
sources brief
summaries/07_Objects.md
summaries/01_Dicts_revisited.md
summaries/01_Class.md
summaries/02_More_functions.md
summaries/04_Sequences.md
summaries/02_Containers.md
summaries/01_Datatypes.md
summaries/00_Overview.md
summaries/05_Lists.md
Python 可变对象可原地修改;赋值和传参只共享引用,不会自动复制对象。

Python 可变对象

本页边界

本页专注列表、字典、集合等对象的原地修改和共享引用副作用。变量重新赋值的基础机制见 concepts/变量绑定;函数调用中的影响见 concepts/Python-参数传递;浅拷贝和深拷贝的复制策略见 concepts/浅拷贝与深拷贝。

Python 可变对象是指对象创建之后,其内部内容仍然可以被修改,而不必创建一个全新的对象。summaries/05_Lists 中介绍的列表是 Python 中最常见、最重要的可变对象之一;summaries/01_Datatypes 说明字典也是典型的可变对象,而元组和字符串则是与之相对的不可变对象;summaries/02_More_functions 强调函数参数传递的是对象引用而不是对象副本;summaries/07_Objects 进一步从 Python 对象模型角度说明:赋值不会复制对象,多个名字或容器元素可能引用同一个可变对象。

理解可变对象,核心不是记住某个方法会不会改变列表,而是理解三件事:

  1. 变量名只是名字,不是内存位置。
  2. 赋值、传参、放入容器通常只是复制引用,不复制对象。
  3. 如果共享的是可变对象,任何一处原地修改都会被所有引用看到。

相关主题包括 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 可变对象非常重要,因为它影响程序的行为和设计方式:

  1. 原地修改会改变原数据
    使用 append()、insert()、remove()、sort()、字典赋值、del 等操作时,原对象会直接变化。

  2. 赋值不会复制对象
    b = a 不会创建新列表,只是让 b 和 a 引用同一个对象。

  3. 函数不会自动复制参数
    把列表或字典传入函数后,函数内部的原地修改会影响调用者持有的对象。

  4. 重新赋值不同于修改对象
    items = [4, 5, 6] 只是重新绑定局部名字;items.append(42) 才是修改原列表。

  5. is 和 == 含义不同
    is 比较对象身份,== 比较对象内容。对共享可变对象进行调试时,这个区别尤其重要。

  6. 浅拷贝可能仍然共享内部对象
    list(a) 只复制外层列表;嵌套列表等内部对象仍可能共享。

  7. 深拷贝用于隔离嵌套可变结构
    copy.deepcopy() 可以递归复制内部对象,但也可能带来额外开销和复杂性。

  8. 有些操作返回新对象
    例如 sorted() 会创建新列表;修改元组时也必须创建新元组,而不是原地改变。

  9. 列表和字典适合不同的可变场景
    列表适合按顺序维护一组元素;字典适合按名称维护一组字段或属性。

  10. 复杂嵌套结构需要谨慎使用
    一个容器可能是可变的,但其中的元素可能是不可变的;也可能多个层级都可变,导致修改影响更难追踪。

  11. 可变对象适合逐步构建数据
    例如先创建空列表,再不断 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