Python & AI Tutorials Logo
Python 编程

18. Python 的数据与对象模型:引用、比较与拷贝

理解 Python 如何存储和管理数据,对于编写正确的程序至关重要。在本章中,我们将探索 Python 的对象模型(object model)——这是支配 Python 中所有数据如何工作的基础系统。你将学习为什么有些赋值会创建彼此独立的拷贝,而另一些会创建共享引用(reference),如何正确比较对象,以及在处理集合(collection)时如何避免常见陷阱。

这些知识将帮助你理解你可能遇到过的一些“反直觉”行为,例如为什么修改一个列表(list)有时会影响另一个,或者为什么用 == 比较两个列表得到的结果会与用 is 比较不同。

18.1) Python 中一切皆对象

在 Python 中,每一份数据都是对象(object)。这不仅是一个理论概念——它对你的程序如何运行有着实际影响。

当你创建一个数字、一个字符串(string)、一个列表,或任何其他值时,Python 都会在内存中创建一个 对象(object)。对象是一个容器,它包含:

  • 实际数据(值(value)
  • 关于它是什么数据类型的信息(类型(type)
  • 一个唯一标识符(身份(identity)

让我们在实践中看看:

python
# 创建不同类型的对象
number = 42
text = "Hello"
items = [1, 2, 3]
 
# 这些变量中的每一个都引用内存中的某个对象
print(number)  # Output: 42
print(text)    # Output: Hello
print(items)   # Output: [1, 2, 3]

即使像整数(int)这样简单的值也是对象。这意味着它们不仅仅能存储一个数字,还具备更多能力:

python
# 整数是带有方法的对象
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。

python
# 创建一个列表对象
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]

originalanother_name 都引用内存中的同一个列表对象。当我们通过 another_name 修改列表时,我们也会通过 original 看到变化,因为它们都指向同一个对象。

变量: original

列表对象: 1, 2, 3, 4

变量: another_name

这种行为称为 引用语义(reference semantics),它是 Python 编程中最重要的概念之一。我们将在本章中对它进行深入探讨。

18.2) 对象的身份、类型和值(使用 type() 和 id())

Python 中的每个对象都有三个定义它的基本特征:身份(identity)类型(type)值(value)。理解这些特征能帮助你推理对象的行为,以及如何正确比较它们。

18.2.1) 用 id() 查看对象身份

对象的 身份(identity) 是 Python 在创建对象时分配的一个唯一数字。这个身份在对象的生命周期中永远不会改变——它就像内存中的永久地址。

你可以使用 id() 函数获取对象的身份:

python
# 创建对象并检查它们的身份
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)

你每次运行程序看到的实际数字都会不同,但模式保持不变:xy 的身份不同,因为它们是不同的对象,即使它们包含相同的值。与此同时,z 的身份与 x 相同,因为 z 只是同一个对象的另一个名字。

这里有一个实际示例,说明为什么身份很重要:

python
# 两个学生有相同的成绩
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]

现在考虑另一种场景:

python
# 同一个学生的成绩由两个变量跟踪
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() 函数检查对象的类型:

python
# 不同类型的对象
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'>

对象的类型在创建后永远不会改变。你不能把一个整数变成字符串——你只能基于该整数的值创建一个新的字符串对象:

python
# 类型在创建时就固定了
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)

理解类型至关重要,因为不同类型支持不同操作:

python
# 列表支持 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 World

18.2.3) 对象的值

对象的 值(value) 是它包含的实际数据。与身份和类型不同,对于 可变(mutable) 对象(如列表和字典),值可以改变;但对于 不可变(immutable) 对象(如整数和字符串),值不能改变。

python
# 对于可变对象,值可以改变
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
# 这创建了一个具有新身份的新对象

下面是一个同时展示这三个特征的完整示例:

python
# 创建一个列表对象
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)

对象

身份: 唯一 ID

类型: class 'list'

值: 10, 20, 30, 40

永远不变

永远不变

对可变类型可能改变

