一道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有非常明確的解答了,推薦閱讀: