Python可以這樣玩(3):Python 序列

首先說明一下,本文章首頁出現的照片,只是預告一下後面關於 Arduino 的課程,與本文無關。所有的電路程式設計,都可以由 Python 達成。

Python 的序列類似 C Basic 語言的一維與多維陣列,但功能要強大許多,使用上也更加靈活。




Python 常用的序列結構有列表、元組、字典、字串、集合等等,大致可以分為有序與無序兩類:其中列表、元組、字串屬於有序序列(有順序的),字典、集合屬於無序序列。前一個章節有討論過列表、字串、集合三個,但是在這裡會更深入的討論。

對於有序序列而言,都會支援雙向索引,第一個元素的索引為 0,第二個元素的索引為 1,依此類推。反向的話,倒數第一個元素的索引為 -1,倒數底二個元素的索引為 -2,依此類推。使用負整數作為索引是 Python 序列的一大特色,熟練之後,可以大幅提升開發效率。

列表(List)與列表推導式

列表在上一章節有簡單介紹過,是 Python 內建重要的可變序列之一,它是包含若干元素的有序連續記憶體空間。形式上,列表的所有元素放在一對中括號裡面,相鄰的元素之間以逗點分開。

Python 中,同一個列表中元素的資料類型可以不一樣,例如可以分別為整數、實數、字串等基本資料類型,或者是列表、元組、字典、集合以及其他自訂類型的物件。下面幾種都是合法的列表物件:




隨堂練習
玩擲骰子遊戲,一次值四個骰子,共擲三次
請問如何用列表表示?
>>> 

列表(List)的建立與刪除

= 直接將列表賦予值給變數,即可建立列表物件,例如:

>>> a_list = [1, 2, 3, 4, 5]
>>> a_list
[1, 2, 3, 4, 5]
>>> b_list = []       # 空值
>>> b_list
[]
>>> 

也可以使用 list() 函數將元組、range 物件、字串、字典、集合等資料類別轉換為列表,請先看下面的例子:




字典比較特殊,如果直接用 list() 轉換,只會轉換鍵,如果要同時轉換鍵:值,則需使用 items()

Python 社群中,習慣將 list() 還有後面很快就會學到的 tuple()set()dict() 等函數稱為 工廠函數,因為這些函數可以生產新的資料型態。

建立列表之後,就可以使用整數作為索引存取其中的元素,其中0代表第一個,前面有說明過:




當不再使用列表時,可使用 del 刪除。

隨堂實作
填空題:
>>> x = [1, 2, 3]
>>> x
_________
>>> del x[1]
>>> x
_________
>>> del x
>>> x
_________

列表常用方法

首先說明一下函數與方法的不同,雖然他們看起來很像,但是呼叫方式卻不同,函數是以 function() 的方式呼叫,方法則是包裝在物件之內的函數,所以是以 object.method() 的方式呼叫。熟悉物件導向程式設計(OOP)的人,應該駕輕就熟。物件裡面會包含兩種東西,一是變數(variable),一是函數(function),物件內的變數稱之為屬性(attribute),物件內的函數稱之為方法(method)

還記得如何使用 help() 來求助嗎? help() 括號裡面的參數可以是一個物件,輸入 help([]),就可以得到所有關於 list 的方法,如下圖:




複製貼上再整理一下,就可以當成我們的教材,刪除不常用的,留下常用的,如下表,這裡出現的都是方法,所以可以看到(reverse() 為例)其呼叫方式為 L.reverse()

>>> help([])
Help on list object:

class list(object)
 |  list() -> new empty list
 |  list(iterable) -> new list initialized from iterable's items
 |  Methods defined here:
 |  append(...)
 |      L.append(object) -> None -- append object to end
 |  clear(...)
 |      L.clear() -> None -- remove all items from L
 |  copy(...)
 |      L.copy() -> list -- a shallow copy of L
 |  count(...)
 |      L.count(value)->integer--return number of occurrences of value
 |  extend(...)
 |      L.extend(iterable) -> None -- extend list by appending elements    
 |      from the iterable
 |  index(...)
 |      L.index(value, [start, [stop]]) -> integer -- return first
 |      index of value.
 |      Raises ValueError if the value is not present.
 |  insert(...)
 |      L.insert(index, object) -- insert object before index
 |  pop(...)
 |      L.pop([index]) -> item -- remove and return item at index
 |      (default last).
 |      Raises IndexError if list is empty or index is out of range.
 |  remove(...)
 |      L.remove(value) -> None -- remove first occurrence of value.
 |      Raises ValueError if the value is not present.
 |  reverse(...)
 |      L.reverse() -- reverse *IN PLACE*
 |  sort(...)
 |      L.sort(key=None, reverse=False) -> None--stable sort *IN PLACE*
 |  -------------------------------------------------------------------

