18. Python 的数据与对象模型:引用、比较与拷贝
理解 Python 如何存储和管理数据,对于编写正确的程序至关重要。在本章中,我们将探索 Python 的对象模型(object model)——这是支配 Python 中所有数据如何工作的基础系统。你将学习为什么有些赋值会创建彼此独立的拷贝,而另一些会创建共享引用(reference),如何正确比较对象,以及在处理集合(collection)时如何避免常见陷阱。
这些知识将帮助你理解你可能遇到过的一些“反直觉”行为,例如为什么修改一个列表(list)有时会影响另一个,或者为什么用 == 比较两个列表得到的结果会与用 is 比较不同。
18.1) Python 中一切皆对象
在 Python 中,每一份数据都是对象(object)。这不仅是一个理论概念——它对你的程序如何运行有着实际影响。
当你创建一个数字、一个字符串(string)、一个列表,或任何其他值时,Python 都会在内存中创建一个 对象(object)。对象是一个容器,它包含:
- 实际数据(值(value))
- 关于它是什么数据类型的信息(类型(type))
- 一个唯一标识符(身份(identity))
让我们在实践中看看:
# 创建不同类型的对象
number = 42
text = "Hello"
items = [1, 2, 3]
# 这些变量中的每一个都引用内存中的某个对象
print(number) # Output: 42
print(text) # Output: Hello
print(items) # Output: [1, 2, 3]即使像整数(int)这样简单的值也是对象。这意味着它们不仅仅能存储一个数字,还具备更多能力:
# 整数是带有方法的对象
number = 42
print(number.bit_length()) # Output: 6
# 字符串是带有方法的对象
text = "hello"
print(text.upper()) # Output: HELLO
# 列表是带有方法的对象
items = [3, 1, 2]
items.sort()
print(items) # Output: [1, 2, 3]为什么这很重要? 因为当你给变量赋值或把数据传给函数(function)时,你并不是在复制对象——你是在创建一个指向同一对象的 引用(reference)。这与一些其他编程语言的工作方式有根本区别,而理解这种区别将避免许多令人困惑的 bug。
# 创建一个列表对象
original = [1, 2, 3]
# 这不会创建一个新列表——它会创建另一个引用
# 指向同一个列表对象
another_name = original
# 通过一个引用进行修改会影响另一个
another_name.append(4)
print(original) # Output: [1, 2, 3, 4]
print(another_name) # Output: [1, 2, 3, 4]original 和 another_name 都引用内存中的同一个列表对象。当我们通过 another_name 修改列表时,我们也会通过 original 看到变化,因为它们都指向同一个对象。
这种行为称为 引用语义(reference semantics),它是 Python 编程中最重要的概念之一。我们将在本章中对它进行深入探讨。
18.2) 对象的身份、类型和值(使用 type() 和 id())
Python 中的每个对象都有三个定义它的基本特征:身份(identity)、类型(type) 和 值(value)。理解这些特征能帮助你推理对象的行为,以及如何正确比较它们。
18.2.1) 用 id() 查看对象身份
对象的 身份(identity) 是 Python 在创建对象时分配的一个唯一数字。这个身份在对象的生命周期中永远不会改变——它就像内存中的永久地址。
你可以使用 id() 函数获取对象的身份:
# 创建对象并检查它们的身份
x = [1, 2, 3]
y = [1, 2, 3]
z = x
print(id(x)) # Output: 140234567890123 (example - actual number varies)
print(id(y)) # Output: 140234567890456 (different from x)
print(id(z)) # Output: 140234567890123 (same as x)你每次运行程序看到的实际数字都会不同,但模式保持不变:x 和 y 的身份不同,因为它们是不同的对象,即使它们包含相同的值。与此同时,z 的身份与 x 相同,因为 z 只是同一个对象的另一个名字。
这里有一个实际示例,说明为什么身份很重要:
# 两个学生有相同的成绩
student1_grades = [85, 90, 92]
student2_grades = [85, 90, 92]
# 这是不同对象(不同身份)
print(id(student1_grades)) # Output: 140234567890123 (example)
print(id(student2_grades)) # Output: 140234567890456 (different)
# 修改一个不会影响另一个
student1_grades.append(88)
print(student1_grades) # Output: [85, 90, 92, 88]
print(student2_grades) # Output: [85, 90, 92]现在考虑另一种场景:
# 同一个学生的成绩由两个变量跟踪
original_grades = [85, 90, 92]
backup_reference = original_grades
# 它们引用同一个对象(相同身份)
print(id(original_grades)) # Output: 140234567890123 (example)
print(id(backup_reference)) # Output: 140234567890123 (same!)
# 通过任意名字修改都会影响两者
backup_reference.append(88)
print(original_grades) # Output: [85, 90, 92, 88]
print(backup_reference) # Output: [85, 90, 92, 88]关键洞见:当两个变量具有相同的身份时,它们引用的是内存中完全相同的对象。通过一个变量做出的更改,会通过另一个变量可见,因为被修改的只有一个对象。
18.2.2) 用 type() 查看对象类型
对象的 类型(type) 决定了它持有什么种类的数据,以及你可以对它执行哪些操作。正如我们在第 3 章中学到的,你可以使用 type() 函数检查对象的类型:
# 不同类型的对象
number = 42
text = "Hello"
items = [1, 2, 3]
mapping = {"name": "Alice"}
print(type(number)) # Output: <class 'int'>
print(type(text)) # Output: <class 'str'>
print(type(items)) # Output: <class 'list'>
print(type(mapping)) # Output: <class 'dict'>对象的类型在创建后永远不会改变。你不能把一个整数变成字符串——你只能基于该整数的值创建一个新的字符串对象:
# 类型在创建时就固定了
x = 42
print(type(x)) # Output: <class 'int'>
# 这不会改变 x 的类型——它会创建一个新的字符串对象
# 并让 x 转而引用那个新对象
x = str(x)
# 原来的整数对象 (42) 仍会在内存中存在,直到被垃圾回收
# x 现在指向一个完全不同的对象:字符串 "42"
print(type(x)) # Output: <class 'str'>
print(x) # Output: 42 (now a string, not an integer)理解类型至关重要,因为不同类型支持不同操作:
# 列表支持 append
grades = [85, 90]
grades.append(92)
print(grades) # Output: [85, 90, 92]
# 字符串没有 append——它们是不可变的
text = "Hello"
# text.append(" World") # AttributeError: 'str' object has no attribute 'append'
# 但字符串支持拼接
text = text + " World"
print(text) # Output: Hello World18.2.3) 对象的值
对象的 值(value) 是它包含的实际数据。与身份和类型不同,对于 可变(mutable) 对象(如列表和字典),值可以改变;但对于 不可变(immutable) 对象(如整数和字符串),值不能改变。
# 对于可变对象,值可以改变
shopping_cart = ["milk", "bread"]
print(shopping_cart) # Output: ['milk', 'bread']
shopping_cart.append("eggs")
print(shopping_cart) # Output: ['milk', 'bread', 'eggs']
# 同一个对象(相同身份),不同的值
# 对于不可变对象,值不能改变
count = 5
print(count) # Output: 5
count = count + 1
print(count) # Output: 6
# 这创建了一个具有新身份的新对象下面是一个同时展示这三个特征的完整示例:
# 创建一个列表对象
data = [10, 20, 30]
print("Identity:", id(data)) # Output: Identity: 140234567890123 (example)
print("Type:", type(data)) # Output: Type: <class 'list'>
print("Value:", data) # Output: Value: [10, 20, 30]
# 修改值(身份和类型保持不变)
data.append(40)
print("Identity:", id(data)) # Output: Identity: 140234567890123 (unchanged)
print("Type:", type(data)) # Output: Type: <class 'list'> (unchanged)
print("Value:", data) # Output: Value: [10, 20, 30, 40] (changed)理解这三个特征能帮助你预测对象在程序中的行为。身份告诉你两个变量是否引用同一个对象;类型告诉你允许哪些操作;值告诉你对象当前持有什么数据。
18.3) 可变类型与不可变类型
Python 中最重要的区分之一,是 可变(mutable) 类型与 不可变(immutable) 类型的区别。这种区别会影响当你尝试更改对象时对象的行为,而理解它可以避免许多常见编程错误。
18.3.1) 不可变类型:无法改变的值
不可变(immutable) 对象是在创建后其值无法改变的对象。当你执行一个看似会修改不可变对象的操作时,Python 实际上会创建一个具有修改后值的新对象。
Python 的不可变类型包括:
- 整数(
int) - 浮点数(
float) - 字符串(
str) - 元组(
tuple) - 布尔值(
bool) - None(
NoneType)
让我们用整数看看不可变性:
# 创建一个整数
x = 100
print("Original x:", x) # Output: Original x: 100
print("Identity of x:", id(x)) # Output: Identity of x: 140234567890123 (example)
# 看起来像是在修改 x,但实际上是在创建一个新对象
x = x + 1
print("Modified x:", x) # Output: Modified x: 101
print("Identity of x:", id(x)) # Output: Identity of x: 140234567890456 (different!)身份改变了,因为 x = x + 1 创建了一个值为 101 的全新整数对象。值为 100 的原对象仍然存在(直到 Python 的垃圾回收器将其移除),但 x 现在引用的是另一个对象。
字符串更能清楚地体现不可变性:
# 创建一个字符串
message = "Hello"
print("Original:", message) # Output: Original: Hello
print("Identity:", id(message)) # Output: Identity: 140234567890789 (example)
# 字符串方法不会修改原字符串——它们会返回新字符串
uppercase = message.upper()
print("Original:", message) # Output: Original: Hello (unchanged)
print("Uppercase:", uppercase) # Output: Uppercase: HELLO
print("Identity of original:", id(message)) # Output: Identity of original: 140234567890789 (same)
print("Identity of uppercase:", id(uppercase)) # Output: Identity of uppercase: 140234567891012 (different)即使是看起来像在修改字符串的操作,也会创建新的字符串对象:
# 通过拼接构建字符串
text = "Python"
print("Before:", text, "- ID:", id(text)) # Output: Before: Python - ID: 140234567891234 (example)
text = text + " Programming"
print("After:", text, "- ID:", id(text)) # Output: After: Python Programming - ID: 140234567891567 (different)为什么不可变性重要:不可变对象可以在程序的不同部分之间安全共享,因为任何部分都无法意外地修改它们。这让代码更可预测,也更容易推理。
18.3.2) 可变类型:可以改变的值
可变(mutable) 对象是在创建后其值可以改变、且不需要创建新对象的对象。对象的身份保持不变,但其内容可以被修改。
Python 的可变类型包括:
- 列表(
list) - 字典(
dict) - 集合(
set)
让我们用列表看看可变性:
# 创建一个列表
numbers = [1, 2, 3]
print("Original:", numbers) # Output: Original: [1, 2, 3]
print("Identity:", id(numbers)) # Output: Identity: 140234567892345 (example)
# 修改列表——同一个对象,不同的值
numbers.append(4)
print("Modified:", numbers) # Output: Modified: [1, 2, 3, 4]
print("Identity:", id(numbers)) # Output: Identity: 140234567892345 (same!)身份没有改变,因为我们修改的是现有的列表对象,而不是创建一个新的对象。这与不可变类型的工作方式有本质区别。
字典和集合也是可变的:
# 字典示例
student = {"name": "Alice", "grade": 85}
print("Before:", student, "- ID:", id(student)) # Output: Before: {'name': 'Alice', 'grade': 85} - ID: 140234567893012 (example)
student["grade"] = 90 # 修改字典
print("After:", student, "- ID:", id(student)) # Output: After: {'name': 'Alice', 'grade': 90} - ID: 140234567893012 (same)
# 集合示例
unique_numbers = {1, 2, 3}
print("Before:", unique_numbers, "- ID:", id(unique_numbers)) # Output: Before: {1, 2, 3} - ID: 140234567893345 (example)
unique_numbers.add(4) # 修改集合
print("After:", unique_numbers, "- ID:", id(unique_numbers)) # Output: After: {1, 2, 3, 4} - ID: 140234567893345 (same)18.3.3) 为什么可变性在实践中很重要
当多个变量引用同一个对象时,可变与不可变类型的差异就变得至关重要:
# 不可变示例——安全共享
x = "Hello"
y = x # y 引用与 x 相同的字符串对象
# “修改” x 会创建一个新对象
x = x + " World"
print(x) # Output: Hello World
print(y) # Output: Hello (unchanged - y still refers to the original)# 可变示例——共享修改
list1 = [1, 2, 3]
list2 = list1 # list2 引用同一个列表对象
# 通过 list1 修改会影响 list2
list1.append(4)
print(list1) # Output: [1, 2, 3, 4]
print(list2) # Output: [1, 2, 3, 4] (also changed!)理解可变性对以下方面很重要:
- 预测行为:知道某个操作是创建新对象还是修改现有对象
- 避免 bug:防止对象共享时出现非预期修改
- 编写高效代码:为用例选择正确的类型
- 理解函数行为:知道函数参数何时可能被修改
在接下来的章节中,我们将探索赋值在不同类型下如何工作,以及在需要时如何创建彼此独立的拷贝。
18.4) 赋值在对象上的工作方式
Python 中的赋值不会复制对象——它会创建指向对象的 引用(reference)。理解这种区别对于编写正确程序至关重要,尤其是在处理可变类型时。
18.4.1) 赋值创建的是引用,而不是拷贝
当你写 x = y 时,Python 不会创建 y 所引用对象的拷贝。相反,它会让 x 引用 y 引用的同一个对象。两个变量都成为内存中同一对象的名字。
我们先用不可变对象看看:
# 对整数(不可变)的赋值
a = 100
b = a # b 现在引用与 a 相同的整数对象
print("a:", a) # Output: a: 100
print("b:", b) # Output: b: 100
print("Same object?", id(a) == id(b)) # Output: Same object? True
# “修改” a 会创建一个新对象
a = a + 1
print("a:", a) # Output: a: 101
print("b:", b) # Output: b: 100 (unchanged)
print("Same object?", id(a) == id(b)) # Output: Same object? False对于不可变对象,这种行为通常是安全的,因为你无法修改原对象。当你执行一个会改变值的操作时,Python 会创建一个新对象。
然而,对于可变对象,行为就非常不同:
# 对列表(可变)的赋值
list1 = [1, 2, 3]
list2 = list1 # list2 引用与 list1 相同的列表对象
print("list1:", list1) # Output: list1: [1, 2, 3]
print("list2:", list2) # Output: list2: [1, 2, 3]
print("Same object?", id(list1) == id(list2)) # Output: Same object? True
# 通过 list1 修改会影响 list2
list1.append(4)
print("list1:", list1) # Output: list1: [1, 2, 3, 4]
print("list2:", list2) # Output: list2: [1, 2, 3, 4] (also changed!)
print("Same object?", id(list1) == id(list2)) # Output: Same object? Truelist1 和 list2 都是同一个列表对象的名字。当你通过任意名字修改列表时,你会通过两个名字都看到变化,因为列表只有一个。
这里有一个实际示例,展示为什么这很重要:
# 管理学生成绩
alice_grades = [85, 90, 92]
backup_grades = alice_grades # 尝试创建一个备份
print("Original:", alice_grades) # Output: Original: [85, 90, 92]
print("Backup:", backup_grades) # Output: Backup: [85, 90, 92]
# 添加一个新成绩
alice_grades.append(88)
# “备份”也被修改了!
print("Original:", alice_grades) # Output: Original: [85, 90, 92, 88]
print("Backup:", backup_grades) # Output: Backup: [85, 90, 92, 88]这根本不是备份——两个变量都引用同一个列表。要创建真正的备份,你需要创建拷贝(我们将在 18.8 节介绍)。
18.4.2) 函数调用中的赋值
当你把参数传给函数时,Python 使用同样的引用语义。形参(parameter)会成为同一个对象的另一个名字:
# 带不可变参数的函数
def increment(number):
number = number + 1 # 创建新对象
return number
value = 5
result = increment(value)
print("Original value:", value) # Output: Original value: 5 (unchanged)
print("Returned result:", result) # Output: Returned result: 6形参 number 起初引用与 value 相同的整数对象。当我们执行 number = number + 1 时,我们创建一个新的整数对象并让 number 引用它。原对象(以及 value)保持不变。
对于可变对象,行为就不同了:
# 带可变参数的函数
def add_item(items, new_item):
items.append(new_item) # 修改原列表
shopping_list = ["milk", "bread"]
add_item(shopping_list, "eggs")
print("Original list:", shopping_list) # Output: Original list: ['milk', 'bread', 'eggs']形参 items 引用与 shopping_list 相同的列表对象。当我们通过 items 修改列表时,我们是在修改原列表。
下面是一个常见错误以及如何避免它:
# 错误:无意中修改了原对象
def process_grades(grades):
grades.append(100) # 修改了原列表!
return grades
student_grades = [85, 90, 92]
processed = process_grades(student_grades)
print("Original:", student_grades) # Output: Original: [85, 90, 92, 100] (modified!)
print("Processed:", processed) # Output: Processed: [85, 90, 92, 100]
# 正确:如果不想修改原对象,就创建一份拷贝
def process_grades_safely(grades):
# 创建一个包含相同元素的新列表
result = grades + [100] # 拼接会创建一个新列表
return result
student_grades = [85, 90, 92]
processed = process_grades_safely(student_grades)
print("Original:", student_grades) # Output: Original: [85, 90, 92] (unchanged)
print("Processed:", processed) # Output: Processed: [85, 90, 92, 100]关于可变默认参数的重要说明:一个相关的常见陷阱是把可变对象用作默认参数值(例如 def func(items=[]):)。默认参数在函数定义时只创建一次,而不是每次调用都创建,这可能导致意外行为:默认列表会在多次函数调用之间累积值。我们会在第 20 章详细探讨这一点,但请注意,这是在处理可变参数时非常常见的 bug 来源。
18.5) 引用语义与对象别名
引用语义(reference semantics) 表示:Python 中的变量是引用对象的名字,而不是装着值的容器。当多个变量引用同一个对象时,我们称之为 别名(aliasing)。理解别名对于预测程序行为至关重要。
18.5.1) 什么是别名?
当两个或多个变量引用内存中的同一个对象时,就发生了 别名(aliasing)。这些变量互为“别名”——对同一事物的不同名字。
让我们用一个简单示例看看别名:
# 创建一个列表以及一个别名
original = [1, 2, 3]
alias = original # alias 引用与 original 相同的列表
print("Original:", original) # Output: Original: [1, 2, 3]
print("Alias:", alias) # Output: Alias: [1, 2, 3]
print("Same object?", id(original) == id(alias)) # Output: Same object? True
# 通过别名进行修改
alias.append(4)
# 通过两个名字都能看到变化
print("Original:", original) # Output: Original: [1, 2, 3, 4]
print("Alias:", alias) # Output: Alias: [1, 2, 3, 4]内存中只有一个列表对象,但它有两个名字:original 和 alias。通过任意名字进行的修改都会影响同一个底层对象。
这里有一个更贴近实际的学生记录示例:
# 带别名的学生数据库
students = {
"alice": {"name": "Alice", "grade": 85},
"bob": {"name": "Bob", "grade": 90}
}
# 为 Alice 的记录创建一个别名
alice_record = students["alice"]
print("Alice's grade:", alice_record["grade"]) # Output: Alice's grade: 85
# 通过别名进行修改
alice_record["grade"] = 95
# 在原字典中也能看到变化
print("Updated grade:", students["alice"]["grade"]) # Output: Updated grade: 95变量 alice_record 是存储在 students["alice"] 处的字典的一个别名。当我们修改 alice_record 时,我们是在修改存放于 students 字典中的同一个字典。
18.5.2) 使用 is 运算符检测别名
你可以使用 is 运算符检查两个变量是否为别名(是否引用同一个对象):
# 检查是否存在别名
list1 = [1, 2, 3]
list2 = list1 # 别名
list3 = [1, 2, 3] # 值相同但对象不同
print("list1 is list2:", list1 is list2) # Output: list1 is list2: True (aliases)
print("list1 is list3:", list1 is list3) # Output: list1 is list3: False (different objects)
print("list1 == list3:", list1 == list3) # Output: list1 == list3: True (same value)is 运算符检查的是身份(两个变量是否引用同一个对象),而 == 检查的是值(两个对象内容是否相同)。我们将在 18.6 节详细探讨这种区别。
18.5.3) 集合中的别名
当对象被存放在集合中时,别名会变得更复杂:
# 创建一个由列表组成的列表
row = [0, 0, 0]
grid = [row, row, row] # 这三个元素都是同一个列表的别名!
print("Grid:")
for r in grid:
print(r)
# Output:
# [0, 0, 0]
# [0, 0, 0]
# [0, 0, 0]
# 修改一个元素会影响所有行
grid[0][0] = 1
print("\nAfter modification:")
for r in grid:
print(r)
# Output:
# [1, 0, 0]
# [1, 0, 0]
# [1, 0, 0]这是尝试创建二维网格时很常见的错误。三行都是同一个列表的别名,所以修改一行会修改所有行。
创建彼此独立的行的正确方式:
# 创建彼此独立的行
grid = [[0, 0, 0], [0, 0, 0], [0, 0, 0]] # 每一行都是一个独立列表
print("Grid:")
for r in grid:
print(r)
# Output:
# [0, 0, 0]
# [0, 0, 0]
# [0, 0, 0]
# 现在修改一个元素只会影响那一行
grid[0][0] = 1
print("\nAfter modification:")
for r in grid:
print(r)
# Output:
# [1, 0, 0]
# [0, 0, 0]
# [0, 0, 0]18.6) 跨类型的相等性、身份与成员关系(==、is 和 in)
Python 提供了三个用于比较与检查对象关系的基本运算符:用于相等性的 ==、用于身份的 is、用于成员关系的 in。理解何时使用哪个运算符,对于编写正确程序至关重要。
18.6.1) 使用 == 的相等性(比较值)
== 运算符检查两个对象是否具有 相同的值(value)。它们是否是内存中的同一个对象并不重要——重要的是它们的内容是否相等。
# 使用 == 比较值
list1 = [1, 2, 3]
list2 = [1, 2, 3]
list3 = list1
print(list1 == list2) # Output: True (same values)
print(list1 == list3) # Output: True (same values)即使 list1 和 list2 在内存中是不同对象,它们的值相同,所以 == 返回 True。
下面是 == 在不同类型上的表现:
# 不同类型之间的相等性
print(42 == 42) # Output: True (same integer value)
print(42 == 42.0) # Output: True (integer equals float with same value)
print("hello" == "hello") # Output: True (same string value)
print([1, 2] == [1, 2]) # Output: True (same list contents)
print({"a": 1} == {"a": 1}) # Output: True (same dictionary contents)
# 不同的值
print(42 == 43) # Output: False
print("hello" == "Hello") # Output: False (case-sensitive)
print([1, 2] == [2, 1]) # Output: False (order matters)对于集合类型,== 会执行 深度比较(deep comparison)——它会检查所有元素是否相等:
# 带嵌套结构的深度比较
list1 = [[1, 2], [3, 4]]
list2 = [[1, 2], [3, 4]]
print(list1 == list2) # Output: True (all nested elements are equal)
# 即使内部列表是不同对象
print(id(list1[0]) == id(list2[0])) # Output: False (different objects)
print(list1[0] == list2[0]) # Output: True (same values)18.6.2) 使用 is 的身份(比较对象身份)
is 运算符检查两个变量是否引用内存中的 同一个对象。它比较的是身份,而不是值。
# 使用 is 比较身份
list1 = [1, 2, 3]
list2 = [1, 2, 3]
list3 = list1
print(list1 is list2) # Output: False (different objects)
print(list1 is list3) # Output: True (same object)
# 用 id() 验证
print(id(list1) == id(list2)) # Output: False
print(id(list1) == id(list3)) # Output: True何时使用 is:is 最常见的用途是检查 None:
# 检查 None(正确方式)
def find_student(name, students):
"""Return student record or None if not found."""
for student in students:
if student["name"] == name:
return student
return None
students = [
{"name": "Alice", "grade": 85},
{"name": "Bob", "grade": 90}
]
result = find_student("Charlie", students)
# 使用 'is' 来检查 None
if result is None:
print("Student not found") # Output: Student not found
else:
print(f"Found: {result}")18.6.3) 使用 in 的成员关系(检查包含关系)
in 运算符检查某个值是否包含在集合中。它适用于字符串、列表、元组、集合和字典:
# 不同类型中的成员关系
print(2 in [1, 2, 3]) # Output: True
print("hello" in "hello world") # Output: True
print("x" in {"x": 10, "y": 20}) # Output: True (checks keys)
print(5 in {1, 2, 3, 4, 5}) # Output: True对于字典,in 检查的是 key 是否存在:
# 检查字典成员关系
student = {"name": "Alice", "grade": 85, "age": 20}
print("name" in student) # Output: True (key exists)
print("Alice" in student) # Output: False (value, not key)
print("grade" in student) # Output: True (key exists)
# 检查 value 需要访问 .values()
print("Alice" in student.values()) # Output: Truenot in 运算符用于检查不存在:
# 检查不存在
shopping_list = ["milk", "bread", "eggs"]
if "butter" not in shopping_list:
print("Don't forget to buy butter!") # Output: Don't forget to buy butter!各运算符的使用总结:
- 当你想检查两个对象是否有相同的值时,使用
== - 当你想检查两个变量是否引用同一个对象时,使用
is(最常见的是与None一起使用,或在调试别名问题时使用) - 当你想检查某个值是否包含在集合中时,使用
in
理解这些区别能帮助你在程序中写出更精确、更正确的比较。
18.7) 比较包含其他对象的对象
当对象包含其他对象(例如列表中的列表,或字典中包含列表)时,比较会更微妙。理解 Python 如何比较嵌套结构,对于处理复杂数据至关重要。
18.7.1) == 在嵌套结构上的工作方式
对于嵌套结构,== 运算符会进行 递归比较(recursive comparison)。它不仅比较外层容器,还会比较所有嵌套对象:
# 比较嵌套列表
list1 = [[1, 2], [3, 4]]
list2 = [[1, 2], [3, 4]]
print(list1 == list2) # Output: True
# 即使内部列表是不同对象
print(id(list1[0]) == id(list2[0])) # Output: False
print(list1[0] == list2[0]) # Output: TruePython 会递归地比较每个元素。要让 list1 == list2 为 True,每个对应元素都必须相等,包括嵌套元素。
这里有一个更复杂的示例:
# 多层嵌套结构
data1 = {
"students": [
{"name": "Alice", "grades": [85, 90, 92]},
{"name": "Bob", "grades": [88, 91, 87]}
],
"class": "Python 101"
}
data2 = {
"students": [
{"name": "Alice", "grades": [85, 90, 92]},
{"name": "Bob", "grades": [88, 91, 87]}
],
"class": "Python 101"
}
print(data1 == data2) # Output: TruePython 比较:
- 顶层的字典 key 和 value("students" 和 "class")
- 学生列表
- 每个学生字典(包含 "name" 和 "grades" key)
- 每个学生的成绩列表
- 每一个具体的成绩数字
所有层级都必须匹配,比较结果才会返回 True。
18.7.2) 序列的顺序很重要
对于序列(列表与元组),元素顺序很重要:
# 列表中顺序很重要
list1 = [[1, 2], [3, 4]]
list2 = [[3, 4], [1, 2]]
print(list1 == list2) # Output: False (different order)
# 但集合中顺序不重要
set1 = {frozenset([1, 2]), frozenset([3, 4])}
set2 = {frozenset([3, 4]), frozenset([1, 2])}
print(set1 == set2) # Output: True (sets are unordered)18.7.3) 比较不同类型的集合
不同的集合类型(list、tuple、set)永远不会彼此相等,即使它们包含相同元素:
# 比较不同类型
print([1, 2, 3] == (1, 2, 3)) # Output: False (list vs tuple)
print([1, 2, 3] == {1, 2, 3}) # Output: False (list vs set)
# 即使包含相同元素
list_version = [1, 2, 3]
tuple_version = (1, 2, 3)
set_version = {1, 2, 3}
print(list_version == tuple_version) # Output: False
print(list_version == set_version) # Output: False
print(tuple_version == set_version) # Output: False18.8) 列表、字典与集合的浅拷贝(以及它们不会拷贝什么)
在处理可变对象时,你经常需要创建彼此独立的拷贝,以避免非预期的修改。例如,在处理数据之前进行备份、创建不影响生产数据的测试场景,或把数据传给不应修改原对象的函数。理解 Python 的拷贝机制如何工作,有助于你在需要时创建真正独立的拷贝。
然而,并不是所有拷贝方法都会创建完全独立的拷贝。理解 浅拷贝(shallow copy) 与 深拷贝(deep copy) 的区别,对于避免隐蔽 bug 至关重要。
18.8.1) 什么是浅拷贝?
浅拷贝(shallow copy) 会创建一个新对象,但不会为其中包含的对象创建拷贝。相反,新对象会包含指向与原对象相同的嵌套对象的引用。
让我们先看一个简单列表:
# 对简单列表创建浅拷贝
original = [1, 2, 3]
copy = original.copy() # 创建一个浅拷贝
print("Original:", original) # Output: Original: [1, 2, 3]
print("Copy:", copy) # Output: Copy: [1, 2, 3]
# 它们是不同对象
print("Same object?", original is copy) # Output: Same object? False
# 修改拷贝不会影响原对象
copy.append(4)
print("Original:", original) # Output: Original: [1, 2, 3]
print("Copy:", copy) # Output: Copy: [1, 2, 3, 4]对于包含不可变对象(如整数)的简单列表,浅拷贝非常好用。拷贝与原对象彼此独立。
但对于嵌套结构会发生什么?让我们看看浅拷贝的局限:
# 带嵌套列表的浅拷贝
original = [[1, 2], [3, 4]]
copy = original.copy()
print("Original:", original) # Output: Original: [[1, 2], [3, 4]]
print("Copy:", copy) # Output: Copy: [[1, 2], [3, 4]]
# 外层列表是不同对象
print("Same outer list?", original is copy) # Output: Same outer list? False
# 但嵌套列表是同一个对象
print("Same nested list?", original[0] is copy[0]) # Output: Same nested list? True
# 修改嵌套列表会影响两者
copy[0].append(99)
print("Original:", original) # Output: Original: [[1, 2, 99], [3, 4]]
print("Copy:", copy) # Output: Copy: [[1, 2, 99], [3, 4]]18.8.2) 创建列表的浅拷贝
有几种方式可以创建列表的浅拷贝:
# 方法 1:使用 copy() 方法
original = [[1, 2], [3, 4]]
copy1 = original.copy()
# 方法 2:使用列表切片
copy2 = original[:]
# 方法 3:使用 list() 构造器
copy3 = list(original)
# 这三种都会创建浅拷贝
print(copy1) # Output: [[1, 2], [3, 4]]
print(copy2) # Output: [[1, 2], [3, 4]]
print(copy3) # Output: [[1, 2], [3, 4]]
# 外层列表不同
print(original is copy1) # Output: False
print(original is copy2) # Output: False
print(original is copy3) # Output: False
# 但内层列表是共享的
print(original[0] is copy1[0]) # Output: True
print(original[0] is copy2[0]) # Output: True
print(original[0] is copy3[0]) # Output: True18.8.3) 创建字典的浅拷贝
字典也支持浅拷贝:
# 方法 1:使用 copy() 方法
original = {"name": "Alice", "grade": 85}
copy1 = original.copy()
# 方法 2:使用 dict() 构造器
copy2 = dict(original)
# 两种都会创建浅拷贝
print(copy1) # Output: {'name': 'Alice', 'grade': 85}
print(copy2) # Output: {'name': 'Alice', 'grade': 85}
# 它们是不同对象
print(original is copy1) # Output: False
print(original is copy2) # Output: False
# 修改拷贝不会影响原对象
copy1["grade"] = 90
print("Original:", original) # Output: Original: {'name': 'Alice', 'grade': 85}
print("Copy:", copy1) # Output: Copy: {'name': 'Alice', 'grade': 90}不过,对于嵌套结构,同样存在浅拷贝的限制:
# 带嵌套结构的浅拷贝
original = {
"name": "Alice",
"grades": [85, 90, 92]
}
copy = original.copy()
print("Original:", original) # Output: Original: {'name': 'Alice', 'grades': [85, 90, 92]}
print("Copy:", copy) # Output: Copy: {'name': 'Alice', 'grades': [85, 90, 92]}
# 字典是不同对象
print("Same dict?", original is copy) # Output: Same dict? False
# 但 grades 列表是同一个对象
print("Same grades list?", original["grades"] is copy["grades"]) # Output: Same grades list? True
# 修改 grades 列表会影响两者
copy["grades"].append(88)
print("Original:", original) # Output: Original: {'name': 'Alice', 'grades': [85, 90, 92, 88]}
print("Copy:", copy) # Output: Copy: {'name': 'Alice', 'grades': [85, 90, 92, 88]}