SICP Python 描述 2.3 序列

2.3 序列

來源:2.3 Sequenceshtml

譯者:飛龍python

協議:CC BY-NC-SA 4.0git

序列是數據值的順序容器。不像偶對只有兩個元素,序列能夠擁有任意(可是有限)個有序元素。程序員

序列在計算機科學中是強大而基本的抽象。例如,若是咱們使用序列,咱們就能夠列出伯克利的每一個學生,或者世界上的每所大學,或者每所大學中的每一個學生。咱們能夠列出上過的每一門課,提交的每一個做業,或者獲得的每一個成績。序列抽象讓數千個數據驅動的程序影響着咱們天天的生活。github

序列不是特定的抽象數據類型,而是不一樣類型共有的一組行爲。也就是說,它們是許多序列種類,可是都有必定的屬性。特別地,express

長度。序列擁有有限的長度。編程

元素選擇。序列的每一個元素都擁有相應的非負整數做爲下標,它小於序列長度,以第一個元素的 0 開始。數據結構

不像抽象數據類型,咱們並無闡述如何構造序列。序列抽象是一組行爲,它們並無徹底指定類型(例如,使用構造器和選擇器),可是能夠在多種類型中共享。序列提供了一個抽象層級,將特定程序如何操做序列類型的細節隱藏。編程語言

這一節中,咱們開發了一個特定的抽象數據類型,它能夠實現序列抽象。咱們以後介紹實現相同抽象的 Python 內建類型。分佈式

2.3.1 嵌套偶對

對於有理數,咱們使用二元組將兩個整數對象配對,以後展現了咱們能夠一樣經過函數來實現偶對。這種狀況下,每一個咱們構造的偶對的元素都是整數。然而,就像表達式,元組能夠嵌套。每一個偶對的元素自己也能夠是偶對,這個特性在實現偶對的任意一個方法,元組或調度函數中都有效。

可視化偶對的一個標準方法 -- 這裏也就是偶對(1,2) -- 叫作盒子和指針記號。每一個值,複合或原始,都描述爲指向盒子的指針。原始值的盒子只包含那個值的表示。例如,數值的盒子只包含數字。偶對的盒子其實是兩個盒子:左邊的部分(箭頭指向的)包含偶對的第一個元素,右邊的部分包含第二個。

嵌套元素的 Python 表達式:

>>> ((1, 2), (3, 4))
((1, 2), (3, 4))

具備下面的結構:

使用元組做爲其它元組元素的能力,提供了咱們編程語言中的一個新的組合手段。咱們將這種將元組以這種方式嵌套的能力叫作元組數據類型的封閉性。一般,若是組合結果本身可使用相同的方式組合,組合數據值的方式就知足封閉性。封閉性在任何組合手段中都是核心能力,由於它容許咱們建立層次數據結構 -- 結構由多個部分組成,它們本身也由多個部分組成,以此類推。咱們在第三章會探索一些層次結構。如今,咱們考慮一個特定的重要結構。

2.3.2 遞歸列表

咱們可使用嵌套偶對來構建任意長度的元素列表,它讓咱們可以實現抽象序列。下面的圖展現了四元素列表1, 2, 3, 4的遞歸表示:

這個列表由一系列偶對錶示。每一個偶對的第一個元素是列表中的元素,而第二個元素是用於表示列表其他部分的偶對。最後一個偶對的第二個元素是None,它代表列表到末尾了。咱們可使用嵌套的元組字面值來構造這個結構:

>>> (1, (2, (3, (4, None))))
(1, (2, (3, (4, None))))

這個嵌套的結構一般對應了一種很是實用的序列思考方式,咱們在 Python 解釋器的執行規則中已經見過它了。一個非空序列能夠劃分爲:

  • 它的第一個元素,以及

  • 序列的其他部分。

序列的其他部分自己就是一個(可能爲空的)序列。咱們將序列的這種見解叫作遞歸,由於序列包含其它序列做爲第二個組成部分。

因爲咱們的列表表示是遞歸的,咱們在實現中叫它rlist,以便不會和 Python 內建的list類型混淆,咱們會稍後在這一章介紹它。一個遞歸列表能夠由第一個元素和列表的剩餘部分構造。None值表示空的遞歸列表。

>>> empty_rlist = None
>>> def make_rlist(first, rest):
        """Make a recursive list from its first element and the rest."""
        return (first, rest)
>>> def first(s):
        """Return the first element of a recursive list s."""
        return s[0]
>>> def rest(s):
        """Return the rest of the elements of a recursive list s."""
        return s[1]

