約維安計畫:Python 的資料結構

第十四週。

什麼是資料結構

約維安計畫:第十三週中我們已經知道 Python 的基本資料型別,包含整數、浮點數、文字、布林與未定義值,資料型別在程式設計中所扮演的角色就像是樂高、積木、模型或拼圖等遊戲中的基本單元零件,透過結合資料型別,可以堆疊出更進階的 Python 應用,接下來我們會從資料型別邁向結構(Structures)。在電腦科學中,資料結構是電腦儲存、組織以及存取資料的機制,在 Python 中我們可以透過四種基本資料結構來儲存、組織以及存取我們在約維安計畫:第十三週所認識的資料型別。

  1. 串列 list

  2. tuple

  3. 字典 dict

  4. 集合 set

以 type() 函數識別資料結構

和資料型別的應用場景相同,常見對資料結構應用函數或者呼叫資料結構本身的方法時候,都是對應完成宣告的物件應用,而非直接對應資料結構,這是因為程式設計通常在後續會有資料記錄與運算處理的需求,並不僅止與將其印出來而已。所以我們可以運用能夠將完成宣告的物件所儲存之資料結構告知使用者的內建函數 type(),作用是將被應用物件的類別回傳,在 Python 中物件(Objects)是類別(Classes)的實例(Instances),而類別有分為資料型別、資料結構、函數以及特殊類別(例如自行定義類別、產生器或迭代器等進階的 Python 類別)。

primes_list = [2, 3, 5, 7, 11]
primes_tuple = (2, 3, 5, 7, 11)
primes_dict = {
    "1st": 2,
    "2nd": 3,
    "3rd": 5,
    "4th": 7,
    "5th": 11
}
primes_set = {2, 3, 5, 7, 11}
print(type(primes_list))
print(type(primes_tuple))
print(type(primes_dict))
print(type(primes_set))
## <class 'list'>
## <class 'tuple'>
## <class 'dict'>
## <class 'set'>

函數的使用方式

在 Python 中使用函數有兩種機制,一是對資料型別或資料結構應用函數;二是呼叫綁定於資料型別或資料結構的方法,這時要注意,我們不再稱呼「函數」,而是改稱「方法」。

舉一個例子說明,Python 有一個內建函數 sorted(),串列有一個方法 sort(),兩者都能夠排序一個串列,但是在使用的語法與意義上不同,使用內建函數 sorted() 排序串列是我們習慣且直觀的「對資料結構應用函數」,語法是 sorted(a_list_to_be_sorted);使用串列方法 sort() 排序是我們較為陌生的「呼叫綁定於資料結構的方法」,語法是 a_list_to_be_sorted.sort()

primes_list = [11, 7, 5, 3, 2]
sorted(primes_list) # Apply sorted function to primes_list
primes_list.sort()  # Call sort method of primes_list

除了這兩種函數使用的機制差異,另外一個值得注意之處在於函數的應用以及方法的呼叫都可能有兩種讓應用或者綁定的資料型別與資料結構造成更動的機制,一是以回傳值型態輸出更動後的結果;二是直接更動資料型別與資料結構而沒有輸出。這個差異能夠延續前述內建函數 sorted() 與串列方法 sort() 的例子,sorted(a_list_to_be_sorted) 是以回傳值輸出排序後的串列,因此如果沒有將回傳值更新原本命名的串列,排序的更動並不會被保留。

primes_list = [11, 7, 5, 3, 2]
sorted(primes_list) # Apply sorted function to primes_list
print(primes_list)  # primes_list is not sorted
primes_list = sorted(primes_list)  # Update primes_list with function output
print(primes_list)  # primes_list is now sorted
## [11, 7, 5, 3, 2]
## [2, 3, 5, 7, 11]

a_list_to_be_sorted.sort() 則是直接將排序更動了,不需要更新原本命名的串列,也不會伴隨有回傳值。

primes_list = [11, 7, 5, 3, 2]
primes_list.sort()  # Call sort method of primes_list
print(primes_list)  # primes_list is sorted
## [2, 3, 5, 7, 11]

函數或者方法多數情況下會在「以回傳值輸出」或「直接更動物件狀態」中擇一,少數情況下也會有兩者兼具的情況,例如串列方法 pop() 能夠將串列末端的資料拋出為回傳值,並刪除串列末端的資料點。

primes_list = [11, 7, 5, 3, 2]
the_last_element = primes_list.pop()
print(primes_list)       # The last element was deleted
print(the_last_element)  # The last element was returned
## [11, 7, 5, 3]
## 2

串列

串列 list 是 Python 最基礎且最為常用的資料結構,可以容納不同的資料型別與資料結構,建立的時候使用中括號 [] 將希望儲存的資料型別與資料結構包括起來,並將希望存放的資料型別與資料結構以逗號分隔。

