SICP 第二章 使用數(shù)據(jù)構(gòu)建抽象 2.3 序列

文檔:2.3 Sequence

參考:cs61a.org/spring2018


2.3 序列

序列是數(shù)據(jù)值的順序容器。序列在計算機(jī)科學(xué)中是強大而基本的抽象。序列不是特定的抽象數(shù)據(jù)類型,而是不同類型共有的一組行為。也就是說,它們是許多序列種類,但是都有一定的屬性。以下為特別屬性:

長度。序列擁有有限的長度。空的序列長度為0。
元素選擇。序列的每個元素都擁有相應(yīng)的非負(fù)整數(shù)作為下標(biāo),它小于序列長度,以第一個元素的 0 開始。
Python中有各種序列的數(shù)據(jù)類型,其中最重要的一種是list列表。

2.3.1 列表

列表值是可以任意長度的序列。列表有大量的內(nèi)置行為,并帶有表達(dá)這些行為的特定的語法。我們已經(jīng)介紹過list literal,它計算了一個列表實例,以及元素選擇表達(dá)式。內(nèi)置的len函數(shù)返回序列的長度。下面,'digits'是一個有四個元素的列表。序號索引3中的元素是8。

>>> digits = [1, 8, 2, 8]
>>> len(digits)
4
>>> digits[3]
8

此外,列表可以相加或者于整數(shù)做乘法。對于序列,加法或乘法不會增添元素,而是組合或者復(fù)制序列本身。也就是說,運算符模塊中的add函數(shù)生成了一個列表,它連接了add的參數(shù)。操作符中的mul函數(shù)可以使用一個列表和一個整數(shù)k來返回包含k次重復(fù)項的原始列表。

>>> [2, 7] + digits * 2
[2, 7, 1, 8, 2, 8, 1, 8, 2, 8]

任何值都可以包含在列表中,包括另一個列表。元素選擇可以多次應(yīng)用于,在包含列表的列表中選擇深層嵌套的元素。

>>> pairs = [[10, 20], [30, 40]]
>>> pairs[1]
[30, 40]
>>> pairs[1][0]
30

2.3.2 序列迭代

在大多數(shù)情況下,我們希望能遍歷序列中的元素,并依次為每個元素執(zhí)行一些計算。這種模式非常常見,Python有一個額外的控制語句來處理順序數(shù)據(jù):'for'語句。
現(xiàn)在我們需要考慮一個值在序列中出現(xiàn)次數(shù)的問題。我們可以使用'while'循環(huán)來實現(xiàn)一個計數(shù)函數(shù)。

>>> def count(s, value):
        """Count the number of occurrences of value in sequence s."""
        total, index = 0, 0
        while index < len(s):
            if s[index] == value:
                total = total + 1
            index = index + 1
        return total
>>> count(digits, 8)
2

Python 中的'for'語句可以通過直接遍歷元素值來簡化該函數(shù)體,而無需引入名稱索引'index'。

>>> def count(s, value):
        """Count the number of occurrences of value in sequence s."""
        total = 0
        for elem in s:
            if elem == value:
                total = total + 1
        return total
>>> count(digits, 8)
2

一個'for'語句由一個以下形式的簡單語句組成:

for <name> in <expression>:
    <suite>

一個for語句由以下步驟執(zhí)行:

  1. 評估開始的<expression>表達(dá)式,它必須能生成可迭代的值。
  2. 對可迭代值中的每個元素,依次:在當(dāng)前幀中綁定<name>到該值上;執(zhí)行<suite>。

這個執(zhí)行過程是指可迭代的值。列表是一種序列,序列是可迭代的值。它們的元素按順序被依次考慮。Python包含其他可迭代類型,但我們現(xiàn)在將重點聚焦序列;“iterable”這個術(shù)語的定義出現(xiàn)在第4章的迭代器部分。
這個評估過程的一個重要結(jié)果是< name >將被綁定到執(zhí)行語句之后的序列的最后一個元素上。'for'循環(huán)還引入了另一種可以通過語句更新環(huán)境的方法。