這兩個選擇器和一個構造器,以及一個常量共同實現了抽象數據類型的遞歸列表。遞歸列表惟一的行爲條件是,就像偶對那樣,它的構造器和選擇器是相反的函數。

  • 若是一個遞歸列表s由元素f和列表r構造,那麼first(s)返回f,而且rest(s)返回r

咱們可使用構造器和選擇器來操做遞歸列表。

>>> counts = make_rlist(1, make_rlist(2, make_rlist(3, make_rlist(4, empty_rlist))))
>>> first(counts)
1
>>> rest(counts)
(2, (3, (4, None)))

遞歸列表能夠按序儲存元素序列,可是它尚未實現序列的抽象。使用咱們已經定義的數據類型抽象,咱們就能夠實現描述兩個序列的行爲:長度和元素選擇。

>>> def len_rlist(s):
        """Return the length of recursive list s."""
        length = 0
        while s != empty_rlist:
            s, length = rest(s), length + 1
        return length
>>> def getitem_rlist(s, i):
        """Return the element at index i of recursive list s."""
        while i > 0:
            s, i = rest(s), i - 1
        return first(s)

如今,咱們能夠將遞歸列表用做序列了:

>>> len_rlist(counts)
4
>>> getitem_rlist(counts, 1)  # The second item has index 1
2

兩個實現都是可迭代的。它們隔離了嵌套偶對的每一個層級,直到列表的末尾(在len_rlist中),或者到達了想要的元素(在getitem_rlist中)。

下面的一系列環境圖示展現了迭代過程,getitem_rlist經過它找到了遞歸列表中下標1中的元素2

while頭部中的表達式求值爲真,這會致使while語句組中的賦值語句被執行:

這裏,局部名稱s如今指向以原列表第二個元素開始的子列表。如今,while頭中的表達式求值爲假,因而 Python 會求出getitem_rlist最後一行中返回語句中的表達式。

最後的環境圖示展現了調用first的局部幀,它包含綁定到相同子列表的sfirst函數挑選出值2並返回了它,完成了getitem_rlist的調用。

這個例子演示了遞歸列表計算的常見模式,其中迭代的每一步都操做原列表的一個逐漸變短的後綴。尋找遞歸列表的長度和元素的漸進式處理過程須要一些時間來計算。(第三章中,咱們會學會描述這種函數的計算時間。)Python 的內建序列類型以不一樣方式實現,它對於計算序列長度和獲取元素並不具備大量的計算開銷。

2.3.2 元組 II

實際上,咱們引入用於造成原始偶對的tuple類型自己就是完整的序列類型。元組比起咱們以函數式實現的偶對抽象數據結構,本質上提供了更多功能。

元組具備任意的長度,而且也擁有序列抽象的兩個基本行爲:長度和元素選擇。下面的digits是一個四元素元組。

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

此外,元素能夠彼此相加以及與整數相乘。對於元組,加法和乘法操做並不對元素相加或相乘,而是組合和重複元組自己。也就是說,operator模塊中的add函數(以及+運算符)返回兩個被加參數鏈接成的新元組。operator模塊中的mul函數(以及*運算符)接受整數k和元組,並返回含有元組參數k個副本的新元組。

>>> (2, 7) + digits * 2
(2, 7, 1, 8, 2, 8, 1, 8, 2, 8)

映射。將一個元組變換爲另外一個元組的強大手段是在每一個元素上調用函數,並收集結果。這一計算的經常使用形式叫作在序列上映射函數,對應內建函數mapmap的結果是一個自己不是序列的對象,可是能夠經過調用tuple來轉換爲序列。它是元組的構造器。

>>> alternates = (-1, 2, -3, 4, -5)
>>> tuple(map(abs, alternates))
(1, 2, 3, 4, 5)

map函數很是重要,由於它依賴於序列抽象:咱們不須要關心底層元組的結構,只須要可以獨立訪問每一個元素,以便將它做爲參數傳入用於映射的函數中(這裏是abs)。

2.3.4 序列迭代

映射自己就是通用計算模式的一個實例:在序列中迭代全部元素。爲了在序列上映射函數,咱們不只僅須要選擇特定的元素,還要依次選擇每一個元素。這個模式很是廣泛,Python 擁有額外的控制語句來處理序列數據:for語句。

考慮一個問題,計算一個值在序列中出現了多少次。咱們可使用while循環實現一個函數來計算這個數量。

>>> 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語句能夠經過直接迭代元素值來簡化這個函數體,徹底不須要引入index。例如(原文是For example,爲雙關語),咱們能夠寫成:

>>> 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語句按照如下過程來執行:

  1. 求出頭部表達式<expression>,它必須產生一個可迭代的值。

  2. 對於序列中的每一個元素值,按順序:

    1. 在局部環境中將變量名<name>綁定到這個值上。

    2. 執行語句組<suite>

步驟 1 引用了可迭代的值。序列是可迭代的,它們的元素可看作迭代的順序。Python 的確擁有其餘可迭代類型,可是咱們如今只關注序列。術語「可迭代對象」的通常定義會在第四章的迭代器一節中出現。

這個求值過程的一個重要結果是,在for語句執行完畢以後,<name>會綁定到序列的最後一個元素上。這個for循環引入了另外一種方式,其中局部環境能夠由語句來更新。

序列解構。程序中的一個常見模式是,序列的元素自己就是序列,可是具備固定的長度。for語句可在頭部中包含多個名稱,將每一個元素序列「解構」爲各個元素。例如,咱們擁有一個偶對(也就是二元組)的序列:

>>> pairs = ((1, 2), (2, 2), (2, 3), (4, 4))

下面的for語句的頭部帶有兩個名詞,會將每一個名稱xy分別綁定到每一個偶對的第一個和第二個元素上。

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

這個綁定多個名稱到定長序列中多個值的模式,叫作序列解構。它的模式和咱們在賦值語句中看到的,將多個名稱綁定到多個值的模式相同。

範圍。range是另外一種 Python 的內建序列類型,它表示一個整數範圍。範圍可使用range函數來建立,它接受兩個整數參數:所得範圍的第一個數值和最後一個數值加一。

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

在範圍上調用tuple構造器會建立與範圍具備相同元素的元組,使元素易於查看。

>>> tuple(range(5, 8))
(5, 6, 7)

若是隻提供了一個元素,它會解釋爲最後一個數值加一,範圍開始於 0。

>>> total = 0
>>> for k in range(5, 8):
        total = total + k
>>> total
18

常見的慣例是將單下劃線字符用於for頭部,若是這個名稱在語句組中不會使用。

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

Go Bears!
Go Bears!
Go Bears!

要注意對解釋器來講,下劃線只是另外一個名稱,可是在程序員中具備固定含義,它代表這個名稱不該出如今任何表達式中。

2.3.5 序列抽象

咱們已經介紹了兩種原生數據類型,它們實現了序列抽象:元組和範圍。兩個都知足這一章開始時的條件:長度和元素選擇。Python 還包含了兩種序列類型的行爲,它們擴展了序列抽象。

成員性。能夠測試一個值在序列中的成員性。Python 擁有兩個操做符innot in,取決於元素是否在序列中出現而求值爲TrueFalse

>>> digits
(1, 8, 2, 8)
>>> 2 in digits
True
>>> 1828 not in digits
True

全部序列都有叫作indexcount的方法,它會返回序列中某個值的下標(或者數量)。

切片。序列包含其中的子序列。咱們在開發咱們的嵌套偶對實現時觀察到了這一點,它將序列切分爲它的第一個元素和其他部分。序列的切片是原序列的任何部分,由一對整數指定。就像range構造器那樣,第一個整數表示切片的起始下標,第二個表示結束下標加一。

Python 中,序列切片的表示相似於元素選擇,使用方括號。冒號分割了起始和結束下標。任何邊界上的省略都被看成極限值:起始下標爲 0,結束下標是序列長度。

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

Python 序列抽象的這些額外行爲的枚舉,給咱們了一個機會來反思數據抽象一般由什麼構成。抽象的豐富性(也就是說它包含行爲的多少)很是重要。對於使用抽象的用戶,額外的行爲頗有幫助,另外一方面,知足新類型抽象的豐富需求是個挑戰。爲了確保咱們的遞歸列表實現支持這些額外的行爲,須要一些工做量。另外一個抽象豐富性的負面結果是,它們須要用戶長時間學習。

序列擁有豐富的抽象,由於它們在計算中無處不在,因此學習一些複雜的行爲是合理的。一般,多數用戶定義的抽象應該儘量簡單。

擴展閱讀。切片符號接受不少特殊狀況,例如負的起始值,結束值和步長。Dive Into Python 3 中有一節叫作列表切片,完整描述了它。這一章中,咱們只會用到上面描述的基本特性。

2.3.6 字符串

文本值可能比數值對計算機科學來講更基本。做爲一個例子,Python 程序以文本編寫和儲存。Python 中原生的文本數據類型叫作字符串,相應的構造器是str