primes_list = [2, 3, 5, 7, 11]
print(type(primes_list))
## <class 'list'>

有時候我們也使用 Python 的內建函數 list() 來建立串列,例如常被用來生成數列的內建函數 range() 其輸出是一個可以被迭代的物件(Iterable),屬於 range 類別,如果直接應用不會被印出所有的數字,這時可以用 list() 函數將它轉換為 list 來觀察其中包含的數字。

integer_sequence = range(10)
print(integer_sequence)
print(list(integer_sequence))
## range(0, 10)
## [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

使用串列將多筆資料型別或結構儲存於一個物件之中,就像是在進行化零為整的工作,這表示 Python 必然也能夠讓我們進行逆向工程:化整為零,化整為零的專有名稱為子集(Subset),串列的子集操作技巧可以分為兩種:

  1. 索引(Indexing)

  2. 切割(Slicing)

利用內建函數 len() 可以先暸解我們想要取為子集的串列長度(亦即串列中所儲存的元素個數),進而再利用索引、切割的技法。

primes_list = [2, 3, 5, 7, 11]
len_primes_list = len(primes_list)
print(len_primes_list)
# 5

選擇串列中的資料我們使用中括號 [] 搭配索引值,值得注意的是索引值從左邊由 0 開始,而非由 1 開始。

# start from left at index 0
print(primes_list[0])
print(primes_list[1])
print(primes_list[2])
print(primes_list[3])
print(primes_list[len_primes_list - 1])
## 2
## 3
## 5
## 7
## 11

從右邊則由 -1 開始。

# start from right at index -1
print(primes_list[-1])
print(primes_list[-2])
print(primes_list[-3])
print(primes_list[-4])
print(primes_list[-len_primes_list])
## 11
## 7
## 5
## 3
## 2

利用 [start:stop:step] 語法對串列指定要切割出來的子集,其中 stop 不會出現在最後選擇出來的結果中,例如我們希望將位於 [0] 、 [1] 、 [2] 的資料另外切割為一個長度為 3 的串列,要使用 [0:3] 而非 [0:2]

print(primes_list[0:3])
## [2, 3, 5]

使用切割的語法時, start 、 stop 與 step 這三個參數都有預設值, start 預設是 0, stop 預設為串列的長度, step 預設為 1,假如沒有指定就採預設值,像是切割出位於 [0] 、 [2] 與 [4] 這三個位置的資料可以簡單使用 [::2] 。

print(primes_list[::2])
## [2, 5, 11]

比較特別的 step 參數如果指定為 -1 可以將串列中的資料順序反轉(Reverse)。

print(primes_list[::-1])
## [11, 7, 5, 3, 2]

除了索引與切割,常用的串列操作還有新增、刪除與排序資料,新增可以使用串列的append() 方法將資料加入到串列末端,這個方法採用直接更動沒有輸出的機制。

primes_list = [2, 3, 5, 7, 11]
primes_list.append(13)
print(primes_list)  # 13 was appended to primes_list
## [2, 3, 5, 7, 11, 13]

刪除可以使用串列的 pop() 方法將串列末端的資料拋出,這個方法同時採用回傳值與直接更動的機制。

primes_list.pop()  # 13 is popped out and returned
print(primes_list) # 13 was deleted
## [2, 3, 5, 7, 11]

排序可以使用串列的 sort() 方法將串列的資料以遞增排序,亦可以指定參數 reverse=True 以遞減排序,這個方法採用直接更動沒有輸出的機制。

primes_list = [2, 3, 5, 7, 11]
primes_list.sort(reverse=True)
print(primes_list) # primes_list is now sorted with descending order
primes_list.sort()
print(primes_list) # primes_list is now sorted with ascending order
## [11, 7, 5, 3, 2]
## [2, 3, 5, 7, 11]

Tuples

tuple 是 Python 另一常用的資料結構,多數的特性都與串列相同,例如在索引與切割的部分與串列操作語法完全相同。在建立的時候使用小括號 () 將希望儲存的資訊包括起來,並將希望存放的資料型別與資料結構以逗號分隔。

primes_tuple = (2, 3, 5, 7, 11)
print(type(primes_tuple))
## <class 'tuple'>

tuple 與串列最大的差異在於 tuple 是不可變動(Immutable)的資料結構,不具備任何包含新增、刪除與排序資料的操作,這樣的特性讓 tuple 被設計在函數的彈性參數與預設多個回傳值之中。自行定義函數時如果希望參數為不定個數,例如一個能夠進行兩個數字、三個數字甚至 n 個數字相加的函數,彈性參數 *args 在函數作用域中其實就是一個 tuple

