1. 程式人生 > >Python進階:全面解讀高階特性之切片!

Python進階:全面解讀高階特性之切片!

導讀:切片系列文章連續寫了三篇,本文是對它們做的彙總。為什麼要把序列文章合併呢?在此說明一下,本文絕不是簡單地將它們做了合併,主要是修正了一些嚴重的錯誤(如自定義序列切片的部分),還對行文結構與章節銜接做了大量改動,如此一來,本文結構的完整性與內容的質量都得到了很好的保證。

眾所周知,我們可以通過索引值(或稱下標)來查詢序列型別(如字串、列表、元組…)中的單個元素,那麼,如果要獲取一個索引區間的元素該怎麼辦呢?

切片(slice)就是一種擷取索引片段的技術,藉助切片技術,我們可以十分靈活地處理序列型別的物件。通常來說,切片的作用就是擷取序列物件,然而,對於非序列物件,我們是否有辦法做到切片操作呢?在使用切片的過程中,有什麼要點值得重視,又有什麼底層原理值得關注呢?本文將主要跟大家一起來探討這些內容,希望我能與你共同學習進步。

1、切片的基礎用法

列表是 Python 中極為基礎且重要的一種資料結構,也是最能發揮切片的用處的一種資料結構,所以在前兩節,我將以列表為例介紹切片的一些常見用法。

首先是切片的書寫形式:[i : i+n : m] ;其中,i 是切片的起始索引值,為列表首位時可省略;i+n 是切片的結束位置,為列表末位時可省略;m 可以不提供,預設值是1,不允許為0 ,當m為負數時,列表翻轉。注意:這些值都可以大於列表長度,不會報越界。

切片的基本含義是:從序列的第i位索引起,向右取到後n位元素為止,按m間隔過濾

li = [1456
79111416]