理解这三个特征能帮助你预测对象在程序中的行为。身份告诉你两个变量是否引用同一个对象;类型告诉你允许哪些操作;值告诉你对象当前持有什么数据。

18.3) 可变类型与不可变类型

Python 中最重要的区分之一,是 可变(mutable) 类型与 不可变(immutable) 类型的区别。这种区别会影响当你尝试更改对象时对象的行为,而理解它可以避免许多常见编程错误。

18.3.1) 不可变类型:无法改变的值

不可变(immutable) 对象是在创建后其值无法改变的对象。当你执行一个看似会修改不可变对象的操作时,Python 实际上会创建一个具有修改后值的新对象。

Python 的不可变类型包括:

  • 整数int
  • 浮点数float
  • 字符串str
  • 元组tuple
  • 布尔值bool
  • NoneNoneType

让我们用整数看看不可变性:

python
# 创建一个整数
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 现在引用的是另一个对象。

字符串更能清楚地体现不可变性:

python
# 创建一个字符串
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)

即使是看起来像在修改字符串的操作,也会创建新的字符串对象:

python
# 通过拼接构建字符串
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

让我们用列表看看可变性:

python
# 创建一个列表
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!)

身份没有改变,因为我们修改的是现有的列表对象,而不是创建一个新的对象。这与不可变类型的工作方式有本质区别。

字典和集合也是可变的:

python
# 字典示例
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) 为什么可变性在实践中很重要

当多个变量引用同一个对象时,可变与不可变类型的差异就变得至关重要:

python
# 不可变示例——安全共享
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)
python
# 可变示例——共享修改
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!)

不可变类型

int, float, str, tuple, bool, None

值无法改变

操作会创建新对象

可安全共享

可变类型

list, dict, set

值可以改变

操作会修改现有对象

共享时需要谨慎

理解可变性对以下方面很重要:

  1. 预测行为:知道某个操作是创建新对象还是修改现有对象
  2. 避免 bug:防止对象共享时出现非预期修改
  3. 编写高效代码:为用例选择正确的类型
  4. 理解函数行为:知道函数参数何时可能被修改

在接下来的章节中,我们将探索赋值在不同类型下如何工作,以及在需要时如何创建彼此独立的拷贝。

18.4) 赋值在对象上的工作方式

Python 中的赋值不会复制对象——它会创建指向对象的 引用(reference)。理解这种区别对于编写正确程序至关重要,尤其是在处理可变类型时。

18.4.1) 赋值创建的是引用,而不是拷贝

当你写 x = y 时,Python 不会创建 y 所引用对象的拷贝。相反,它会让 x 引用 y 引用的同一个对象。两个变量都成为内存中同一对象的名字。

我们先用不可变对象看看:

python
# 对整数(不可变)的赋值
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 会创建一个新对象。

然而,对于可变对象,行为就非常不同:

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? True

list1list2 都是同一个列表对象的名字。当你通过任意名字修改列表时,你会通过两个名字都看到变化,因为列表只有一个。

对不可变类型的赋值

两个变量起初都引用同一对象

操作会创建新对象

变量变得彼此独立

对可变类型的赋值

两个变量都引用同一对象

操作会修改共享对象

通过两个变量都能看到变化

这里有一个实际示例,展示为什么这很重要:

python
# 管理学生成绩
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)会成为同一个对象的另一个名字:

python
# 带不可变参数的函数
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)保持不变。

对于可变对象,行为就不同了:

python
# 带可变参数的函数
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 修改列表时,我们是在修改原列表。

下面是一个常见错误以及如何避免它:

python
# 错误:无意中修改了原对象
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)。这些变量互为“别名”——对同一事物的不同名字。

让我们用一个简单示例看看别名:

python
# 创建一个列表以及一个别名
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]

内存中只有一个列表对象,但它有两个名字:originalalias。通过任意名字进行的修改都会影响同一个底层对象。

这里有一个更贴近实际的学生记录示例:

python
# 带别名的学生数据库
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 运算符检查两个变量是否为别名(是否引用同一个对象):