序列拆封。程序中一個常見的模式是,有一個元素序列,這些元素本身是序列,但都是固定長度的。'for'語句可能包括在其頭中的多個名稱,以“解壓縮”每個元素序列到其各自的元素中。例如,我們可能有一個兩元素列表的列表。

>>> pairs = [[1, 2], [2, 2], [2, 3], [4, 4]]

我們希望能找到擁有相同第一和第二元素的pair的數(shù)目。

>>> same_count = 0

下面的for語句的header有兩個名稱,它們分別將x和y綁定在每個pair的第一和第二個元素上。

>>> for x, y in pairs:
        if x == y:
            same_count = same_count + 1
>>> same_count
2

這種將多個名稱綁定到固定長度序列中的多個值的模式稱為序列解壓縮;這與我們在賦值語句中看到的綁定多個名稱到多個值的模式相同。

范圍(range)。范圍是Python中另一個內(nèi)置類型的序列,他表示一個整數(shù)范圍。范圍是用范圍創(chuàng)建的,它需要兩個整數(shù)參數(shù):第一個數(shù)字和一個大于所需區(qū)間的數(shù)字。

>>> range(1, 10)  # Includes 1, but not 10
range(1, 10)

在一個范圍內(nèi)調(diào)用列表構(gòu)造函數(shù),以與范圍相同的元素來計算列表,這樣就可以很容易地檢查元素。

>>> list(range(5, 8))
[5, 6, 7]

如果只給出一個參數(shù),那么它將被解釋為從0開始的范圍的最后一個值。

>>> list(range(4))
[0, 1, 2, 3]

范圍通常顯示在一個for語句的開頭,他用來表示該循環(huán)被執(zhí)行的次數(shù):一個常見的約定是如果在循環(huán)體中未使用名稱,則在for語句的開頭中使用單個下劃線字符。

>>> for _ in range(3):
        print('Go Bears!')

Go Bears!
Go Bears!
Go Bears!

這個下劃線在解釋器中是環(huán)境中的另一個名稱,但是它在程序員中有一個約定俗成的說法,它表示這個名稱不會出現(xiàn)在以后任何的表達(dá)式中。

2.3.3 序列處理

序列是復(fù)合數(shù)據(jù)的一種常見形式,整個程序通常圍繞這個抽象進(jìn)行。有序列作為輸入和輸出的模塊組件可以混合和匹配來執(zhí)行數(shù)據(jù)處理。復(fù)雜的組件可以通過鏈接到一個序列處理操作的管道來定義,每一個過程都是簡單而集中的。

列表表達(dá)式。可以通過對序列中每個元素的固定表達(dá)式求值,并在結(jié)果序列中收集結(jié)果值來表示許多序列處理操作。在Python中,列表表達(dá)式是執(zhí)行這種計算的表達(dá)式。

>>> odds = [1, 3, 5, 7, 9]
>>> [x+1 for x in odds]
[2, 4, 6, 8, 10]

上面的關(guān)鍵字不是for語句的一部分,而是列表表達(dá)式的一部分,因為它包含在方括號內(nèi)。子表達(dá)式x + 1被求值,同時x與奇數(shù)的每個元素相結(jié)合,結(jié)果的值被收集到一個列表中。
另一個常見的順序處理操作是選擇滿足某些條件的值的子集。列表表達(dá)式也可以表達(dá)這個模式,例如選擇所有的幾率平均劃分為25的元素。

>>> [x for x in odds if 25 % x == 0]
[1, 5]

列表表達(dá)式的通用形式是:

[<map expression> for <name> in <sequence expression> if <filter expression>]

