Python3 CookBook | 資料結構和演算法(二)

yongxinz發表於2017-11-30

歡迎關注我的微信公眾號 AlwaysBeta,更多精彩內容等你來。

Python3 CookBook | 資料結構和演算法(二)

以下測試程式碼全部基於 Python3

1、查詢最大或最小的 N 個元素

工作中有時會遇到這樣的需求,取出資料中前面 10% 的值,或者最後 10% 的值。

我們可以先對這個列表進行排序,然後再進行切片操作,很輕鬆的解決這個問題。但是,有沒有更好的方法呢?

heapq 模組有兩個函式 nlargest() 和 nsmallest() 可以完美解決這個問題。

In [50]: import heapq

In [51]: n = [1, 8, 2, 23, 7, -4, 18, 23, 42, 37, 2, 23, 45, 76]

In [52]: heapq.nlargest(3, n)
Out[52]: [76, 45, 42]

In [53]: heapq.nsmallest(3, n)
Out[53]: [-4, 1, 2]
複製程式碼

如果是取排在前面的 10% 應該怎麼做?

heapq.nlargest(round(len(n)/10), n)
複製程式碼

而且,使用這兩個函式還會有更好的效能,因為在底層實現裡面,會先把資料進行堆排序後放入一個列表中,然後再進行後續操作。大家如果對堆資料結構感興趣的話,可以繼續進行深入研究,由於我瞭解的並不深,也沒辦法再展開了。

但是也並不是什麼時候都是這兩個函式效果更好,比如只取一個最大值或者最小值,那還是 min() 或 max() 效果更好;如果要查詢的元素個數已經跟集合元素個數接近時,那還是用 sorted(items)[:N] 更好,具體情況具體分析吧。

2、序列中出現次數最多的元素

以前碰到這類問題時,我都會手動建立一個字典,然後以列表中元素作為 key,進而統計出 key 出現的次數,再進行比較得到出現次數最多的元素。

殊不知 collections 中就有專門為這類問題設計的類 Counter,瞬間感覺自己蠢爆了,話不多說,直接上程式碼。

In [54]: from collections import Counter

In [55]: w = ['a', 'b', 'c', 'd', 'a', 'a', 'b']

In [56]: w_count = Counter(w)

In [57]: w_count
Out[57]: Counter({'a': 3, 'b': 2, 'c': 1, 'd': 1})

In [58]: w_count['a']
Out[58]: 3

In [59]: top = w_count.most_common(2)

In [60]: top
Out[60]: [('a', 3), ('b', 2)]
複製程式碼

可以看到,Counter 返回的就是一個字典,想知道哪個元素出現幾次,直接取,是不是很方便?

而且還有 most_common 函式,簡直不要太棒。

3、過濾序列元素

有一個列表,如下:

In [61]: a = [1, 2, 3, 4, 5, -3]
複製程式碼

要求過濾所有負數。需要新建一個列表?直接一行程式碼搞定。

In [64]: [n for n in a if n > 0]
Out[64]: [1, 2, 3, 4, 5]
複製程式碼

如果要把負數替換成 0 呢?

In [67]: [n if n > 0 else 0 for n in a]
Out[67]: [1, 2, 3, 4, 5, 0]
複製程式碼

但是有時候過濾條件可能比較複雜,這時就需要藉助於 filter() 函式了。

values = ['1', '2', '-3', '-', '4', 'N/A', '5']
def is_int(val):
  try:
    x = int(val)
      return True
  except ValueError:
    return False

ivals = list(filter(is_int, values))
print(ivals)
# Outputs ['1', '2', '-3', '4', '5']
複製程式碼

4、通過某個關鍵字將記錄分組

有下面這個字典:

rows = [
  {'address': '5412 N CLARK', 'date': '07/01/2012'},
  {'address': '5148 N CLARK', 'date': '07/04/2012'},
  {'address': '5800 E 58TH', 'date': '07/02/2012'},
  {'address': '2122 N CLARK', 'date': '07/03/2012'},
  {'address': '5645 N RAVENSWOOD', 'date': '07/02/2012'},
  {'address': '1060 W ADDISON', 'date': '07/02/2012'},
  {'address': '4801 N BROADWAY', 'date': '07/01/2012'},
  {'address': '1039 W GRANVILLE', 'date': '07/04/2012'},
]
複製程式碼

那麼怎麼對這個字典按照 date 進行分組呢?藉助於 itertools.groupby() 函式可以解決這個問題,程式碼如下:

# Sort by the desired field first
rows.sort(key=itemgetter('date'))
# Iterate in groups
for date, items in groupby(rows, key=itemgetter('date')):
  print(date)
  for i in items:
    print(' ', i)
複製程式碼

輸出結果如下:

07/01/2012
  {'address': '5412 N CLARK', 'date': '07/01/2012'}
  {'address': '4801 N BROADWAY', 'date': '07/01/2012'}
07/02/2012
  {'address': '5800 E 58TH', 'date': '07/02/2012'}
  {'address': '5645 N RAVENSWOOD', 'date': '07/02/2012'}
  {'address': '1060 W ADDISON', 'date': '07/02/2012'}
07/03/2012
  {'address': '2122 N CLARK', 'date': '07/03/2012'}
07/04/2012
  {'address': '5148 N CLARK', 'date': '07/04/2012'}
  {'address': '1039 W GRANVILLE', 'date': '07/04/2012'}
複製程式碼

需要注意的是,groupby() 函式僅僅檢查連續相同的元素,所以在分組之前,一定要先對資料,按照分組欄位進行排序。如果沒有排序,便得不到想要的結果。

5、對映名稱到序列元素

我常常有這樣的苦惱,就是有一個列表,然後通過下標來取值,取值時很認真的數所需要元素在第幾個,很怕取錯值。取到值後開始下面的運算。

一段時間之後,再看這段程式碼,感覺很陌生,已經忘了帶下標的值是什麼了,還需要重新看一下這個列表的由來,才找到回憶。

如果能有一個名稱對映到元素上就好了,直接通過名稱就可以知道元素的含義。collections.namedtuple() 函式就可以解決這個問題。

In [76]: from collections import namedtuple

In [77]: subscriber = namedtuple('Subscriber', ['addr', 'joined'])

In [78]: sub = subscriber('jonesy@example.com', '2012-10-19')

In [79]: sub
Out[79]: Subscriber(addr='jonesy@example.com', joined='2012-10-19')

In [80]: sub.addr
Out[80]: 'jonesy@example.com'

In [81]: sub.joined
Out[81]: '2012-10-19'
複製程式碼

這樣就可以通過名稱來取值了,程式碼可讀性也更高。

需要注意的是,這種命名元祖的方式不能直接修改其中的值,直接修改會報錯

In [82]: a = namedtuple('SSS', ['name', 'shares', 'price'])

In [83]: _a = a('yongxinz', 1, 2)

In [84]: _a.shares = 4
-----------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-84-f62a5288a29a> in <module>()
> 1 _a.shares = 4

AttributeError: can't set attribute
複製程式碼

想要修改的話可以使用 _replace() 函式。

In [85]: _a._replace(shares=4)
Out[85]: SSS(name='yongxinz', shares=4, price=2)
複製程式碼

但是還有一個疑問,如果這個列表元素比較多的話,那就需要定義很多的名稱,也比較麻煩,還有更好的方式嗎?

未完待續。。。

相關文章