iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 25
3
Software Development

活用python- 路遙知碼力,日久練成精系列 第 25

Day25- python內建collections模組簡介,更優雅的選擇容器

路遙知碼力,日久練成精-只要在程式之路鑽研的夠深,便能夠充分發揮程式碼的力量; 練習的日子夠久,便能夠練成寫出精簡代碼的能力。

首先先來討論一下昨日課後練習的解答,
(還沒看過題目的朋友歡迎點昨日題目傳送門)

昨日課後練習討論

昨天談的是一個排序問題,
要將字串依照指定的規則重排:

  1. 數字一定排在最右邊
  2. 偶數一定排在奇數的右邊
  3. 大寫字母一定排在小寫字母的右邊
  4. 在上述規則下,儘量保持原本字串的順序(在就是在原來字串的index愈大者排愈右邊)

其實這不就是有優先次序的排序嗎?
優先次序: 數字(偶數>奇數)>字母(大寫>小寫)>位置
因此我們可以按照這樣的順序,
將要比較的值映射到一個元組,
我想到最精簡的程式如下:

s = input() #測試範例輸入 Sorting1234
L=sorted(s, key= lambda x: (x.isdigit(), x.isdigit() and int(x) % 2 == 0, x.isupper(), s.index(x)))
print("".join(L)) #預期結果: ortingS1324

元組中將字串中的每個字元依序映射出四個值:
「是否為數字」、「若是數字是否為偶數」、「是否為大寫字母」、「在原字串的位置」。
你可能會問說為何不用判斷是否為小寫字母?
因為原題目有說input只有字母和數字,
若不是數字也不是大寫字母,
那一定是小寫字母了,
便不必再特別判斷。
如有問題或其它想法也歡迎留言討論哦。

認識更多的容器

其實這一篇像是Day11-讓我們優雅泡咖啡般地選擇容器吧的延伸,
正所謂工欲善其事,必先利其器,
選擇好的工具往往可以達到更好的效果,
可能是你所需要的程式碼數量減少,
也可能是選擇合適的工具使得程式效能提升了。
本篇要簡介的是python內建模組- collections
此模組實現了提供特定目標的容器。

首先,其實對初學者來說,
內建的list, set, tuple, dict,已經算非常夠用了,
若你對於一般狀況如何選擇這些容器已經很困惑了,
可以先略過本篇不看。

不過若是你對於list, set, tuple, dict這些基本容器夠了解了,
有時選擇更客製化的容器效果更佳。

欲知collections模組全部有哪些容器,
可以參考官方文檔

容器介紹一、namedtuple()-可命名屬性的元組

例如我們在昨天Day24的範例24-2中,
我們用元組表示一個人的國、英、數三科的分數,
例如說(9, 6, 11)表示國文9級分,英文6級分,數學11級分,
然而當我們開發大型程式時,
可能看到(9, 6, 11)具體並不記得這是什麼東西,
這時,使用namedtuple()便可以讓我們為每個分量做命名了。
範例如下:

from collections import namedtuple
Score = namedtuple('Score', ['Chinese', 'English', 'math'])
s1 = Score(9, 6, 11)
print(s1)
print(s1.Chinese)

結果為:
Score(Chinese=9, English=6, math=11)
9
可以看到此時我們可以用s1.Chinese這樣的語法取得國文成績了,
當程式規模很大時可增加程式的可讀性。

容器介紹二、deque()- 高效的在列表兩端刪增元素

deque是個類似列表(list)的容器,
它實現了高效率的在容器的左、右兩端新增或刪除元素,
因此適合用來做為資料結構上的stackqueue來使用。
(若你不懂資料結構的話也沒關係,這並非本系列文討論的重點)
以下是deque()提供的幾個方法:

append(x)

添加x到右端

appendleft(x)

添加x到左端

pop(x)

移除並返回deque最右端的元素

popleft(x)

移除並返回deque最右端的元素
究竟deque的效率跟list相比如何呢?
直接以程式碼測試給大家看:

list右插入 v.s. deque右插入

我們同樣在list和deque的右端插入一百萬個元素,
用time模組測量時間,程式如下:

from collections import deque
import time

# 測試list的插入效能
tStart = time.time()#計時開始
myList = []
for i in range(10**6):
    myList.append(i)
tEnd = time.time()#計時結束
print("Total time= %f seconds" % (tEnd - tStart))

# 測試deque的插入效能
tStart = time.time()#計時開始
myDeque = deque()
for i in range(10**6):
    myDeque.append(i)
tEnd = time.time()#計時結束
print("Total time= %f seconds" % (tEnd - tStart))

結果為:
Total time= 0.180000 seconds
Total time= 0.150000 seconds
你會覺得說感覺兩者之間好像也沒差多少嘛,
我們再比較左插入的狀況看看。

list左插入 v.s. deque左插入

這次我們在list和deque的左端插入十萬個元素,
一樣用time模組測量時間,程式如下:

from collections import deque
import time

# 測試list的插入效能
tStart = time.time()#計時開始
myList = []
for i in range(10**5):
    myList.insert(0,i)
tEnd = time.time()#計時結束
print("Total time= %f seconds" % (tEnd - tStart))

# 測試deque的插入效能
tStart = time.time()#計時開始
myDeque = deque()
for i in range(10**5):
    myDeque.appendleft(i)
tEnd = time.time()#計時結束
print("Total time= %f seconds" % (tEnd - tStart))

結果為:
Total time= 2.180000 seconds
Total time= 0.030000 seconds
哇,效率相差將近百倍呢,驚不驚人?
由此可知在適當的時機選擇適合的容器相當重要呢。