為了對列表表達(dá)式求值,Python對<序列表達(dá)式>進(jìn)行了求值,它必須返回一個可迭代的值。然后,對于按順序的每個元素,元素值被綁定到< name >,篩選器表達(dá)式被求值,如果它產(chǎn)生一個真實值,則對映射表達(dá)式進(jìn)行評估。將map表達(dá)式的值收集到一個列表中。

聚合。序列處理中的第三個常見模式是將序列中的所有值聚合為單個值。內(nèi)置函數(shù)sum、min和max都是聚合函數(shù)的示例。
通過結(jié)合評估每個元素表達(dá)式的模式,選擇元素的子集和聚合元素,我們可以使用序列處理方法來解決問題。
一個完美數(shù)是一個正整數(shù),等于它的除數(shù)的和。n的除數(shù)是小于n的正整數(shù),除以n。列出n的除數(shù)可以用一個列表表達(dá)式來表示。

>>> def divisors(n):
        return [1] + [x for x in range(2, n) if n % x == 0]
>>> divisors(4)
[1, 2]
>>> divisors(12)
[1, 2, 3, 4, 6]

使用除數(shù),我們可以用另一個列表表達(dá)式來計算從1到1000的所有完美數(shù)。(1通常被認(rèn)為是一個完美數(shù),但它不符合我們對除數(shù)的定義。)

>>> [n for n in range(1, 1000) if sum(divisors(n)) == n]
[6, 28, 496]

我們可以重新用我們對除數(shù)的定義來解決另一個問題:已知矩形面積,求邊長都為整數(shù)的矩形的最小周長。矩形的面積等于高度與寬度的乘積。因此,如果已知面積和高度,我們可以計算矩形的寬度。

>>> def width(area, height):
        assert area % height == 0
        return area // height

矩形的周長等于其邊長之和。

>>> def perimeter(width, height):
        return 2 * width + 2 * height

具有整數(shù)邊長的矩形的高度必須是其面積的除數(shù)。我們可以通過考慮所有高度來計算最小周長。

>>> def minimum_perimeter(area):
        heights = divisors(area)
        perimeters = [perimeter(width(area, h), h) for h in heights]
        return min(perimeters)
>>> area = 80
>>> width(area, 5)
16
>>> perimeter(16, 5)
42
>>> perimeter(10, 8)
36
>>> minimum_perimeter(area)
36
>>> [minimum_perimeter(n) for n in range(1, 10)]
[4, 6, 8, 8, 12, 10, 16, 12, 12]

高階函數(shù)。在序列處理中我們看到的情況常常被應(yīng)用于高階函數(shù)。首先,對一個序列里的每個元素進(jìn)行求值也可以表達(dá)成對每個元素調(diào)用函數(shù)的形式。

>>> def apply_to_all(map_fn, s):
        return [map_fn(x) for x in s]

選擇表達(dá)式為真的元素,然后對其調(diào)用函數(shù)。

>>> def keep_if(filter_fn, s):
        return [x for x in s if filter_fn(x)]

最后,很多種形式的聚合可以被表達(dá)成:反復(fù)地對reduced應(yīng)用二個參數(shù)的函數(shù)。

>>> def reduce(reduce_fn, s, initial):
        reduced = initial
        for x in s:
            reduced = reduce_fn(reduced, x)
        return reduced

例如,reduce可以對一個序列里的所有元素進(jìn)行連乘。用mul作為reduce_fn,1作為initial值,reduce可以被一個序列的元素進(jìn)行連乘。

>>> reduce(mul, [2, 4, 8], 1)
64

我們也可以用高階函數(shù)來找到完美數(shù)字。

>>> def divisors_of(n):
        divides_n = lambda x: n % x == 0
        return [1] + keep_if(divides_n, range(2, n))
>>> divisors_of(12)
[1, 2, 3, 4, 6]
>>> from operator import add
>>> def sum_of_divisors(n):
        return reduce(add, divisors_of(n), 0)
>>> def perfect(n):
        return sum_of_divisors(n) == n
>>> keep_if(perfect, range(1, 1000))
[1, 6, 28, 496]