關於字符串在 Python 中如何表示和操做有許多細節。字符串是豐富抽象的另外一個示例,程序員須要知足一些實質性要求來掌握。這一節是字符串基本行爲的摘要。

字符串字面值能夠表達任意文本,被單引號或者雙引號包圍。

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

咱們已經在代碼中見過字符串了,在print的調用中做爲文檔字符串,以及在assert語句中做爲錯誤信息。

字符串知足兩個基本的序列條件,咱們在這一節開始介紹過它們:它們擁有長度而且支持元素選擇。

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

字符串的元素自己就是包含單一字符的字符串。字符是字母表中的任意單一字符,標點符號,或者其它符號。不像許多其它編程語言那樣,Python 沒有單獨的字符類型,任何文本都是字符串,表示單一字符的字符串長度爲 一、

就像元組,字符串能夠經過加法和乘法來組合:

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

字符串的行爲不一樣於 Python 中其它序列類型。字符串抽象沒有實現咱們爲元組和範圍描述的完整序列抽象。特別地,字符串上實現了成員性運算符in,可是與序列上的實現具備徹底不一樣的行爲。它匹配子字符串而不是元素。

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

與之類似,字符串上的countindex方法接受子串做爲參數,而不是單一字符。count的行爲有細微差異,它統計字符串中非重疊字串的出現次數。

>>> 'Mississippi'.count('i')
4
>>> 'Mississippi'.count('issi')
1

多行文本。字符串並不限制於單行文本,三個引號分隔的字符串字面值能夠跨越多行。咱們已經在文檔字符串中使用了三個引號。

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

在上面的打印結果中,\n(叫作「反斜槓加 n」)是表示新行的單一元素。雖然它表示爲兩個字符(反斜槓和 n)。它在長度和元素選擇上被認爲是單個字符。

字符串強制。字符串能夠從 Python 的任何對象經過以某個對象值做爲參數調用str構造函數來建立,這個字符串的特性對於從多種類型的對象中構造描述性字符串很是實用。

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

str函數能夠以任何類型的參數調用,並返回合適的值,這個機制是後面的泛用函數的主題。

方法。字符串在 Python 中的行爲很是具備生產力,由於大量的方法都返回字符串的變體或者搜索其內容。一部分這些方法由下面的示例介紹。

>>> '1234'.isnumeric()
True
>>> 'rOBERT dE nIRO'.swapcase()
'Robert De Niro'
>>> 'snakeyes'.upper().endswith('YES')
True

擴展閱讀。計算機中的文本編碼是個複雜的話題。這一章中,咱們會移走字符串如何表示的細節,可是,對許多應用來講,字符串如何由計算機編碼的特定細節是必要的知識。Dive Into Python 3 的 4.1 ~ 4.3 節提供了字符編碼和 Unicode 的描述。

2.3.7 接口約定

在複合數據的處理中,咱們強調了數據抽象如何讓咱們設計程序而不陷入數據表示的細節,以及抽象如何爲咱們保留靈活性來嘗試備用表示。這一節中,咱們引入了另外一種強大的設計原則來處理數據結構 -- 接口約定的用法。

接口約定使在許多組件模塊中共享的數據格式,它能夠混合和匹配來展現數據。例如,若是咱們擁有多個函數,它們所有接受序列做爲參數而且返回序列值,咱們就能夠把它們每個用於上一個的輸出上,並選擇任意一種順序。這樣,咱們就能夠經過將函數連接成流水線,來建立一個複雜的過程,每一個函數都是簡單而專注的。

這一節有兩個目的,來介紹以接口約定組織程序的概念,以及展現模塊化序列處理的示例。

考慮下面兩個問題,它們首次出現,而且只和序列的使用相關。

  1. 對前n個斐波那契數中的偶數求和。

  2. 列出一個名稱中的全部縮寫字母,它包含每一個大寫單詞的首字母。

這些問題是有關係的,由於它們能夠解構爲簡單的操做,它們接受序列做爲輸入,併產出序列做爲輸出。並且,這些操做是序列上的計算的通常方法的實例。讓咱們思考第一個問題,它能夠解構爲下面的步驟:

enumerate     map    filter  accumulate
-----------    ---    ------  ----------
naturals(n)    fib    iseven     sum

下面的fib函數計算了斐波那契數(如今使用了for語句更新了第一章中的定義)。

>>> def fib(k):
        """Compute the kth Fibonacci number."""
        prev, curr = 1, 0  # curr is the first Fibonacci number.
        for _ in range(k - 1):
             prev, curr = curr, prev + curr
        return curr

