Python(九) 基础语法:可变与不可变对象
一、为什么要学习可变与不可变对象
在 Python 中,变量、赋值、列表、字典、字符串这些内容都和“对象”有关。
初学者经常会遇到这样的疑问:
a = [1, 2, 3]
b = a
b.append(4)
print(a)
print(b)
输出:
[1, 2, 3, 4]
[1, 2, 3, 4]
很多学生会问:
我明明修改的是 b,为什么 a 也变了?
再看另一个例子:
a = 10
b = a
b = 20
print(a)
print(b)
输出:
10
20
这一次,修改 b,a 却没有变。
为什么列表会互相影响,而整数不会?
这就涉及 Python 中一个非常重要的基础概念:
可变对象和不可变对象。
学懂这个概念,可以帮助学生理解:
- 变量到底保存的是什么。
- 为什么列表赋值后会互相影响。
- 为什么字符串不能直接修改某个字符。
- 为什么函数里修改列表会影响函数外的数据。
- 为什么不建议使用可变对象作为函数默认参数。
- 什么时候需要复制对象。
二、对象的定义
在 Python 中,几乎所有数据都是对象。
例如:
10
"Python"
[1, 2, 3]
{"name": "小明"}
这些都是对象。
通俗地说:
对象就是 Python 程序中真正存在的数据。
变量不是对象本身,变量只是对象的名字。
例如:
age = 18
可以理解为:
创建了一个整数对象 18,然后把 age 这个名字指向它。
再比如:
students = ["小明", "小红"]
可以理解为:
创建了一个列表对象 ["小明", "小红"],然后把 students 这个名字指向它。
三、变量和对象的关系
很多初学者会把变量想象成“盒子”,数据放在盒子里。
这个比喻在入门时有帮助,但在理解可变对象时容易出问题。
在 Python 中,更准确的比喻是:
对象像物品,变量像贴在物品上的标签。
例如:
a = [1, 2, 3]
可以想象为:
有一个列表对象 [1, 2, 3],a 是贴在它上面的标签。
再写:
b = a
并不是复制出一个新列表,而是:
b 这个标签也贴到了同一个列表对象上。
所以:
a = [1, 2, 3]
b = a
b.append(4)
print(a)
print(b)
输出:
[1, 2, 3, 4]
[1, 2, 3, 4]
因为 a 和 b 指向的是同一个列表对象。
四、可变对象的定义
可变对象指的是:
对象创建以后,对象内部的内容可以被修改。
常见可变对象包括:
| 类型 | 示例 |
|---|---|
列表 list |
[1, 2, 3] |
字典 dict |
{"name": "小明"} |
集合 set |
{1, 2, 3} |
例如,列表是可变对象:
numbers = [1, 2, 3]
numbers.append(4)
print(numbers)
输出:
[1, 2, 3, 4]
这里并不是创建了一个完全新的列表变量,而是在原来的列表对象里添加了一个元素。
五、不可变对象的定义
不可变对象指的是:
对象创建以后,对象内部的内容不能被原地修改。
常见不可变对象包括:
| 类型 | 示例 |
|---|---|
整数 int |
10 |
浮点数 float |
3.14 |
布尔值 bool |
True、False |
字符串 str |
"Python" |
元组 tuple |
(1, 2, 3) |
空值 NoneType |
None |
例如,字符串是不可变对象。
text = "Python"
text[0] = "J"
这会报错。
因为字符串创建以后,不能直接修改其中某一个字符。
如果想得到 "Jython",需要创建一个新的字符串:
text = "Python"
new_text = "J" + text[1:]
print(new_text)
输出:
Jython
六、可变与不可变的核心区别
可以用一句话概括:
可变对象可以原地修改,不可变对象不能原地修改。
对比:
numbers = [1, 2, 3]
numbers[0] = 100
print(numbers)
输出:
[100, 2, 3]
列表可以修改第一个元素。
但是:
text = "abc"
text[0] = "A"
会报错。
因为字符串不能修改第一个字符。
七、使用 id() 观察对象是否改变
Python 中可以使用 id() 查看对象的身份标识。
可以简单理解为:
id() 可以帮助我们观察变量是否还指向同一个对象。
1. 不可变对象重新赋值
x = 10
print(id(x))
x = x + 1
print(id(x))
两次 id(x) 通常不同。
原因是:
整数是不可变对象。
x = x + 1
不是把原来的整数 10 改成 11,而是创建或引用了另一个整数对象 11,然后让 x 指向它。
2. 可变对象原地修改
numbers = [1, 2, 3]
print(id(numbers))
numbers.append(4)
print(id(numbers))
两次 id(numbers) 通常相同。
原因是:
列表是可变对象,append() 是在原来的列表对象上添加元素。
对象本身还是同一个对象,只是内部内容变了。
八、列表 list 是可变对象
列表是最常见的可变对象。
students = ["小明", "小红"]
students.append("小刚")
print(students)
students[0] = "小李"
print(students)
输出:
['小明', '小红', '小刚']
['小李', '小红', '小刚']
列表支持原地修改,例如:
- 添加元素:
append() - 删除元素:
remove()、pop() - 修改元素:
students[0] = "小李" - 排序:
sort() - 清空:
clear()
例子:
numbers = [3, 1, 2]
numbers.sort()
print(numbers)
输出:
[1, 2, 3]
sort() 会直接修改原列表。
九、字典 dict 是可变对象
字典也是可变对象。
student = {
"name": "小明",
"score": 90
}
student["score"] = 100
student["age"] = 18
print(student)
输出:
{'name': '小明', 'score': 100, 'age': 18}
字典可以原地修改:
- 修改已有键对应的值。
- 添加新的键值对。
- 删除键值对。
- 清空字典。
例子:
student = {"name": "小明", "score": 90}
student.pop("score")
print(student)
输出:
{'name': '小明'}
十、集合 set 是可变对象
集合也是可变对象。
numbers = {1, 2, 3}
numbers.add(4)
numbers.remove(2)
print(numbers)
输出可能是:
{1, 3, 4}
集合可以原地添加和删除元素。
注意:
集合本身是可变的,但集合中的元素必须是可哈希的,通常不能把列表放进集合。
错误示例:
items = {[1, 2], [3, 4]}
这会报错,因为列表是可变对象,不能作为集合元素。
可以使用元组:
items = {(1, 2), (3, 4)}
print(items)
十一、字符串 str 是不可变对象
字符串是不可变对象。
text = "Python"
print(text[0])
输出:
P
虽然可以读取某个字符,但不能修改某个字符。
错误:
text = "Python"
text[0] = "J"
正确做法是创建新字符串:
text = "Python"
new_text = "J" + text[1:]
print(new_text)
输出:
Jython
字符串方法通常返回新字符串
text = " python "
new_text = text.strip()
print(text)
print(new_text)
输出:
python
python
strip() 不会修改原字符串,而是返回一个新字符串。
再如:
text = "python"
upper_text = text.upper()
print(text)
print(upper_text)
输出:
python
PYTHON
这说明字符串方法通常不会原地修改字符串。
十二、元组 tuple 是不可变对象
元组是不可变对象。
point = (3, 5)
print(point[0])
输出:
3
可以读取元组元素,但不能修改。
错误:
point = (3, 5)
point[0] = 10
会报错。
元组中如果包含可变对象
这是一个容易让学生困惑的地方。
元组本身不可变,意思是元组中每个位置指向的对象不能换掉。
但是,如果元组里面的某个元素是可变对象,那么这个可变对象自身的内容仍然可以修改。
例子:
data = ([1, 2], "Python")
data[0].append(3)
print(data)
输出:
([1, 2, 3], 'Python')
为什么没有报错?
因为:
data这个元组没有把第一个元素换成另一个对象。- 它只是修改了第一个元素所指向的列表对象的内部内容。
但是下面这样会报错:
data = ([1, 2], "Python")
data[0] = [9, 9]
因为这试图改变元组第一个位置指向的对象。
教学时可以这样说:
元组不可变,指的是元组的结构不可变;如果里面装的是列表,列表本身仍然可以变。
十三、整数、浮点数、布尔值是不可变对象
数字类型通常是不可变对象。
x = 10
y = x
y = y + 1
print(x)
print(y)
输出:
10
11
y = y + 1 不会修改原来的整数对象 10,而是让 y 指向新的结果。
浮点数也是类似:
price = 9.9
new_price = price + 1
print(price)
print(new_price)
输出:
9.9
10.9
布尔值也不可变:
flag = True
flag = False
这里不是把 True 对象改成 False,而是让变量 flag 指向另一个布尔值。
十四、赋值不是复制
这是本节非常重要的一句话:
赋值不是复制。
看例子:
a = [1, 2, 3]
b = a
这里 b = a 并没有创建新列表。
它只是让 b 和 a 指向同一个列表。
所以:
b.append(4)
print(a)
输出:
[1, 2, 3, 4]
如果想复制列表,应该明确使用复制方法。
十五、浅拷贝
浅拷贝可以创建一个新的容器对象。
1. 使用 copy()
a = [1, 2, 3]
b = a.copy()
b.append(4)
print(a)
print(b)
输出:
[1, 2, 3]
[1, 2, 3, 4]
这时 a 和 b 是两个不同列表。
2. 使用切片复制列表
a = [1, 2, 3]
b = a[:]
b.append(4)
print(a)
print(b)
输出:
[1, 2, 3]
[1, 2, 3, 4]
3. 使用 list() 创建新列表
a = [1, 2, 3]
b = list(a)
b.append(4)
print(a)
print(b)
输出:
[1, 2, 3]
[1, 2, 3, 4]
十六、浅拷贝的注意事项
浅拷贝只复制最外层容器。
如果列表里还有列表,里面的子列表可能仍然是共享的。
a = [[1, 2], [3, 4]]
b = a.copy()
b[0].append(99)
print(a)
print(b)
输出:
[[1, 2, 99], [3, 4]]
[[1, 2, 99], [3, 4]]
为什么?
因为:
b = a.copy()创建了一个新的外层列表。- 但是里面的
[1, 2]和[3, 4]这些子列表仍然和原来共享。
如果要连内部对象也复制,就需要深拷贝。
十七、深拷贝
深拷贝会尽量复制对象内部嵌套的对象。
需要使用 copy 模块。
import copy
a = [[1, 2], [3, 4]]
b = copy.deepcopy(a)
b[0].append(99)
print(a)
print(b)
输出:
[[1, 2], [3, 4]]
[[1, 2, 99], [3, 4]]
这时修改 b 内部的子列表,不会影响 a。
基础教学中可以这样讲:
- 一层普通列表,常用浅拷贝就够。
- 多层嵌套列表,如果要完全独立,可以使用深拷贝。
十八、函数参数中的可变对象
函数参数也会涉及可变对象和不可变对象。
1. 传入不可变对象
def change_number(x):
x = x + 1
print("函数内部:", x)
number = 10
change_number(number)
print("函数外部:", number)
输出:
函数内部: 11
函数外部: 10
函数内部的 x = x + 1 不会改变函数外的 number。
2. 传入可变对象
def add_item(items):
items.append("Python")
languages = ["Java"]
add_item(languages)
print(languages)
输出:
['Java', 'Python']
为什么函数外的列表也变了?
因为函数参数 items 和外面的 languages 指向同一个列表对象。
append() 修改的是这个列表对象本身。
3. 如果不想修改原列表
可以在函数内部先复制。
def add_item(items):
new_items = items.copy()
new_items.append("Python")
return new_items
languages = ["Java"]
result = add_item(languages)
print(languages)
print(result)
输出:
['Java']
['Java', 'Python']
十九、不要使用可变对象作为函数默认参数
这是 Python 中非常经典的注意事项。
错误示例:
def add_student(name, students=[]):
students.append(name)
return students
print(add_student("小明"))
print(add_student("小红"))
print(add_student("小刚"))
输出:
['小明']
['小明', '小红']
['小明', '小红', '小刚']
很多学生可能以为每次调用都会从空列表开始,但实际不是。
原因是:
默认参数中的列表只会在函数定义时创建一次,后面多次调用会一直使用同一个列表对象。
推荐写法:
def add_student(name, students=None):
if students is None:
students = []
students.append(name)
return students
print(add_student("小明"))
print(add_student("小红"))
print(add_student("小刚"))
输出:
['小明']
['小红']
['小刚']
如果传入已有列表:
class1 = ["小明"]
print(add_student("小红", class1))
输出:
['小明', '小红']
教学口诀:
函数默认参数不要直接写空列表、空字典、空集合。
二十、可变对象作为字典键和集合元素
字典的键必须是可哈希对象。
集合中的元素也必须是可哈希对象。
基础阶段可以简单理解:
通常不可变对象可以作为字典键,常见可变对象不能作为字典键。
正确:
student_scores = {
"小明": 95,
"小红": 88
}
字符串是不可变对象,可以作为字典键。
正确:
locations = {
(10, 20): "位置A",
(30, 40): "位置B"
}
元组通常可以作为字典键。
错误:
data = {
[1, 2]: "列表作为键"
}
列表是可变对象,不能作为字典键。
集合元素也是类似:
items = {(1, 2), (3, 4)}
可以。
items = {[1, 2], [3, 4]}
不可以。
二十一、可变与不可变和 +=
+= 在不同类型上的表现也容易让学生困惑。
1. 不可变对象上的 +=
x = 10
print(id(x))
x += 1
print(id(x))
整数不可变,所以 x += 1 会让 x 指向新的整数对象。
2. 列表上的 +=
a = [1, 2]
b = a
a += [3]
print(a)
print(b)
输出:
[1, 2, 3]
[1, 2, 3]
对列表来说,+= 通常会原地扩展列表,a 和 b 仍然看到同一个被修改后的列表。
如果写成:
a = [1, 2]
b = a
a = a + [3]
print(a)
print(b)
输出:
[1, 2, 3]
[1, 2]
这里 a + [3] 创建了一个新列表,然后让 a 指向新列表,b 仍然指向原列表。
这部分可以作为进阶补充,帮助学生理解“原地修改”和“创建新对象”的区别。
二十二、如何判断对象是否相同
可以使用 is 判断两个变量是否指向同一个对象。
a = [1, 2, 3]
b = a
c = [1, 2, 3]
print(a is b)
print(a is c)
print(a == c)
输出:
True
False
True
解释:
a is b是True,因为它们指向同一个列表对象。a is c是False,因为它们是两个不同列表对象。a == c是True,因为两个列表内容相同。
教学口诀:
== 比内容,is 比身份。
二十三、使用方法中的原地修改与返回新对象
学习可变与不可变对象时,还要注意方法的行为。
1. 列表 sort() 原地修改
numbers = [3, 1, 2]
result = numbers.sort()
print(numbers)
print(result)
输出:
[1, 2, 3]
None
sort() 会原地修改列表,并返回 None。
所以不要写:
numbers = numbers.sort()
这样会让 numbers 变成 None。
2. sorted() 返回新列表
numbers = [3, 1, 2]
result = sorted(numbers)
print(numbers)
print(result)
输出:
[3, 1, 2]
[1, 2, 3]
sorted() 不修改原列表,而是返回一个新的排序结果。
3. 字符串 upper() 返回新字符串
text = "python"
result = text.upper()
print(text)
print(result)
输出:
python
PYTHON
因为字符串不可变,upper() 只能返回新字符串。
二十四、可变与不可变对象的使用建议
1. 需要频繁修改的一组数据,用列表
students = []
students.append("小明")
students.append("小红")
print(students)
2. 不希望被修改的一组数据,用元组
point = (10, 20)
weekdays = ("Monday", "Tuesday", "Wednesday")
元组表达的是“这组数据结构比较固定”。
3. 文本处理用字符串,但修改时要接收新结果
name = " xiao ming "
name = name.strip()
print(name)
如果忘记接收结果:
name = " xiao ming "
name.strip()
print(name)
输出仍然带空格,因为原字符串没有被修改。
4. 函数中修改列表前要想清楚
如果函数的目的就是修改原列表,可以直接修改。
def add_student(students, name):
students.append(name)
如果不想影响原列表,就先复制。
def add_student(students, name):
new_students = students.copy()
new_students.append(name)
return new_students
5. 默认参数避免使用可变对象
不推荐:
def func(items=[]):
pass
推荐:
def func(items=None):
if items is None:
items = []
二十五、常见错误和改正方法
1. 以为赋值会复制列表
错误理解:
a = [1, 2, 3]
b = a
b.append(4)
以为 a 不会变。
实际:
print(a)
输出:
[1, 2, 3, 4]
如果要复制:
b = a.copy()
2. 试图修改字符串中的字符
错误:
text = "Python"
text[0] = "J"
正确:
text = "Python"
text = "J" + text[1:]
3. 忘记字符串方法返回新字符串
错误:
name = " 小明 "
name.strip()
print(name)
正确:
name = " 小明 "
name = name.strip()
print(name)
4. 使用可变对象作为默认参数
错误:
def add_item(item, items=[]):
items.append(item)
return items
正确:
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
5. 把列表作为字典键
错误:
data = {[1, 2]: "value"}
正确:
data = {(1, 2): "value"}
6. 把 sort() 的返回值重新赋给原变量
错误:
numbers = [3, 1, 2]
numbers = numbers.sort()
print(numbers)
输出:
None
正确:
numbers = [3, 1, 2]
numbers.sort()
print(numbers)
或者:
numbers = [3, 1, 2]
sorted_numbers = sorted(numbers)
print(sorted_numbers)
二十六、课堂综合示例
示例:学生名单管理
def add_student(students, name):
students.append(name)
class1 = ["小明", "小红"]
class2 = class1
add_student(class2, "小刚")
print("class1:", class1)
print("class2:", class2)
输出:
class1: ['小明', '小红', '小刚']
class2: ['小明', '小红', '小刚']
分析:
class1指向一个列表对象。class2 = class1没有复制列表,只是让class2也指向同一个列表。add_student(class2, "小刚")修改了这个列表对象。- 所以
class1和class2看到的结果一样。
如果想让 class2 是独立列表,可以写:
class1 = ["小明", "小红"]
class2 = class1.copy()
add_student(class2, "小刚")
print("class1:", class1)
print("class2:", class2)
输出:
class1: ['小明', '小红']
class2: ['小明', '小红', '小刚']
二十七、课堂讲解建议
教学时可以按下面顺序讲:
- 先讲变量和对象的关系:变量像标签,对象像物品。
- 再讲可变对象:列表、字典、集合可以原地修改。
- 再讲不可变对象:数字、字符串、元组不能原地修改。
- 用
a = [1, 2]、b = a演示共享引用。 - 用字符串修改失败的例子说明不可变。
- 再讲复制:赋值不是复制,
copy()才是复制的一种方式。 - 最后讲函数参数和默认参数中的注意事项。
可以给学生一个口诀:
列表字典集合可变,数字字符串元组不可变。
赋值只是贴标签,复制才是造新对象。
二十八、课堂练习
练习 1:判断是否会互相影响
阅读代码,判断输出结果。
a = [1, 2, 3]
b = a
b.append(4)
print(a)
print(b)
参考答案:
[1, 2, 3, 4]
[1, 2, 3, 4]
原因:
a 和 b 指向同一个列表对象。
练习 2:复制列表
请修改下面代码,让修改 b 不影响 a。
a = [1, 2, 3]
b = a
b.append(4)
print(a)
print(b)
参考答案:
a = [1, 2, 3]
b = a.copy()
b.append(4)
print(a)
print(b)
输出:
[1, 2, 3]
[1, 2, 3, 4]
练习 3:字符串能否修改
下面代码是否正确?
text = "Python"
text[0] = "J"
参考答案:
不正确。
字符串是不可变对象,不能直接修改某个字符。
可以写成:
text = "Python"
text = "J" + text[1:]
print(text)
练习 4:判断 id 是否可能变化
阅读代码,判断 id(numbers) 前后通常是否相同。
numbers = [1, 2, 3]
print(id(numbers))
numbers.append(4)
print(id(numbers))
参考答案:
通常相同。
因为列表是可变对象,append() 是原地修改。
练习 5:浅拷贝问题
阅读代码,判断输出结果。
a = [[1, 2], [3, 4]]
b = a.copy()
b[0].append(99)
print(a)
print(b)
参考答案:
[[1, 2, 99], [3, 4]]
[[1, 2, 99], [3, 4]]
原因:
浅拷贝只复制外层列表,里面的子列表仍然共享。
练习 6:修正默认参数问题
下面函数有什么问题?请改正。
def add_item(item, items=[]):
items.append(item)
return items
参考答案:
问题是使用了可变对象 [] 作为默认参数。
推荐写法:
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
练习 7:sort() 和 sorted()
阅读代码,判断输出结果。
numbers = [3, 1, 2]
result = numbers.sort()
print(numbers)
print(result)
参考答案:
[1, 2, 3]
None
原因:
sort() 原地修改列表,并返回 None。
练习 8:判断可变或不可变
请判断下面类型是可变对象还是不可变对象。
int
str
list
dict
tuple
set
float
bool
参考答案:
| 类型 | 可变或不可变 |
|---|---|
int |
不可变 |
str |
不可变 |
list |
可变 |
dict |
可变 |
tuple |
不可变 |
set |
可变 |
float |
不可变 |
bool |
不可变 |
二十九、常见错误对照表
| 错误现象 | 常见原因 | 修改方法 |
|---|---|---|
修改 b 后 a 也变了 |
a 和 b 指向同一个可变对象 |
使用 copy()、切片或深拷贝 |
text[0] = "J" 报错 |
字符串不可变 | 创建新字符串 |
name.strip() 后原变量没变 |
字符串方法返回新字符串 | 写成 name = name.strip() |
| 多层列表浅拷贝后仍互相影响 | 浅拷贝只复制外层 | 使用 copy.deepcopy() |
| 函数默认参数列表越积越多 | 可变默认参数只创建一次 | 默认值用 None |
| 列表不能作为字典键 | 列表可变,不可哈希 | 使用元组作为键 |
numbers = numbers.sort() 得到 None |
sort() 原地修改并返回 None |
直接 numbers.sort() 或用 sorted() |
三十、总结
可变与不可变对象是 Python 中非常重要的基础概念。
可变对象创建以后,内部内容可以被原地修改。
常见可变对象:
- 列表
list - 字典
dict - 集合
set
不可变对象创建以后,内部内容不能被原地修改。
常见不可变对象:
- 整数
int - 浮点数
float - 布尔值
bool - 字符串
str - 元组
tuple - 空值
None
本节需要重点掌握:
- 变量是对象的名字,不是对象本身。
- 赋值不是复制,只是让变量指向对象。
- 可变对象可以原地修改。
- 不可变对象不能原地修改,修改通常会产生新对象。
- 列表、字典、集合是常见可变对象。
- 数字、字符串、元组是常见不可变对象。
- 复制列表可以使用
copy()、切片或list()。 - 嵌套结构需要注意浅拷贝和深拷贝。
- 函数中修改可变对象可能影响函数外部。
不要使用可变对象作为函数默认参数。
教学时可以让学生记住一句话:
可变对象改内容,不可变对象换对象;赋值只是贴标签,复制才是造新对象。
掌握可变与不可变对象以后,学生对变量赋值、函数参数、列表字典操作、对象引用和程序调试都会理解得更深入。