默認(rèn)名。在計算機(jī)科學(xué)的社區(qū)中,'apply_to_all'更常被命名為'map','keep_if'更常被命名為'filter'。在python中,內(nèi)置函數(shù)'map'和'filter'是不返回列表的函數(shù)的一般化。這些函數(shù)將在第4章討論。上面的定義等同于將list構(gòu)造函數(shù)應(yīng)用于內(nèi)置map和filter調(diào)用的結(jié)果。

>>> apply_to_all = lambda map_fn, s: list(map(map_fn, s))
>>> keep_if = lambda filter_fn, s: list(filter(filter_fn, s))

reduce函數(shù)位于Python標(biāo)準(zhǔn)庫的functools模塊。在這里,參數(shù)initial是可選非必需的。

>>> from functools import reduce
>>> from operator import mul
>>> def product(s):
        return reduce(mul, s)
>>> product([1, 2, 3, 4, 5])
120

在Python程序中,使用列表表達(dá)式比使用高階函數(shù)更為常見,然而這兩種進(jìn)行序列處理的方式使用都非常廣泛。

2.3.4 序列抽象

我們引入了兩種滿足序列抽象的原生數(shù)據(jù)類型:list列表和range范圍。 兩者都滿足使用條件:同時具有長度和元素選擇兩種功能。 Python中包含多于兩個序列類型的行為,它們擴(kuò)展了序列抽象。

Membership成員資格。 一個value值可以用于測試一個序列中的成員資格。 Python中有兩個運算符innot in,它們根據(jù)一個元素是否出現(xiàn)在序列中而做出True或False的判斷。

>>> digits
[1, 8, 2, 8]
>>> 2 in digits
True
>>> 1828 not in digits
True

Slicing切片。序列中包含較小的序列。 一小段序列可以是原始序列的任何連續(xù)跨度,由一對整數(shù)來構(gòu)造規(guī)定。 與range范圍構(gòu)造函數(shù)一樣,第一個數(shù)是切片的起始索引,第二個是超出結(jié)束索引的一個整數(shù)。

在Python中,與元素選擇相似,我們使用方括號表示序列切片,用一個冒號來分隔開始和結(jié)束索引。 任何被忽略的邊界被假定為極端值:起始索引為0,結(jié)束索引為整個序列長度。

>>> digits[0:2]
[1, 8]
>>> digits[1:]
[8, 2, 8]

列舉Python序列抽象的這些額外行為使我們有機(jī)會反思數(shù)據(jù)抽象的一般構(gòu)成。抽象的豐富(即包含多少行為)可以引出很多結(jié)果。對于使用抽象的用戶,其他行為可能會有所幫助。另一方面,用新數(shù)據(jù)類型滿足抽象的要求可能是具有挑戰(zhàn)性的。豐富抽象的另一個負(fù)面后果是用戶學(xué)習(xí)需要更長的時間。

序列具有豐富的抽象,因為它們在計算中非常普遍,所以學(xué)習(xí)一些復(fù)雜的行為是合理的。但通常,大多數(shù)用戶定義的抽象應(yīng)盡可能簡單。

進(jìn)一步閱讀。切片符號容許各種特殊情況,例如負(fù)的起始值,結(jié)束值和步長。完整的描述出現(xiàn)在Dive Into Python 3中的切片列表小節(jié)中。在本章中,我們將僅使用上述基本特征。

2.3.5 字符串

對于計算機(jī)科學(xué)而言,文本值可能比數(shù)字更重要。 比方說,Python程序被編寫并存儲為文本。 Python中文本的本地數(shù)據(jù)類型稱為字符串string,并對應(yīng)于構(gòu)造函數(shù)str

在Python中有很多關(guān)于如何表示,表達(dá)和操縱字符串的細(xì)節(jié)。 字符串是豐富抽象的另一個例子,這需要程序員掌握一定的知識。 本節(jié)僅僅對基本字符串行為進(jìn)行簡要介紹。