# 以下寫法都可以表示整個列表,其中 X >= len(li)
li[0:X] == li[0:] == li[:X] == li[:
== li[::] == li[-X:X] == li[-X:]

li[1:5] == [4,5,6,7# 從1起,取5-1位元素
li[1:5:2] == [4
,6# 從1起,取5-1位元素,按2間隔過濾
li[-1:] == [16# 取倒數第一個元素
li[-4:-2] == [911# 從倒數第四起,取-2-(-4)=2位元素
li[:-2] == li[-len(li):-2
== [1,4,5,6,7,9,11# 從頭開始,取-2-(-len(li))=7位元素

# 步長為負數時,列表先翻轉,再擷取
li[::-1] == [16,14,11,9,7,6,5,4,1# 翻轉整個列表
li[::-2] == [16,11,7,5,1# 翻轉整個列表,再按2間隔過濾
li[:-5:-1] == [16,14,11,9# 翻轉整個列表,取-5-(-len(li))=4位元素
li[:-5:-3] == [16,9# 翻轉整個列表,取-5-(-len(li))=4位元素,再按3間隔過濾

# 切片的步長不可以為0
li[::0]  # 報錯(ValueError: slice step cannot be zero)

上述的某些例子對於初學者(甚至很多老手)來說,可能還不好理解,但是它們都離不開切片的基本語法,所以為方便起見,我將它們也歸入基礎用法中。

對於這些樣例,我個人總結出兩條經驗:

(1)牢牢記住公式[i : i+n : m] ,當出現預設值時,通過想象把公式補全;

(2)索引為負且步長為正時,按倒數計算索引位置;索引為負且步長為負時,先翻轉列表,再按倒數計算索引位置。

2、切片的高階用法

一般而言,切片操作的返回結果是一個新的獨立的序列(PS:也有例外,參見《Python是否支援複製字串呢?》)。以列表為例,列表切片後得到的還是一個列表,佔用新的記憶體地址。

當取出切片的結果時,它是一個獨立物件,因此,可以將其用於賦值操作,也可以用於其它傳遞值的場景。但是,切片只是淺拷貝 ,它拷貝的是原列表中元素的引用,所以,當存在變長物件的元素時,新列表將受制於原列表。

li = [1, 2, 3, 4]
ls = li[::]

li == ls # True
id(li) == id(ls) # False
li.append(li[2:4]) # [1, 2, 3, 4, [3, 4]]
ls.extend(ls[2:4]) # [1, 2, 3, 4, 3, 4]

# 下例等價於判斷li長度是否大於8
if(li[8:]):
    print("not empty")
else:
    print("empty")

# 切片列表受制於原列表
lo = [1,[1,1],2,3]
lp = lo[:2] # [1, [1, 1]]
lo[1].append(1) # [1, [1, 1, 1], 2, 3]
lp # [1, [1, 1, 1]]

由於可見,將切片結果取出,它可以作為獨立物件使用,但是也要注意,是否取出了變長物件的元素。

切片既可以作為獨立物件被“取出”原序列,也可以留在原序列,作為一種佔位符使用。

不久前,我介紹了幾種拼接字串的方法(連結見文末),其中三種格式化類的拼接方法(即 %、format()、template)就是使用了佔位符的思想。對於列表來說,使用切片作為佔位符,同樣能夠實現拼接列表的效果。特別需要注意的是,給切片賦值的必須是可迭代物件。

li = [1, 2, 3, 4]

# 在頭部拼接
li[:0] = [0] # [0, 1, 2, 3, 4]
# 在末尾拼接
li[len(li):] = [5,7] # [0, 1, 2, 3, 4, 5, 7]
# 在中部拼接
li[6:6] = [6] # [0, 1, 2, 3, 4, 5, 6, 7]

# 給切片賦值的必須是可迭代物件
li[-1:-1] = 6 # (報錯,TypeError: can only assign an iterable)
li[:0] = (9,) #  [9, 0, 1, 2, 3, 4, 5, 6, 7]
li[:0] = range(3) #  [0, 1, 2, 9, 0, 1, 2, 3, 4, 5, 6, 7]

上述例子中,若將切片作為獨立物件取出,那你會發現它們都是空列表,即 li[:0]==li[len(li):]==li[6:6]==[] ,我將這種佔位符稱為“純佔位符”,對純佔位符賦值,並不會破壞原有的元素,只會在特定的索引位置中拼接進新的元素。刪除純佔位符時,也不會影響列表中的元素。

與“純佔位符”相對應,“非純佔位符”的切片是非空列表,對它進行操作(賦值與刪除),將會影響原始列表。如果說純佔位符可以實現列表的拼接,那麼,非純佔位符可以實現列表的替換。

li = [1234]

# 不同位置的替換
li[:3] = [7,8,9# [7, 8, 9, 4]
li[3:] = [5,6,7# [7, 8, 9, 5, 6, 7]
li[2:4] = ['a','b'# [7, 8, 'a', 'b', 6, 7]

# 非等長替換
li[2:4] = [1,2,3,4# [7, 8, 1, 2, 3, 4, 6, 7]
li[2:6] = ['a']  # [7, 8, 'a', 6, 7]

# 刪除元素
del li[2:3# [7, 8, 6, 7]

切片佔位符可以帶步長,從而實現連續跨越性的替換或刪除效果。需要注意的是,這種用法只支援等長替換。

li = [123456]

li[::2] = ['a','b','c'] # ['a'2'b'4'c'6]
li[::2] = [0]*3 # [020406]
li[::2] = ['w'] # 報錯,attempt to assign sequence of size 1 to extended slice of size 3

del li[::2] # [246]

3、自定義物件實現切片功能

切片是 Python 中最迷人最強大最 Amazing 的語言特性(幾乎沒有之一),以上兩小節雖然介紹了切片的基礎用法與高階用法,但這些還不足以充分地展露切片的魅力,所以,在接下來的兩章節中,我們將聚焦於它的更高階用法。

前兩節內容都是基於原生的序列型別(如字串、列表、元組……),那麼,我們是否可以定義自己的序列型別並讓它支援切片語法呢?更進一步,我們是否可以自定義其它物件(如字典)並讓它支援切片呢?

3.1、魔術方法:`getitem()`

想要使自定義物件支援切片語法並不難,只需要在定義類的時候給它實現魔術方法 __getitem__() 即可。所以,這裡就先介紹一下這個方法。

語法: object.__getitem__(self, key)

官方文件釋義:Called to implement evaluation of self[key]. For sequence types, the accepted keys should be integers and slice objects. Note that the special interpretation of negative indexes (if the class wishes to emulate a sequence type) is up to the __getitem__() method. If key is of an inappropriate type, TypeError may be raised; if of a value outside the set of indexes for the sequence (after any special interpretation of negative values), IndexError should be raised. For mapping types, if key is missing (not in the container), KeyError should be raised.

概括翻譯一下:__getitem__() 方法用於返回引數 key 所對應的值,這個 key 可以是整型數值和切片物件,並且支援負數索引;如果 key 不是以上兩種型別,就會拋 TypeError;如果索引越界,會拋 IndexError ;如果定義的是對映型別,當 key 引數不是其物件的鍵值時,則會拋 KeyError 。

3.2、自定義序列實現切片功能

接下來,我們定義一個簡單的 MyList ,並給它加上切片功能。(PS:僅作演示,不保證其它功能的完備性)。

import numbers

class MyList():
    def __init__(self, anylist):
        self.data = anylist
    def __len__(self):
        return len(self.data)
    def __getitem__(self, index):
        print("key is : " + str(index))
        cls = type(self)
        if isinstance(index, slice):
            print("data is : " + str(self.data[index]))
            return cls(self.data[index])
        elif isinstance(index, numbers.Integral):
            return self.data[index]
        else:
            msg = "{cls.__name__} indices must be integers"
            raise TypeError(msg.format(cls=cls))

l = MyList(["My""name""is""Python貓"])

### 輸出結果:
key is : 3
Python貓
key is : slice(None2None)
data is : ['My''name']
<__main__.MyList object at 0x0000019CD83A7A90>
key is : hi
Traceback (most recent call last):
...
TypeError: MyList indices must be integers or slices

從輸出結果來看,自定義的 MyList 既支援按索引查詢,也支援切片操作,這正是我們的目的。

3.3、自定義字典實現切片功能

切片是序列型別的特性,所以在上例中,我們不需要寫切片的具體實現邏輯。但是,對於其它非序列型別的自定義物件,就得自己實現切片邏輯。以自定義字典為例(PS:僅作演示,不保證其它功能的完備性):

class MyDict():
    def __init__(self):
        self.data = {}
    def __len__(self):
        return len(self.data)
    def append(self, item):
        self.data[len(self)] = item
    def __getitem__(self, key):
        if isinstance(key, int):
            return self.data[key]
        if isinstance(key, slice):
            slicedkeys = list(self.data.keys())[key]
            return {k: self.data[k] for k in slicedkeys}
        else:
            raise TypeError

d = MyDict()
d.append("My")
d.append("name")
d.append("is")
d.append("Python貓")
print(d[2])
print(d[:2])
print(d[-4:-2])
print(d['hi'])

### 輸出結果:
is
{0'My'1'name'}
{0'My'1'name'}
Traceback (most recent call last):
...
TypeError

上例的關鍵點在於將字典的鍵值取出,並對鍵值的列表做切片處理,其妙處在於,不用擔心索引越界和負數索引,將字典切片轉換成了字典鍵值的切片,最終實現目的。

4、迭代器實現切片功能

好了,介紹完一般的自定義物件如何實現切片功能,這裡將迎來另一類非同一般的物件。

迭代器是 Python 中獨特的一種高階物件,它本身不具備切片功能,然而若能將它用於切片,這便彷彿是錦上添花,能達到如虎添翼的效果。所以,本節將隆重地介紹迭代器如何實現切片功能。

4.1、迭代與迭代器

首先,有幾個基本概念要澄清:迭代、可迭代物件、迭代器。

迭代 是一種遍歷容器型別物件(例如字串、列表、字典等等)的方式,例如,我們說迭代一個字串“abc”,指的就是從左往右依次地、逐個地取出它的全部字元的過程。(PS:漢語中迭代一詞有迴圈反覆、層層遞進的意思,但 Python 中此詞要理解成單向水平線性 的,如果你不熟悉它,我建議直接將其理解為遍歷。)

那麼,怎麼寫出迭代操作的指令呢?最通用的書寫語法就是 for 迴圈。

# for迴圈實現迭代過程
for char in "abc":
    print(char, end=" ")
# 輸出結果:a b c

for 迴圈可以實現迭代的過程,但是,並非所有物件都可以用於 for 迴圈,例如,上例中若將字串“abc”換成任意整型數字,則會報錯: 'int' object is not iterable .

這句報錯中的單詞“iterable”指的是“可迭代的”,即 int 型別不是可迭代的。而字串(string)型別是可迭代的,同樣地,列表、元組、字典等型別,都是可迭代的。

那怎麼判斷一個物件是否可迭代呢?為什麼它們是可迭代的呢?怎麼讓一個物件可迭代呢?

要使一個物件可迭代,就要實現可迭代協議,即需要實現__iter__() 魔術方法,換言之,只要實現了這個魔術方法的物件都是可迭代物件。

那怎麼判斷一個物件是否實現了這個方法呢?除了上述的 for 迴圈外,我還知道四種方法:

# 方法1:dir()檢視__iter__
dir(2)     # 沒有,略
dir("abc"# 有,略

# 方法2:isinstance()判斷