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中两者的行为确实不同:

  1. Python中的.extend()就是在原始内存地址上对list进行了扩展,没有改变内存地址,也就不会报错。
  2. += 在不可变对象中调用.__add__()(和+一致);而在可变对象中调用的是.__iadd__()(原地址修改)。
  3. .__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

标签: Python, 编程

知识共享许可协议 作者: 链接:https://byteofbio.com/archives/2.html
本文采用“署名-非商业性使用-相同方式分享 4.0 国际许可协议”进行许可

暂无评论

添加新评论