字符串文字可以用于表示任意文本,它由單引號或雙引號包圍。

>>> 'I am string!'
'I am string!'
>>> "I've got an apostrophe"
"I've got an apostrophe"
>>> '您好'
'您好'

我們已經(jīng)看到字符串出現(xiàn)在我們的代碼中,比如在print打印時被調(diào)用,以及在assert語句中的錯誤消息中出現(xiàn)。

字符串滿足我們在本節(jié)開頭介紹的序列的兩個基本條件:它擁有長度并支持元素選擇功能。

>>> city = 'Berkeley'
>>> len(city)
8
>>> city[3]
'k'

一個字符串的元素是它本身的情況是該字符串為單字符。 字符可以是字母表元素,標(biāo)點符號或其他符號。 與許多其他編程語言不同,Python沒有單獨的字符類型; 任何文本都是字符串,而表示單個字符的字符串長度為1。

像列表一樣,字符串也可以通過加法和乘法組合。

>>> 'Berkeley' + ', CA'
'Berkeley, CA'
>>> 'Shabu ' * 2
'Shabu Shabu '

Membership成員資格。字符串的行為與Python中的其他序列類型不同。 字符串抽象與list列表和range范圍不同。 比如in適用于字符串,但用法完全不同。 它匹配子串而不是元素。

>>> 'here' in "Where's Waldo?"
True

多行文字。 字符串不限于一行。 三重引號可以分隔跨越多行的字符串文字。 我們已經(jīng)廣泛地使用這個三重引用來處理文檔。

>>> """The Zen of Python
claims, Readability counts.
Read more: import this."""
'The Zen of Python\nclaims, "Readability counts."\nRead more: import this.'

在上面的打印結(jié)果中,\ n(發(fā)音為“backslash en”)是表示新行的單個元素。 盡管它顯示為兩個字符(反斜杠和“n”),但它被認(rèn)為是用于長度和元素選擇的單個字符。

字符串強制轉(zhuǎn)換。 通過以對象值作為參數(shù)調(diào)用str構(gòu)造函數(shù),可以從Python中的任何對象創(chuàng)建一個字符串。 字符串的這一特性對于從各種類型的對象構(gòu)造描述性字符串非常有用。

>>> str(2) + ' is an element of ' + str(digits)
'2 is an element of [1, 8, 2, 8]'

進(jìn)一步閱讀。 在計算機(jī)中編寫文本是一個復(fù)雜的話題。 在本章中,我們將抽象出字符串如何表示的細(xì)節(jié)。 但是,對于許多應(yīng)用程序而言,計算機(jī)如何編碼字符串的具體細(xì)節(jié)是必不可少的知識。 Dive Into Python 3的字符串章節(jié)提供了字符編碼和Unicode的描述。

2.3.6 Trees樹

我們使用列表作為其他列表中元素的方法為我們的編程語言提供了一種新的組合方式。 這種方法被稱為數(shù)據(jù)類型的閉包屬性。 通常,如果組合的結(jié)果本身可以使用相同的方法進(jìn)行組合,則組合數(shù)據(jù)值的方法具有閉包屬性。 閉合是以任何方式進(jìn)行組合的關(guān)鍵,因為它允許我們創(chuàng)建分層結(jié)構(gòu) - 由部分組成的結(jié)構(gòu),它們本身由部分組成,等等。

我們可以使用框和指針表示法在環(huán)境圖中顯示列表。 列表被描述為包含列表元素的相鄰框。 原始值(如數(shù)字,字符串,布爾值和無)出現(xiàn)在元素框中。 復(fù)合值(如函數(shù)值和其他列表)由箭頭表示。

上一節(jié):SICP 第一章 使用數(shù)據(jù)構(gòu)建抽象 2.2 數(shù)據(jù)抽象
下一節(jié):SICP 第二章 使用數(shù)據(jù)構(gòu)建抽象 2.4 序列

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容