Python中对象分为可变和不可变两类,区别在于创建后能否修改其内容。可变对象(如列表、字典、集合)允许原地修改,内存地址不变;不可变对象(如整数、字符串、元组)一旦创建内容不可更改,任何“修改”实际是创建新对象。这种机制影响函数参数传递、哈希性、并发安全和性能优化。例如,不可变对象可作为字典键,因具有固定哈希值;而可变对象则不能。判断可变性可通过尝试修改并观察id()是否变化,或使用hash()检查是否可哈希。理解此差异有助于避免副作用和潜在bug,提升代码可预测性与健壮性。

Python中的对象大致可以分为两类:可变对象和不可变对象。核心区别在于,可变对象在创建后其内部状态可以被修改,而不可变对象一旦创建,其值就不能再改变。如果看起来它们“变”了,那通常是创建了一个新的对象。理解这一点,对于写出健壮、可预测的Python代码至关重要。
解决方案
在Python的世界里,对象的“可变性”(mutability)和“不可变性”(immutability)是理解其工作机制的关键概念。简单来说,一个可变对象就像一块橡皮泥,你可以随意捏扁搓圆,改变它的形状,但它依然是那块橡皮泥,内存地址不变。而不可变对象则更像是一块雕塑,一旦完成,你就不能再改变它。任何对它的“修改”操作,实际上都是在旁边重新雕刻了一块新的雕塑。
这种设计哲学渗透在Python的方方面面。比如,当你有一个列表(list),你可以直接在原地添加元素、删除元素,甚至修改特定位置的值,这些操作都不会改变列表本身的内存地址。但如果你有一个字符串(string),尝试修改它的某个字符,Python会直接报错,因为字符串是不可变的。所有看似修改字符串的操作,比如字符串拼接,实际上都是创建了一个全新的字符串对象。
这种区分带来的影响非常深远,尤其是在函数参数传递、多线程编程(虽然Python的GIL在一定程度上简化了并发数据修改的问题,但理解可变性依然重要)、以及作为字典键或集合元素时。我个人觉得,很多初学者在遇到意料之外的行为时,往往就是因为没有搞清楚对象的可变性。
立即学习“Python免费学习笔记(深入)”;
Python中常见的可变对象类型有哪些?
谈到可变对象,我们最常打交道的莫过于列表(
list)、字典(
dict)和集合(
set)了。它们是Python数据结构中的主力军,也是我们日常编程中修改频率最高的几种。
列表(
list)是我觉得最直观的可变对象。你可以直接在原地增删改查。比如,
my_list = [1, 2, 3],然后
my_list.append(4),你会发现
my_list变成了
[1, 2, 3, 4],而
id(my_list)并没有变。这种“原地修改”的特性让列表在处理动态数据集时非常高效。
my_list = [1, 2, 3]
print(f"原始列表: {my_list}, ID: {id(my_list)}")
my_list.append(4) # 在原地修改
print(f"添加元素后: {my_list}, ID: {id(my_list)}")
my_list[0] = 10 # 修改第一个元素
print(f"修改元素后: {my_list}, ID: {id(my_list)}")字典(
dict)也一样,它是键值对的集合。你可以随时添加新的键值对,修改现有键对应的值,或者删除键值对,而字典对象本身依然是那个字典。
my_dict = {'a': 1, 'b': 2}
print(f"原始字典: {my_dict}, ID: {id(my_dict)}")
my_dict['c'] = 3 # 添加新键值对
print(f"添加元素后: {my_dict}, ID: {id(my_dict)}")
my_dict['a'] = 10 # 修改值
print(f"修改值后: {my_dict}, ID: {id(my_dict)}")集合(
set)是无序不重复元素的集合,它也支持添加和删除元素的操作。
my_set = {1, 2, 3}
print(f"原始集合: {my_set}, ID: {id(my_set)}")
my_set.add(4) # 添加元素
print(f"添加元素后: {my_set}, ID: {id(my_set)}")
my_set.remove(1) # 删除元素
print(f"删除元素后: {my_set}, ID: {id(my_set)}")此外,像
bytearray也是可变类型。自定义的类实例,除非你特别设计成不可变,否则它们通常也是可变的,因为你可以随时修改它们的属性。理解这些,在构建复杂数据结构时,能避免很多坑。
Python中常见的不可变对象类型有哪些?
与可变对象相对,不可变对象一旦创建,其值就无法改变。这听起来可能有点限制,但实际上,这种特性带来了稳定性和可预测性。最典型的不可变对象包括数字(
int,
float,
complex)、字符串(
str)、元组(
tuple)和冻结集合(
frozenset)。
数字类型,例如整数和浮点数,是不可变的。当你写
x = 5,然后
x = x + 1时,你并不是改变了
5这个值本身,而是创建了一个新的整数对象
6,然后让变量
x指向了它。
x = 5
print(f"原始整数: {x}, ID: {id(x)}")
x = x + 1 # 实际上是创建了一个新的整数对象 6
print(f"加1后: {x}, ID: {id(x)}") # ID会改变字符串(
str)是另一个非常重要的不可变类型。在我看来,字符串的不可变性是Python设计中一个非常优雅的地方。它使得字符串操作更安全,尤其是在多线程环境下,你不用担心某个线程意外修改了另一个线程正在使用的字符串。当你进行字符串拼接时,比如
s = "hello" + " world",实际上是创建了一个新的字符串
"hello world"。
s = "hello"
print(f"原始字符串: {s}, ID: {id(s)}")
s += " world" # 实际上是创建了一个新的字符串对象
print(f"拼接后: {s}, ID: {id(s)}") # ID会改变元组(
tuple)是不可变序列。一旦创建,你不能添加、删除或修改其内部元素。如果元组中包含可变对象,比如一个列表,那么这个列表本身还是可以被修改的,但元组中指向这个列表的引用是不可变的。这有点像一个只读的指针,指向的内容可以变,但指针本身不能指向别的地方。
本文档主要讲述的是Python之模块学习;python是由一系列的模块组成的,每个模块就是一个py为后缀的文件,同时模块也是一个命名空间,从而避免了变量名称冲突的问题。模块我们就可以理解为lib库,如果需要使用某个模块中的函数或对象,则要导入这个模块才可以使用,除了系统默认的模块(内置函数)不需要导入外。希望本文档会给有需要的朋友带来帮助;感兴趣的朋友可以过来看看
my_tuple = (1, 2, [3, 4])
print(f"原始元组: {my_tuple}, ID: {id(my_tuple)}")
# my_tuple[0] = 10 # 这会报错,因为元组不可变
# print(f"尝试修改元素: {my_tuple}, ID: {id(my_tuple)}")
my_tuple[2].append(5) # 元组中的列表是可变的,所以可以修改
print(f"修改元组内列表后: {my_tuple}, ID: {id(my_tuple)}") # 元组ID不变冻结集合(
frozenset)是不可变的集合版本,可以作为字典的键。字节串(
bytes)也是不可变的。理解这些,有助于我们更好地选择数据结构,并预测代码的行为。
为什么Python要区分可变与不可变对象?
Python区分可变与不可变对象,绝不是为了增加复杂性,而是出于多方面的设计考量,这些考量最终提升了语言的实用性、安全性和性能。我个人认为,这体现了Python在灵活性和健壮性之间寻求平衡的智慧。
首先,哈希性(Hashability) 是一个非常重要的原因。不可变对象通常是“可哈希的”(hashable),这意味着它们有一个固定的哈希值,这个哈希值在对象的生命周期内不会改变。这使得不可变对象能够被用作字典的键(
dictkeys)和集合的元素(
setelements)。想象一下,如果一个字典的键是可变的,那么键的值改变了,它在哈希表中的位置也会变,这会彻底破坏字典的查找机制。而可变对象(如列表、字典)因为其内容可能变化,所以默认是不可哈希的,不能直接作为字典键或集合元素。
其次,数据完整性和安全性。不可变对象一旦创建就不能被修改,这在很多场景下非常有用。比如,当你将一个不可变对象传递给函数时,你可以确信函数不会意外地修改原始对象。这减少了副作用(side effects)的发生,使得代码更容易理解和调试。在并发编程中,虽然Python的GIL(全局解释器锁)限制了多线程的并行执行,但在多进程或异步编程中,不可变数据结构能有效避免竞态条件(race conditions),因为它们本身就是线程安全的。
再者,性能优化。Python解释器可以对不可变对象进行一些内部优化。例如,对于小整数(-5到256)和某些短字符串,Python会进行“interning”(驻留),即在内存中只保留一份副本。当你创建多个相同值的不可变对象时,它们可能都指向内存中的同一个对象,这节省了内存。
a = 1 b = 1 print(id(a) == id(b)) # True,因为小整数被驻留 s1 = "hello" s2 = "hello" print(id(s1) == id(s2)) # True,短字符串也被驻留 l1 = [1, 2] l2 = [1, 2] print(id(l1) == id(l2)) # False,列表是可变的,即使内容相同也是不同对象
最后,函数式编程范式。不可变性是函数式编程的核心原则之一。它鼓励我们编写没有副作用的函数,即函数只接收输入,产生输出,而不修改任何外部状态。这使得代码更模块化,更容易测试和组合。虽然Python不是纯粹的函数式语言,但不可变对象的存在,无疑为追求这种风格的开发者提供了便利。
在我看来,这种区分是Python设计哲学的一个缩影:提供强大的工具,同时也要求开发者理解这些工具的特性,以便更好地驾驭它们。忽略可变性与不可变性的差异,可能会导致一些难以追踪的bug,比如在默认参数中使用可变对象。
如何判断一个对象是可变还是不可变?
在Python中,判断一个对象是可变还是不可变,通常有几种方法,有些是经验性的,有些则是通过内置函数进行验证。这不是一个“一刀切”的检查,更像是结合对Python数据类型的理解和一些小技巧。
最直接的方法,也是我个人最常用的,就是尝试修改它。 如果你能成功地在原地修改对象的内容,并且
id()函数返回的内存地址保持不变,那么它就是可变的。
my_list = [1, 2]
print(f"修改前ID: {id(my_list)}")
my_list.append(3)
print(f"修改后ID: {id(my_list)}") # ID通常不变,列表可变如果尝试修改会抛出错误(比如
TypeError: 'str' object does not support item assignment),或者修改操作实际上是返回了一个新对象(导致
id()发生变化),那么它就是不可变的。
my_string = "hello"
print(f"修改前ID: {id(my_string)}")
# my_string[0] = 'H' # 这会报错
my_string = my_string + " world" # 这会创建新对象
print(f"修改后ID: {id(my_string)}") # ID会变,字符串不可变另一个非常重要的判断依据是哈希性(Hashability)。可哈希的对象通常是不可变的。你可以使用内置的
hash()函数来检查一个对象是否可哈希。如果
hash()函数能成功返回一个整数值,那么这个对象就是可哈希的,通常意味着它是不可变的(自定义类可以通过实现
__hash__和
__eq__方法来变得可哈希)。如果
hash()抛出
TypeError,那么这个对象就是不可哈希的,通常是可变的。
print(f"整数1的哈希值: {hash(1)}") # 可哈希
print(f"字符串'abc'的哈希值: {hash('abc')}") # 可哈希
print(f"元组(1, 2)的哈希值: {hash((1, 2))}") # 可哈希
try:
hash([1, 2])
except TypeError as e:
print(f"列表不可哈希: {e}") # 列表不可哈希
try:
hash({'a': 1})
except TypeError as e:
print(f"字典不可哈希: {e}") # 字典不可哈希最后,查阅Python文档或经验。对于内置类型,记住哪些是可变、哪些是不可变,是最直接有效的方法。比如:
-
不可变:
int
,float
,complex
,str
,tuple
,frozenset
,bytes
-
可变:
list
,dict
,set
,bytearray
对于自定义对象,它们默认是可变的,除非你特意通过一些技巧(比如使用
__slots__并避免设置可变属性)来使其行为更像不可变对象。理解这些判断方法,能帮助我们更好地预测代码行为,并避免一些常见的陷阱。