python
# 检查是否存在别名
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) 集合中的别名

当对象被存放在集合中时,别名会变得更复杂:

python
# 创建一个由列表组成的列表
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]

这是尝试创建二维网格时很常见的错误。三行都是同一个列表的别名,所以修改一行会修改所有行。

创建彼此独立的行的正确方式:

python
# 创建彼此独立的行
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)。它们是否是内存中的同一个对象并不重要——重要的是它们的内容是否相等。

python
# 使用 == 比较值
list1 = [1, 2, 3]
list2 = [1, 2, 3]
list3 = list1
 
print(list1 == list2)  # Output: True (same values)
print(list1 == list3)  # Output: True (same values)

即使 list1list2 在内存中是不同对象,它们的值相同,所以 == 返回 True

下面是 == 在不同类型上的表现:

python
# 不同类型之间的相等性
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)——它会检查所有元素是否相等:

python
# 带嵌套结构的深度比较
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 运算符检查两个变量是否引用内存中的 同一个对象。它比较的是身份,而不是值。

python
# 使用 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

何时使用 isis 最常见的用途是检查 None

python
# 检查 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 运算符检查某个值是否包含在集合中。它适用于字符串、列表、元组、集合和字典:

python
# 不同类型中的成员关系
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 是否存在:

python
# 检查字典成员关系
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: True

not in 运算符用于检查不存在:

python
# 检查不存在
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)。它不仅比较外层容器,还会比较所有嵌套对象:

python
# 比较嵌套列表
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: True

Python 会递归地比较每个元素。要让 list1 == list2True,每个对应元素都必须相等,包括嵌套元素。

这里有一个更复杂的示例:

python
# 多层嵌套结构
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: True

Python 比较:

  1. 顶层的字典 key 和 value("students" 和 "class")
  2. 学生列表
  3. 每个学生字典(包含 "name" 和 "grades" key)
  4. 每个学生的成绩列表
  5. 每一个具体的成绩数字

所有层级都必须匹配,比较结果才会返回 True

18.7.2) 序列的顺序很重要

对于序列(列表与元组),元素顺序很重要:

python
# 列表中顺序很重要
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)永远不会彼此相等,即使它们包含相同元素:

python
# 比较不同类型
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: False

18.8) 列表、字典与集合的浅拷贝(以及它们不会拷贝什么)

在处理可变对象时,你经常需要创建彼此独立的拷贝,以避免非预期的修改。例如,在处理数据之前进行备份、创建不影响生产数据的测试场景,或把数据传给不应修改原对象的函数。理解 Python 的拷贝机制如何工作,有助于你在需要时创建真正独立的拷贝。

然而,并不是所有拷贝方法都会创建完全独立的拷贝。理解 浅拷贝(shallow copy)深拷贝(deep copy) 的区别,对于避免隐蔽 bug 至关重要。

18.8.1) 什么是浅拷贝?

浅拷贝(shallow copy) 会创建一个新对象,但不会为其中包含的对象创建拷贝。相反,新对象会包含指向与原对象相同的嵌套对象的引用。

让我们先看一个简单列表:

python
# 对简单列表创建浅拷贝
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]

对于包含不可变对象(如整数)的简单列表,浅拷贝非常好用。拷贝与原对象彼此独立。

但对于嵌套结构会发生什么?让我们看看浅拷贝的局限:

python
# 带嵌套列表的浅拷贝
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]]

原始列表

嵌套列表 1: 1, 2, 99

嵌套列表 2: 3, 4

浅拷贝

18.8.2) 创建列表的浅拷贝

有几种方式可以创建列表的浅拷贝:

python
# 方法 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: True

18.8.3) 创建字典的浅拷贝

字典也支持浅拷贝:

python
# 方法 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}

不过,对于嵌套结构,同样存在浅拷贝的限制:

python
# 带嵌套结构的浅拷贝
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]}
© 2025. Primesoft Co., Ltd.
support@primesoft.ai