def add_n_numbers(*args):
    print(type(args))
    return sum(args)

add_n_numbers(55, 66)
add_n_numbers(55, 66, 77)
## <class 'tuple'>
## 121
## <class 'tuple'>
## 198

自行定義函數時如果希望回傳值有多個,例如一個能夠將一個英文單字的全大寫、全小寫外型都回傳的函數,可以在 return 後以逗號區隔兩個回傳值,函數其實就是回傳一個 tuple

def upper_lower_cases(x):
    upper_out = x.upper()
    lower_out = x.lower()
    return upper_out, lower_out

multiple_outputs = upper_lower_cases("Luke Skywalker")
print(type(multiple_outputs))
## <class 'tuple'>

字典

字典 dict 顧名思義就是 dictionary 的縮寫,這種資料結構除了儲存資料(Values)以外,還另外利用鍵(Keys)來對資料作索引,這樣的特性讓我們在選擇時可以使用資料的鍵,如此一來在面對長度很大的資料時,不需要耗時計算資料所在的位置。建立的時候使用大括號 {} 將希望儲存的資料包括起來,然後分別將「鍵」與「資料」以 Keys 及 Values 記錄。

primes_dict = {
    "1st": 2,
    "2nd": 3,
    "3rd": 5,
    "4th": 7,
    "5th": 11
}
print(type(primes_dict))
## <class 'dict'>

選擇字典中的資料我們使用中括號 [] 搭配鍵。

print(primes_dict["1st"])
print(primes_dict["2nd"])
print(primes_dict["3rd"])
print(primes_dict["4th"])
print(primes_dict["5th"])
## 2
## 3
## 5
## 7
## 11

新增可以直接宣告 dict_to_be_added["Key"] = Value

primes_dict["6th"] = 13
print(primes_dict)
## {'1st': 2, '2nd': 3, '3rd': 5, '4th': 7, '5th': 11, '6th': 13}

刪除可以使用字典的 pop(Key, None) 方法將指定的「鍵」與「資料」組合拋出,假如找不到就拋出 None

the_popped_value = primes_dict.pop("6th", None)
print(primes_dict)
## {'1st': 2, '2nd': 3, '3rd': 5, '4th': 7, '5th': 11}

使用字典的 keys() 、 values() 與 items() 方法,可以分別取出字典中的所有的鍵(Keys)、所有資料(Values)以及「鍵」與「資料」組合(Items)。

print(primes_dict.keys())
print(primes_dict.values())
print(primes_dict.items())
## dict_keys(['1st', '2nd', '3rd', '4th', '5th'])
## dict_values([2, 3, 5, 7, 11])
## dict_items([('1st', 2), ('2nd', 3), ('3rd', 5), ('4th', 7), ('5th', 11)])

集合

集合 set 是 Python 中用來處理「集合運算」的資料結構,在建立的時候使用大括號 {} 將希望儲存的資訊包括起來,並將希望存放的資料型別與資料結構以逗號分隔。

primes_set = {2, 3, 5, 7, 11}
print(type(primes_set))
## <class 'set'>

這個資料結構用來對應數學中的集合相關定理和操作,所以其中只儲存獨一的資料,若輸入多筆重複資料,則會記錄一筆獨一值,並且是一種「無序」的結構,儲存在其中的資料跟建立時的順序性沒有關聯,可能會在操作後更動順序,但這並無大礙,因為集合並不支援索引,無序這個特性也不會影響集合相關定理和操作。

基礎的集合操作有四個:

  1. 交集(Intersection)

  2. 聯集(Union)

  3. 差集(Difference)

  4. 對稱差集(Symmetric difference)

上述這四個基礎集合操作都能夠透過呼叫集合的方法實作,其中只有差集沒有交換律(如同減法或者除法一般),另外三者都具有交換律(如同加法或者乘法一般),這些方法都採用以回傳值輸出的機制。

primes_set = {2, 3, 5, 7, 11}
odds_set = {1, 3, 5, 7, 9}
print(primes_set.intersection(odds_set))
print(primes_set.union(odds_set))
print(primes_set.difference(odds_set))
print(odds_set.difference(primes_set))
print(primes_set.symmetric_difference(odds_set))
## {3, 5, 7}
## {1, 2, 3, 5, 7, 9, 11}
## {2, 11}
## {1, 9}
## {1, 2, 9, 11}

在認識了資料結構之後,約維安計畫:第十四週來到尾聲,希望您也和我一樣期待下一篇文章。

延伸閱讀

  1. https://docs.python.org/3/tutorial/datastructures.html


對於這篇文章有什麼想法呢?喜歡😻、留言🙋‍♂️或者分享🙌

Leave a comment