Python中列表的 += 和 .extend() 的异同
一道Python题
最近有朋友“考”了我一个Python的题:使用+=
和.extend()
两种方法扩展元组中的列表会发生什么。虽然我对Python中的可变数据类型、不可变数据类型的概念都有较深的理解,并且也对list的+
、+=
、.extend()
、.append()
做过性能分析,但是+=
和.extend()
两者无论在表现(是否为原地址修改)以及性能上都非常近似,所以对两者的区别还没有明确的概念。为了解答这个问题,我我们先直接上代码试验一下:
# 创建一个包含列表的元组:
>>> a_tuple = (1, 2, [])
>>> a_tuple[2] += ['a', 'b'] # (1)
Traceback (most recent call last):
File "<pyshell#14>", line 1, in <module>
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有非常明确的解答了,推荐阅读:
https://docs.python.org/3/faq/programming.html#faq-augmented-assignment-tuple-error
暂无评论