一定要練習看英文的說明,這樣可以快速地得到協助,又可以加強英文能力,一舉兩得。

append()insert()extend()

這三種方法都能增加元素列表物件,請自行參考上面的 help 說明。這三種方法都屬於原地操作,也就是說不會改變列表的位址。下面直接用例子說明用法:

>>> x = [1, 2, 3]
>>> id(x)
1856880275848
>>> x.append(4)
>>> x
[1, 2, 3, 4]
>>> id(x)
1856880275848
>>> x.insert(0, 0)    # 在第一個位置插入 0
>>> x
[0, 1, 2, 3, 4]
>>> id(x)
1856880275848
>>> x.extend([5, 6, 7, 8])
>>> x
[0, 1, 2, 3, 4, 5, 6, 7, 8]
>>> id(x)
1856880275848
>>> 

我們也可以使用運算子 + * 來達到增加列表元素的目的,但這兩個運算子不屬於原地操作,而是返回新的列表,如下:

>>> x = [1, 2, 3]
>>> id(x)
1856880279496
>>> x = x + [4]
>>> x
[1, 2, 3, 4]
>>> id(x)
1856870756488
>>> x = x * 2
>>> x
[1, 2, 3, 4, 1, 2, 3, 4]
>>> id(x)
1856880017544
>>> 

pop()remove()clear()

這三個方法則是用來刪除列表中的元素,其中pop()會刪除並返回指定位置(預設是最後一個)的元素,remove()則刪除列表中第一個與指定值相等的元素,clear()是用來清空列表,這三種方法也屬於原地操作。

實機操作
>>> x = [1, 2, 3, 4, 5, 6, 7]
>>> x.pop()
________
>>> x.pop(0)
________
>>> x.clear()
>>> x
________
>>> x = [1, 2, 1, 1, 2]
>>> x.remove(2)
>>> x
________
>>> del x[3]     # 並不是 list 的方法,為一函數
>>> x
________

count()index()

列表方法 count() 返回列表中指定元素出現的次數,index() 則返回指定元素在列表中首次出現的位置,如果元素不存在則拋出異常。

實機操作
>>> x = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4]
>>> x.count(3)
________
>>> x.count(5)
________
>>> x.index(2)
________
>>> x.index(4)
________
>>> x.index(5)
________
>>> 5 in x
________
>>> 3 in x
________

sort()reverse()

這兩個方法,我們會跟另外兩個函數 sorted()reversed() 做比較,區別方法與函數的不同,以及in-place 與非 in-place 的不同。

