一道Python题
最近有朋友“考”了我一个Python的题:使用+=和.extend()两种方法扩展元组中的列表会发生什么。虽然我对Python中的可变数据类型、不可变数据类型的概念都有较深的理解,并且也对list的+、+=、.extend()、.append()做过性能分析,但是+=和.extend()两者无论在表现(是否为原地址修改)以及性能上都非常近似,所以对两者的区别还没有明确的概念。为了解答这个问题,我我们先直接上代码试验一下:# 创建一个包含列表的元组:
>>> a_tuple = (1, 2, [])
>>> a_tuple[2] += ['a', 'b'] # (1)
Traceback (most recent call last):
File "", line 1, in
a_tuple[2] += ['a', 'b']
TypeError: 'tuple' object does not support item assignment
>>> a_tuple[2].extend(['a', 'b']) # (2)
>>> a_tuple # (3)
(1, 2, ['a', 'b', 'a', 'b'])(1) 通过+=的方法扩展列表出现“元组不支持元素赋值”的报错。
(2) 使用.extend()方法。
(3) 有趣的是,列表被扩展了两次。虽然+=报错,但是却成功修改了列表。
Python中的可变数据类型和不可变数据类型
要解释这个先从Python中的可变数据类型和不可变数据类型谈起。可变数据类型可以在不改变内存地址的情况下对其进行修改。而不可变数据类型只能重新赋值绑定变量,这时变量的内存地址已经发生变化,而原地址的数据在没有被其他变量引用后将被GC(garbage collector)回收:>>> a = 1
>>> id(a) # CPython通过id()查看变量a的内存地址
1942286128
>>> a += 1 # 对变量a进行修改
>>> id(a) # 这时内存地址已经发生变化
1942286160
>>> a_list = [1] # list为可变数据类型
>>> id(a_list)
2170470080648
>>> a_list.append(2)
>>> id(a_list) # 修改后内存地址没有变化
2170470080648
元组不能修改?
学Python时教材里一般都会说元组不能修改,没有.append()、.extend()、.insert()这些方法。没错,元组是不可变数据类型,确实不能修改。但是元组的元素可以是可变数据类型,而元组中保存的实际是可变数据类型的内存地址。所以通过对可变数据类型的修改,元组最终返回的数据是可以变化的。如果了解C语言中“指针”概念的话就很好懂了。
对于list这种可变数据类型,+=和.extend()有什么异同?
还是接上面那个例子:>>> id(a_list)
2170470080648
>>> a_list += [3]
>>> id(a_list) # 通过+=扩展list,内存地址没有变化
2170470080648
>>> a_list.extend([4])
>>> id(a_list) # 通过.extend()扩展list,当然内存地址也不会变化
2170470080648
>>> a_list = a_list + [5] # 会这样写的真是个人才
>>> id(a_list) # 地址发生了变化
2170470080712
这样来说+=和.extend()在修改list时都不会修改地址,那为什么题目中通过这两种方法修改a_tuple中的list会有不同的结果呢?其实Python中两者的行为确实不同:Python中的.extend()就是在原始内存地址上对list进行了扩展,没有改变内存地址,也就不会报错。
+= 在不可变对象中调用.__add__()(和+一致);而在可变对象中调用的是.__iadd__()(原地址修改)。
.__iadd__()实际上已经成功在原地址修改了列表,但是它会对的a_tuple[2]进行重新赋值,而这一步引发了报错,因为元组的元素不能修改。
怎么避免类似的坑?
我认为Tim Peters的《Zen of Python》(Python之禅)里有一句话很经典:There should be one-- and preferably only one --obvious way to do it.
——应当存在一种,而且更应该只有一种最好的解决方案。
所以我的回答是——你基本上不可能记住所有的特例,最简单粗暴的方法就是意识到:当你遇到一个可能的坑,意味着这不是最好的解决方案,那就忘了它,然后记住最好的。在这里就是记住扩展列表用.extend(),忘记+=吧!
附:+、+=、.extend()、.append()的性能分析:import time
def cal_time(func):
def wrapper():
t1 = time.time()
func()
t2 = time.time()
print(t2-t1)
return wrapper
@cal_time
def func_a():
a = []
for x in range(100000):
a = a + [x]
@cal_time
def func_b():
a = []
for x in range(100000):
a += [x]
@cal_time
def func_c():
a = []
for x in range(100000):
a.extend([x])
@cal_time
def func_d():
a = []
for x in range(100000):
a.append(x)
func_a()
func_b()
func_c()
func_d()
Python 3.5.1测试结果:24.90237021446228 # a = a + [x]
0.01898360252380371 # a += [x]
0.02698493003845215 # a.extend([x])
0.013987541198730469 # a.append(x)
参考资料
在总结这篇文章的时候发现其实这个问题早已经在官方文档的FAQ有非常明确的解答了,推荐阅读: