你知道列表切片的這些"反直覺"操作嗎

導讀

列表絕對是python中最常用的數據結構,沒有之一!

——我自己說的

本文內容不多,5分鐘足矣,但對於大多數pythoners相信會有收穫。


切片熱身

列表的切片操作是指對其中單個或者多個索引對應元素進行的操作,具有如下幾個特點:

  • 切片區間是左閉右開區間
  • 切片的下標可以是負數,當為負數時,意味着從後到前的位置,且-1位倒數第一個
  • 默認步長是1,可通過增加第三個參數實現不同切片
  • 步長是-1時,可實現倒序切片
  • 下標缺省時,表示從最"前"到最"後"(這裡的前後要結合上下文來看,具體後面有示例)

例如,下面這些常規操作大家應該都很熟悉:

lyst = list(range(10))  lyst[1:4] #[1, 2, 3]  lyst[1:4:2] #[1, 3]  lyst[-5:-1] #[5, 6, 7, 8]  lyst[-1:-5:-1] #[9, 8, 7, 6]  

這裡重點補充對於缺省下標的理解,即列表內部是以什麼原則處理缺省下標值:

lyst = list(range(10))  lyst[:2] #[0, 1],起始下標缺省,默認為0,等價於lyst[0:2]  lyst[2:] #[2, 3, 4, 5, 6, 7, 8, 9],終止下標缺省,默認為n=len(lyst),等價於lyst[2:n]  lyst[:] #[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],起始和終止下標均缺省,等價於lyst[0:n]  lyst[:2:-1] #[9, 8, 7, 6, 5, 4, 3],步長為負數時,起始下標缺省,默認為-1,等價於lyst[-1:2:-1]  lyst[2::-1] #[2, 1, 0],步長為負數時,終止下標缺省,默認為-n-1,等價於lyst[2:-n-1:-1]  lyst[::-1] #[9, 8, 7, 6, 5, 4, 3, 2, 1, 0],步長為負數,起始和終止下標均缺省,默認為lyst[-1:-n-1:-1]  

總結來說,就是步長為正數時,首末缺省下標分別是0和n;步長為負時,首末缺省下標分別是-1和-n-1。特別地,當步長為-1、首末下標均缺省時,效果等價於lyst.reverse()或者reversed(lyst),但具體功能有區別:

lyst = list(range(10))  lyst[::-1] #輸出[9, 8, 7, 6, 5, 4, 3, 2, 1, 0],只是輸出逆序結果,lyst本身不變  lyst.reverse() #對列表的inplace操作,無返回值,但執行後lyst變為[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]  reversed(lyst) #lyst列表不變,返回逆序結果,但返回的是一個迭代器對象  

另外,列表中切片索引數值要求均為整數(曾有PEP提議,索引可接受任意值,然後由python進行隱式取整處理,但被reject了),且步長索引不能為0

切片訪問

對列表某索引對應值進行訪問,當對單個索引訪問時,要注意索引的合格範圍;但對列表切片時則不會顯式報錯。其中,單索引的合格範圍為-n—n-1,共2n個合格索引,其中n為列表長度;而對於範圍索引時,即使訪問越界也不會顯式報錯,而僅僅是返回結果為空:

lyst = list(range(10))  lyst[10] #IndexError: list index out of range  lyst[-12] #IndexError: list index out of range  lyst[5:15] #[5, 6, 7, 8, 9]  lyst[10:] #[]  lyst[-12:-1:-1] #[]  

這裡補充一個親身經歷的錯誤:要倒序返回一個列表的前n-1個值(即最後一個元素除外的所有元素倒序輸出),因為可以正負索引混合使用,所以自己想當然的寫下了如下語句:

lyst[-2:-1:-1] #返回[]  

我的邏輯是倒數第二個索引是-2,返回前面的所有值,第一個值是下標0,但由於索引是左開右閉區間,所以如果寫0的話訪問不到,那麼要比0再小一個,也就是-1。但實際上,python可不這麼想,它會將索引-2解釋為倒數第二個值沒錯,但是索引-1會解釋成倒數第一個值(更準確的講,是取不到這個值),所以上面的返回結果為空,無論步長是正還是負!