那為什麼對於list來說,
在左邊插入元素和在右邊插入元素效率差這麼多呢?
我試著給個直觀的解釋:
把list想成一個排隊的隊伍,
右側是隊伍的尾巴,
如果在右側插入元素的話,
就相當於在隊伍後面多排一個人,
只有加入隊伍的人動,前面的人都不動。

可是若在左側插入元素的話,
則是硬在「插隊」排在隊伍的第一個,
其它所有人都要移動位置往後一格,
因此相當耗時。

容器介紹三、defaultdict()- 有默認值的字典

我們可能會想到說可以用一個字典(dict)來統計列表中每個元素的個數,
譬如想這樣寫:

myList = ['A','B','A','A','B','B','A','C','C','C']
count = {} #{}表示一個空字典
for element in a_list:
    count[element] += 1

用for迴圈遍歷列表的每個元素,
然後把計數加一,
看起來蠻合理的,
但是直接執行程式的話會出現KeyError
因為字典的鍵值(key)必需要先初始化才能用。
因此,我們每次都要先檢查key值存不存在才行,
正確程式如下:

myList = ['A','B','A','A','B','B','A','C','C','C']
count = {} #{}表示一個空字典
for element in myList:
    if element not in count:
        count[element] = 1
    else:
        count[element] += 1
print(count)

結果為: {'A': 4, 'B': 3, 'C': 3}
但是這樣又有點失去python的簡潔性了。
能不能我們把沒用過的key值都設一個默認值(譬如:0),
這樣只要每次都把count[element]加一就好,
不用先去判斷key值是否存在。

這時便可以用defaultdict()-可以默認值的字典來幫忙了,
程式修改如下:

from collections import defaultdict
myList = ['A','B','A','A','B','B','A','C','C','C']
count = defaultdict(lambda: 0)
for element in myList:
    count[element] += 1
print(count)

結果為:defaultdict(<function <lambda> at 0x000000000B8E99D8>, {'A': 4, 'B': 3, 'C': 3})
defaultdict裡面可以傳入一個函數回傳默認值。

容器介紹四、Counter()- 計數小幫手

說到計數功能的話,還是使用Counter最直接了。
Counter也可以把它想成是字典的一種,
不過跟defaultdict比起來,
Counter多了一些專為計數而設的功能,
因此若是要統計個數的話Counterdefaultdict好用太多啦,
範例程式如下:

from collections import Counter
myList = ['A','B','A','A','B','B','A','C','C','C']
count = Counter(myList)
print(count)
print(count['A'])
print(count['D']) #對於不存在的值默認為0
print(count.most_common(2)) #找出出現次數最高的兩個元素

結果為:
Counter({'A': 4, 'B': 3, 'C': 3})
4
0
[('A', 4), ('B', 3)]

課後練習

有時候我們會碰到這樣的問題,我們拿到一篇英文的文章,
要統計說每個單字出現了幾次。
比如說底下是我隨意打的幾個英文句字:

article = "How are you? I am fine, thank you. And you you you you you? You are so smart."

像這一類跟「計數」相關的問題,
就很適合用Counter來解決。
不過,首先我們要先想辦法把每個單字取出來(提示: 字串的split函數),
並忽略大小寫的區別和標點符號。
(關於字串操作技巧,可複習Day6- 超完整python字串函數用法統整。這篇)

底下給你統計文章各單字次數的程式架構:

def trans(article):
    return #寫你要回傳的值

article = "How are you? I am fine, thank you. And you you you you you? You are so smart."
article = trans(article)
print(article)
count = Counter(article.split())
print(count)

其中,我們定義一個轉換函數trans(article)
效果是把article的字母全部變成小寫,
然後把不是英文字母的字(如標點符號)替換成空白,
函數回傳一個字串。
預期結果:

how are you  i am fine  thank you  and you you you you you  you are so smart 
Counter({'you': 8, 'are': 2, 'how': 1, 'i': 1, 'am': 1, 'fine': 1, 'thank': 1, 'and': 1, 'so': 1, 'smart': 1})

這題難度有點難,不過解答只要寫一行程式即可完成。


上一篇
Day24- 魔鏡啊魔鏡,誰是列表中最美麗的元素? (任意規則的排序方法)
下一篇
Day26- python內建itertools模組簡介,窮舉排列組合
系列文
活用python- 路遙知碼力,日久練成精30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中
1
ccutmis
iT邦高手 2 級 ‧ 2019-09-29 10:18:55
from collections import Counter
from re import sub as reSub

def trans(article):
    return reSub(r'[^A-Za-z]+', ' ', article.lower())

一馬老師的教學愈來愈有深度了,
我知道上面的應該不是標準答案,但是我就是喜歡用RegEXP...
/images/emoticon/emoticon25.gif

不明 檢舉
【**此則訊息已被站方移除**】
0
arguskao
iT邦新手 4 級 ‧ 2022-03-01 21:15:45

請問一下
教學範例
from collections import namedtuple
Score = namedtuple('Score', ['Chinese', 'English', 'math'])
s1 = Score(9, 6, 11)
print(s1)
print(s1.Chinese)

但如果改成

Score = namedtuple('Score', ['1', '2', '3'])
s1 = Score(9, 6, 11)
....
也就說Chinese可否換成數字...

謝謝!

跑了一下,不行
namedtuple是用賦值,把分數(資料)指給變數,變數名稱不能用數字開頭,是物件的屬性

0
bluefishtear
iT邦新手 5 級 ‧ 2022-12-02 05:29:30
def trans(article):
    import re 
    return re.sub(r'[^a-z]','',article.lower())

換成空字串,不換成空白是因為,之後如果要用split(),標點符號後面已經有空白,兩空白之間會切出空字串

我要留言

立即登入留言