目 录CONTENT

文章目录

Python(九) 基础语法:可变与不可变对象

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

这一次,修改 ba 却没有变。

为什么列表会互相影响,而整数不会?

这就涉及 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]

因为 ab 指向的是同一个列表对象。


四、可变对象的定义

可变对象指的是:

对象创建以后,对象内部的内容可以被修改。

常见可变对象包括:

类型 示例
列表 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 TrueFalse
字符串 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 并没有创建新列表。

它只是让 ba 指向同一个列表。

所以:

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]

这时 ab 是两个不同列表。


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]

对列表来说,+= 通常会原地扩展列表,ab 仍然看到同一个被修改后的列表。

如果写成:

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 bTrue,因为它们指向同一个列表对象。
  • a is cFalse,因为它们是两个不同列表对象。
  • a == cTrue,因为两个列表内容相同。

教学口诀:

== 比内容,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: ['小明', '小红', '小刚']

分析:

  1. class1 指向一个列表对象。
  2. class2 = class1 没有复制列表,只是让 class2 也指向同一个列表。
  3. add_student(class2, "小刚") 修改了这个列表对象。
  4. 所以 class1class2 看到的结果一样。

如果想让 class2 是独立列表,可以写:

class1 = ["小明", "小红"]
class2 = class1.copy()

add_student(class2, "小刚")

print("class1:", class1)
print("class2:", class2)

输出:

class1: ['小明', '小红']
class2: ['小明', '小红', '小刚']

二十七、课堂讲解建议

教学时可以按下面顺序讲:

  1. 先讲变量和对象的关系:变量像标签,对象像物品。
  2. 再讲可变对象:列表、字典、集合可以原地修改。
  3. 再讲不可变对象:数字、字符串、元组不能原地修改。
  4. a = [1, 2]b = a 演示共享引用。
  5. 用字符串修改失败的例子说明不可变。
  6. 再讲复制:赋值不是复制,copy() 才是复制的一种方式。
  7. 最后讲函数参数和默认参数中的注意事项。

可以给学生一个口诀:

列表字典集合可变,数字字符串元组不可变。
赋值只是贴标签,复制才是造新对象。

二十八、课堂练习

练习 1:判断是否会互相影响

阅读代码,判断输出结果。

a = [1, 2, 3]
b = a

b.append(4)

print(a)
print(b)

参考答案:

[1, 2, 3, 4]
[1, 2, 3, 4]

原因:

ab 指向同一个列表对象。


练习 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 不可变

二十九、常见错误对照表

错误现象 常见原因 修改方法
修改 ba 也变了 ab 指向同一个可变对象 使用 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

本节需要重点掌握:

  1. 变量是对象的名字,不是对象本身。
  2. 赋值不是复制,只是让变量指向对象。
  3. 可变对象可以原地修改。
  4. 不可变对象不能原地修改,修改通常会产生新对象。
  5. 列表、字典、集合是常见可变对象。
  6. 数字、字符串、元组是常见不可变对象。
  7. 复制列表可以使用 copy()、切片或 list()
  8. 嵌套结构需要注意浅拷贝和深拷贝。
  9. 函数中修改可变对象可能影响函数外部。
    不要使用可变对象作为函数默认参数。

教学时可以让学生记住一句话:

可变对象改内容,不可变对象换对象;赋值只是贴标签,复制才是造新对象。

掌握可变与不可变对象以后,学生对变量赋值、函数参数、列表字典操作、对象引用和程序调试都会理解得更深入。

0
博主关闭了当前页面的评论