實機操作
>>> x = list(range(11)
>>> import random
>>> _______.shuffle(x)
>>> x
___________________________________________________
>>> x.reverse()
>>> x
___________________________________________________
>>> x.sort()
>>> x
___________________________________________________
>>> x.sort(key=str)
>>> x
___________________________________________________

sorted()reversed() 就完全不同了,首先它們是函數而不是list的方法,所以呼叫方式不同,其次它們在排序或逆序之後,會傳回新的序列,不影響原本序列。其中 reversed()更特別,它會傳回一個逆向排序的反覆運算物件,由於傳回的是一個物件,所以我們不會直接看到序列的元素,需要額外處理才看的見:

實機操作
>>> x = list(range(11)
>>> import random
>>> _______.shuffle(x)
>>> x
___________________________________________________
>>> reversed(x)
___________________________________________________
>>> list(reversed(x))
___________________________________________________
>>> sorted(x)
___________________________________________________
>>> sorted(x, key=str)
___________________________________________________
>>> x
___________________________________________________

排序方法中的 key 參數,可以實現更複雜的排序動作,我們用個實際的劇本來練習。假設有兩個隊伍參加線上遊戲,分別是A隊與B對,每隊有三個人,以及每個人的得分:

A 隊:
Bob, 86
Andrew, 95
Mandy, 83
B 隊:
Ruby, 89
Elvis, 91
Peter, 77

我們嘗試使用二維列表來記錄:

實機操作
>>> gameresult = [['Bob', 86, 'A'],
          ['Andrew', 95, 'A'],
          ['Ruby', 89, 'B'],
          ['Elvis', 91, 'B'],
          ['Mandy', 83, 'A'],
          ['Peter', 77, 'B']]
>>> gameresult
[['Bob', 86, 'A'], ['______', 95, 'A'], ['Ruby', ____, '__'], ['______', 91, 'B'], ['Mandy', 83, 'A'], ['______', 77, 'B']]
>>> from operator import itemgetter
>>> sorted(gameresult, key=itemgetter(2))  #按照子列表第三個元素排序
[['Bob', 86, 'A'], ['Andrew', 95, 'A'], ['_____', ___, '__'], ['Ruby', 89, 'B'], ['Elvis', 91, 'B'], ['______', ___, '__']]
>>> sorted(gameresult, key=itemgetter(2,0))#先第三個元素再第一個元素
[['______', ___, '__'], ['Bob', 86, 'A'], ['Mandy', 83, 'A'], ['_______', ___, '__'], ['Peter', 77, 'B'], ['Ruby', 89, 'B']]

問題:先按照隊伍排序(順序不拘),再按照分數高低(由高到低)排序
 (提示 reverse=True 降冪排序)

>>> _______________________________________________________

當然,Python 還有更複雜的排序方式,我們先練習到這。

內建函數對列表的操作

除了列表物件本身的方法之外,很多 Python 的內建函數也可以操作列表,例如 max()min()sum()等函數,這三個之前說明過。而len()則是傳回列表元樹的個數。這四個比較簡單,請自行練習。

接下來說明兩個與列表轉換為元組的相關函數,zip() enumerate()zip()函數重新組合多個列表的元素成為元組,並返回包含這些元組的 zip物件;enumerate() 函數返回包含若干索引和值的反覆運算物件。看說明想必會昏倒,我們直接看例子:

>>> from random import shuffle
>>> x = list(range(11))
>>> y = list(range(11))
>>> shuffle(x)
>>> x
[5, 8, 7, 4, 10, 6, 1, 9, 3, 2, 0]
>>> y
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> zip(x, y)
<zip object at 0x0000024155383C88>
>>> list(zip(x, y))
[(5, 0), (8, 1), (7, 2), (4, 3), (10, 4), (6, 5), (1, 6), (9, 7), (3, 8), (2, 9), (0, 10)]
>>> enumerate(x)
<enumerate object at 0x0000024155370B40>
>>> list(enumerate(x))
[(0, 5), (1, 8), (2, 7), (3, 4), (4, 10), (5, 6), (6, 1), (7, 9), (8, 3), (9, 2), (10, 0)]
>>> 

zip() 把兩個列表中的元素打包成元組物件,enumerate() 則把一個列表的元素與自行加在前面的index 數字打包成元組物件。因為物件無法被看到,所以我們利用 list() 將其轉換成列表之後呈現。至於什麼是元組(tuple),下一個章節就會說明。

map()

接下來要談的是 Python 最強悍的地方,我們會漸漸發現 Python 與其它程式語言的不同之處。在 Python 中,map()reduce()filter() 是函數式程式設計的重要呈現形式。

內建函數 map() 能將一個函數依序作用到序列或反覆運算器物見的每個元素,然後返回 map 物件作為結果,其中每個元素是元序列元素經過該函數處理後的結果,不對原序列作任何修改。

光看敘述我敢保證一定看不懂,不用擔心,我們透過實機操作來了解其意義。

實機操作
>>> x = list(range(5))
>>> x
[0, 1, 2, 3, 4]
>>> map(str, x)
<map object at 0x0000024155382D30>
>>> list(map(str, x))
[_____, _____, _____, _____, _____]
>>> def add5(v):
    return v+5

>>> list(map(add5, x))
[___, ___, ___, ___, ___]
>>> x
[0, 1, 2, 3, 4]
>>> y = list(range(5, 10))
>>> y
[5, 6, 7, 8, 9]
>>> def add(a, b):
    return a+b

>>> list(map(add, x, y))
[___, ___, ___, ___, ___]
>>> list(map(lambda a,b:a+b, x, y))
[___, ___, ___, ___, ___]
>>> [add(a, b) for a, b in zip(x, y)]
[___, ___, ___, ___, ___]
>>>

請注意最後兩行指令,一個使用 lambda 語法,一個使用 for 語法,這就是 Python 沒有最簡、只有更簡的特性。

Lambda 語法直接把 結果=f(參數)” 這樣的函數寫法寫成 “lambda 參數:結果for 語法在這裡則稱為列表推導式,後面會進一步說明。

試著使用學習英文的語法分析方法,分析這兩個指令的執行步驟。

reduce()

標準庫 functools 中的函數 reduce() 會將一個接收2個參數的函數,以遞減的方式從左到右依序作用到一個數列中的所有元素。用白話文說,就是先以第一個和第二個元素作為參數丟給函數執行,執行的結果再和第三個元素當成兩個新的參數丟給函數執行,依此類推,一直到所有元素執行完畢為止。元素會越來越遞減,最後剩下一個結果。

接續上面說明 map() 的例子,繼續往下實作,因為我們會用到前面自定義的 add()函數:

實機操作
>>> from functools import reduce
>>> seq = [1, 2, 3, 4, 5]
>>> reduce(add, seq)
_____
>>> reduce(lambda a, b: a+b, seq)
_____
>>> 

其實就是在計算 1 加到 5 的總和。

filter()

內建函數 filter() 將一個單參數函數作用到序列,並返回該序列中使得該函數的返回值為 True 的所有元素組成的 filter 物件。就像是一個過濾器。

我們在玩線上遊戲的時候通常要取一個別名,如果系統對別名的命名方式規定必須是文字或者是數字的時候,就可以用過濾器把合格的別名挑出來:

實機操作
>>> seq = ['foo', 'xbox360', '!!!', '$_$', 'husky']
>>> def isok(x):
    return x.isalnum()

>>> list(filter(isok, seq))
[__________, __________, __________]
>>> [x for x in seq if x.isalnum()]
['foo', 'xbox360', 'husky']
>>> list(filter(lambda x: x.isalnum(), seq))
['foo', 'xbox360', 'husky']
>>> 

請進一步說明 lambda for 的執行步驟:
Lambda:


For:


列表推導式

前面出現過很多列表推導式的例子,其實在邏輯上它相當於一個迴圈,只是形式更加簡潔,例如:

>>> a_list = [x*x for x in range(10)]
>>> a_list
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> b_list = []
>>> for x in range(10):
    b_list.append(x*x)

>>> b_list
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> 

隨堂測驗 設計黑白棋棋盤
大部分的人應該有玩過黑白棋,沒有玩過應該也有看過
黑白棋的棋盤是一個 8x8 大小的方格盤
通常我們會使用 二維矩陣 來表示
Python 中可以用 二維序列 表示成:

>>> board = [[0,0,0,0,0,0,0,0],
         [0,0,0,0,0,0,0,0],
         [0,0,0,0,0,0,0,0],
         [0,0,0,0,0,0,0,0],
         [0,0,0,0,0,0,0,0],
         [0,0,0,0,0,0,0,0],
         [0,0,0,0,0,0,0,0],
         [0,0,0,0,0,0,0,0]]
>>> board
[[0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0]]
>>> 

請用 for 完成(提示如下):

>>> [[ 0 for …
>>> 

切片

切片也是 Python 序列的重要操作之一,可以這樣來解釋,如果有學過 Basic 語言的人一定寫過 Basic for 迴圈,FOR I = 1 TO 10 STEP 1,這個迴圈裏面包含了三個東西,起始值,結束值(Python則不包含),與間隔。

Python 的切片則寫成這樣:[起始值:結束值(但不包含):間隔]
回想一下我們常常用到的 range(10),我們用 [i for i in range(10)] 來建立一個 0 9 的列表: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]。其實就等同於 [i for i in range(0,10,1)],在 range() 括號裡面同樣也接受三個數字:

起始值(預設0) : 結束值(但不包含) : 間隔(預設1)

這樣所有的謎題應該就解開了吧!

隨堂測驗 建立一個 [1, 3, 5, 7, 9]列表
>>> [ i for __________________ ]
>>> 

使用切片取的列表部分元素

直接使用範例教學:

>>> mylist = [i for i in range(3,18,2)]
>>> mylist
[3, 5, 7, 9, 11, 13, 15, 17]
>>> mylist[::]
[3, 5, 7, 9, 11, 13, 15, 17]
>>> mylist[::-1]
[17, 15, 13, 11, 9, 7, 5, 3]
>>> mylist[::2]
[3, 7, 11, 15]
>>> mylist[1::2]
[5, 9, 13, 17]
>>> mylist[3:6]
[9, 11, 13]
>>> mylist[0:100]
[3, 5, 7, 9, 11, 13, 15, 17]
>>> mylist[100:]
[]
>>> mylist[100]
Traceback (most recent call last):
  File "<pyshell#101>", line 1, in <module>
    mylist[100]
IndexError: list index out of range
>>> 

使用切片對列表元素進行增、刪、改

利用切片能夠快速地實現很多目的,直接看下面的執行結果:

>>> aList = [3,5,7]
>>> aList[len(aList):]
[]
>>> aList
[3, 5, 7]
>>> aList[len(aList):] = [9]
>>> aList
[3, 5, 7, 9]
>>> aList[:3] = [1,2,3]
>>> aList
[1, 2, 3, 9]
>>> aList[:3] = []
>>> aList
[9]
>>> aList = list(range(10))
>>> aList
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> aList[::2] = [0] * (len(aList)//2)
>>> aList
[0, 1, 0, 3, 0, 5, 0, 7, 0, 9]
>>> aList[3:3] = [4,5,6]
>>> aList
[0, 1, 0, 4, 5, 6, 3, 0, 5, 0, 7, 0, 9]
>>> aList[20:30] = [3] * 2
>>> aList
[0, 1, 0, 4, 5, 6, 3, 0, 5, 0, 7, 0, 9, 3, 3]
>>> 

上面的範例如果有任何看不懂的地方,請分段執行。請務必確認全部都弄清楚了之後,再繼續往下。

一般人初學程式語言,通常會從 BASIC開始,當從 BASIC語言進入C語言的世界的時候,最令人頭疼的觀念,就是指標,然後就開始傳值、傳址,搞得初學者一頭霧水。接下來我們要導入 Python 的指標概念,在C語言指標叫做 pointer,在Python 就叫做id

從最簡單的概念開始,當我們把變數a設定為 5 這個數值的時候,用Python 會寫成 a = 5,這點完全沒有問題,對人類而言,我們看到的是a裡面存著5這個數字,但是對 Python而言,則是5 存在 a 所指的記憶體位址上面。

當我們接著指定 b = a 的時候,我們會認為 a 的值是 5 以及 b 的值是 5,焦點會在值上面,但是Python是把 a, b 指向同一個記憶體位置上面,焦點在位址上面。從例子來看:

>>> a = 5
>>> id(a)
1779396192
>>> b = a
>>> id(b)
1779396192
>>> b = 5
>>> id(b)
1779396192
>>> b = 6
>>> id(b)
1779396224
>>> c=5
>>> d=5
>>> id(c)
1779396192
>>> id(d)
1779396192
>>> d=7
>>> id(d)
1779396256
>>> 

不要小看這範例,它詳細說明了 Python 是如何處理記憶體的,不同的變數名稱,即使我們在不同地方指定同一個數值,它們還是指向同一個記憶體位址,直到另一個變數指定了不同數值之後,位址才會改變。

為何要說明這些?因為稍後我們就要討論切片返回的是列表元素的淺複製,與列表物件的直接賦予值是不一樣的:

>>> aList = [3,5,7]
>>> bList = aList
>>> id(aList)
2479625896072
>>> id(bList)
2479625896072
>>> aList == bList        # 值相同
True
>>> aList is bList        # 同一個物件
True
>>> bList[1] = 8
>>> aList
[3, 8, 7]
>>> cList = aList[::]     # 切片,淺複製
>>> aList == cList
True
>>> aList is cList        # 不同物件
False
>>> cList[1] = 9
>>> cList
[3, 9, 7]
>>> aList
[3, 8, 7]
>>> 

不過數字跟列表有一點不同,如下:

>>> a = 5
>>> b = 5
>>> a is b
True
>>> a = [1,2,3]
>>> b = [1,2,3]
>>> a is b
False
>>> 

原因應該是列表太過於龐大,在分開指定值的時候,沒有必要一一比對是否每個元素都相同,反而沒有效率。Python 針對整數、字元、字串資料類別的變數會指向同一個位址,但像是浮點數、序列則不會。

留言

張貼留言

這個網誌中的熱門文章

Python可以這樣玩(16):共陰/共陽七段顯示器

Python可以這樣玩(11):數學繪圖

Python可以這樣玩(15):蜂鳴器與音樂