當然,實現這一需求的方法很多,只要理解了切片的索引原則:

lyst = list(range(10))  lyst[-2::-1] #[8, 7, 6, 5, 4, 3, 2, 1, 0],缺省下標  lyst[:-1][::-1] #[8, 7, 6, 5, 4, 3, 2, 1, 0],先正序訪問前n-1個值再逆序  

切片賦值

前面提到,列表的單索引越界訪問會報錯,切片訪問不報錯但返回結果為空。這一邏輯也類似於列表的賦值操作:對於單索引的賦值,要求索引必須在合格範圍之內,否則報錯;但對於切片的賦值則"無需"考慮索引是否合法,甚至無需考慮賦值長度是否匹配

a = [1,2,3,4,5]  b = [5,6]  a[8] = 8 #IndexError: list assignment index out of range  a[-8] = 8 #IndexError: list assignment index out of range  a[8:] = b #執行後,a為[1, 2, 3, 4, 5, 5, 6]  a[-8:-6] = b #執行後,a為[5, 6, 1, 2, 3, 4, 5],注意這裡限定了賦值區間首末  a[-8:-10] = b #執行後,a也是[5, 6, 1, 2, 3, 4, 5],即便限定的區間實際上為空  a[-8:] = b #執行後,a為[5, 6],因為a的賦值區間未限定長度,而賦值起始索引在a起始之前,所以整體都給覆蓋了  

實際上,由於對超出列表長度的索引位置進行切片賦值會直接拼接,所以這個操作相當於列表的extend():

a = [1,2,3,4,5]  b = [5,6]  a[len(a):] = b # a為[1, 2, 3, 4, 5, 5, 6]  a.extend(b) # a也為[1, 2, 3, 4, 5, 5, 6]  

既然提到了列表的extend()操作,那麼下面的insert()操作不僅不會報錯,而且實際上相當於執行了append()操作:

a = [1,2,3,4,5]  a.insert(len(a), 100) # a為[1, 2, 3, 4, 5, 100],注意這裡insert下標參數為len(a),超出合格範圍,但實際效果等價於a.append(100)  

切片拷貝

由於參數引用的特殊性,python中的賦值操作或許曾令人抓狂其中而不得自拔,個人也不敢說完全理解其中的原理,所以這一部分權當是拋磚引玉。

正因為python中拷貝的特殊性,所以有個專門的庫叫copy,裏面有2個重要的方法分別是copy.copy()和copy.deepcopy(),顧名思義,後者叫做深拷貝,前者自然就叫做淺拷貝。當然,這裡不打算介紹這個庫和相應方法,而只是想就此引出列表中如何通過切片實現拷貝。

如果想要對一個列表進行拷貝,且後續操作互不干擾,那麼簡單的直接賦值是不能完成任務的,例如執行以下語句,a和b其實管理和引用的是同一塊內存,所以操作是同步的,未實現真正的拷貝:

a = [1,2,3,4,5]  b = a #只是a的一個分身,未拷貝  a[0] = 100  b #[100, 2, 3, 4, 5]  

如果想要實現a、b從此毫無瓜葛,那麼簡單的拷貝實現有兩種:

a = [1,2,3,4,5]  b = a[:] #真正實現拷貝  b = list(a) #也可實現拷貝  a[0] = 100  b #[1, 2, 3, 4, 5]  

另外,再體會下這3個例子:

  • 複製列表,改某個值其他不會受到影響
a = [0]*10  a[1] = 1 #[0, 1, 0, 0, 0, 0, 0, 0, 0, 0]  
  • 嵌套複製列表,牽一髮動全身
a = [[0]*10]*2  a[1][0] = 2 #[[2, 0, 0, 0, 0, 0, 0, 0, 0, 0], [2, 0, 0, 0, 0, 0, 0, 0, 0, 0]]  
  • 推導式+複製生成嵌套列表,改1個值其他不受影響
a = [[0]*10 for _ in range(2)]  a[1][0] = 2 #[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [2, 0, 0, 0, 0, 0, 0, 0, 0, 0]]  

不得不說,python裏面的變量賦值與引用確實有些難以理解,這個只能靠不斷積累和嘗試,得細品!