謂詞iseven可使用整數取餘運算符%來定義。

>>> def iseven(n):
        return n % 2 == 0

mapfilter函數是序列操做,咱們已經見過了map,它在序列中的每一個元素上調用函數而且收集結果。filter函數接受序列,而且返回序列中謂詞爲真的元素。兩個函數都返回間接對象,mapfilter對象,它們是能夠轉換爲元組或求和的可迭代對象。

>>> nums = (5, 6, -7, -8, 9)
>>> tuple(filter(iseven, nums))
(6, -8)
>>> sum(map(abs, nums))
35

如今咱們能夠實現even_fib,第一個問題的解,使用mapfiltersum

>>> def sum_even_fibs(n):
        """Sum the first n even Fibonacci numbers."""
        return sum(filter(iseven, map(fib, range(1, n+1))))
>>> sum_even_fibs(20)
3382

如今,讓咱們思考第二個問題。它能夠解構爲序列操做的流水線,包含mapfilter

enumerate  filter   map   accumulate
---------  ------  -----  ----------
  words    iscap   first    tuple

字符串中的單詞能夠經過字符串對象上的split方法來枚舉,默認以空格分割。

>>> tuple('Spaces between words'.split())
('Spaces', 'between', 'words')

單詞的第一個字母可使用選擇運算符來獲取,肯定一個單詞是否大寫的謂詞可使用內建謂詞isupper定義。

>>> def first(s):
        return s[0]
>>> def iscap(s):
        return len(s) > 0 and s[0].isupper()\

這裏,咱們的縮寫函數可使用mapfilter定義。

>>> def acronym(name):
        """Return a tuple of the letters that form the acronym for name."""
        return tuple(map(first, filter(iscap, name.split())))
>>> acronym('University of California Berkeley Undergraduate Graphics Group')
('U', 'C', 'B', 'U', 'G', 'G')

這些不一樣問題的類似解法展現瞭如何使用通用的計算模式,例如映射、過濾和累計,來組合序列的接口約定上的操做。序列抽象讓咱們編寫出這些簡明的解法。

將程序表達爲序列操做有助於咱們設計模塊化的程序。也就是說,咱們的設計由組合相關的獨立片斷構建,每一個片斷都對序列進行轉換。一般,咱們能夠經過提供帶有接口約定的標準組件庫來鼓勵模塊化設計,接口約定以靈活的方式鏈接這些組件。

生成器表達式。Python 語言包含第二個處理序列的途徑,叫作生成器表達式。它提供了與mapreduce類似的功能,可是須要更少的函數定義。

生成器表達式組合了過濾和映射的概念,並集成於單一的表達式中,如下面的形式:

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

爲了求出生成器表達式,Python 先求出<sequence expression>,它必須返回一個可迭代值。以後,對於每一個元素,按順序將元素值綁定到<name>,求出過濾器表達式,若是它產生真值,就會求出映射表達式。

生成器表達式的求解結果值自己是個可迭代值。累計函數,好比tuplesummaxmin能夠將返回的對象做爲參數。

>>> def acronym(name):
        return tuple(w[0] for w in name.split() if iscap(w))
>>> def sum_even_fibs(n):
        return sum(fib(k) for k in range(1, n+1) if fib(k) % 2 == 0)

生成器表達式是使用可迭代(例如序列)接口約定的特化語法。這些表達式包含了mapfilter的大部分功能,可是避免了被調用函數的實際建立(或者,順便也避免了環境幀的建立須要調用這些函數)。

歸約。在咱們的示例中,咱們使用特定的函數來累計結果,例如tuple或者sum。函數式編程語言(包括 Python)包含通用的高階累加器,具備多種名稱。Python 在functools模塊中包含reduce,它對序列中的元素從左到右依次調用二元函數,將序列歸約爲一個值。下面的表達式計算了五個因數的積。

>>> from operator import mul
>>> from functools import reduce
>>> reduce(mul, (1, 2, 3, 4, 5))
120

使用這個更廣泛的累計形式,除了求和以外,咱們也能夠計算斐波那契數列中奇數的積,將序列用做接口約定。

>>> def product_even_fibs(n):
        """Return the product of the first n even Fibonacci numbers, except 0."""
        return reduce(mul, filter(iseven, map(fib, range(2, n+1))))
>>> product_even_fibs(20)
123476336640

mapfilterreduce對應的高階過程的組合會再一次在第四章出現,在咱們思考多臺計算機之間的分佈式計算方法的時候。

相關文章
相關標籤/搜索