2018-03-01數據結構與演算法(4) 1.16過濾序列元素 最簡單的過濾序列元素的方法就是使用列表推導。比如: 用列表推導的一個潛在缺陷就是如果輸入非常大的時候會產生一個非常大的結果集,占用大量記憶體。 如果你對記憶體比較敏感, 那麼你可以使用生成器表達式迭代產生過濾的元素。比如: 有時候,過濾規則 ...
2018-03-01數據結構與演算法(4)
1.16過濾序列元素
最簡單的過濾序列元素的方法就是使用列表推導。比如:
>>> mylist = [1, 4, -5, 10, -7, 2, 3, -1] >>> [n for n in mylist if n > 0] [1, 4, 10, 2, 3] >>> [n for n in mylist if n < 0] [-5, -7, -1] >>>
用列表推導的一個潛在缺陷就是如果輸入非常大的時候會產生一個非常大的結果集,占用大量記憶體。 如果你對記憶體比較敏感,
那麼你可以使用生成器表達式迭代產生過濾的元素。比如:
>>> pos = (n for n in mylist if n > 0) >>> pos <generator object <genexpr> at 0x1006a0eb0> >>> for x in pos: ... print(x) ... 1 4 10 2 3 >>>
有時候,過濾規則比較複雜,不能簡單的在列表推導或者生成器表達式中表達出來。 比如,假設過濾的時候需要處理一些異常或者其他複雜情況。這時候你可以將過濾代碼放到一個函數中, 然後使用內建的 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']
列表推導和生成器表達式通常情況下是過濾數據最簡單的方式。 其實它們還能在過濾的時候轉換數據。比如:
>>> mylist = [1, 4, -5, 10, -7, 2, 3, -1] >>> import math >>> [math.sqrt(n) for n in mylist if n > 0] [1.0, 2.0, 3.1622776601683795, 1.4142135623730951, 1.7320508075688772] >>>
過濾操作的一個變種就是將不符合條件的值用新的值代替,而不是丟棄它們。 比如,在一列數據中你可能不僅想找到正數,而且還想將不是正數的數替換成指定的數。 通過將過濾條件放到條件表達式中去,可以很容易的解決這個問題,就像這樣:
>>> clip_neg = [n if n > 0 else 0 for n in mylist] >>> clip_neg [1, 4, 0, 10, 0, 2, 3, 0] >>> clip_pos = [n if n < 0 else 0 for n in mylist] >>> clip_pos [0, 0, -5, 0, -7, 0, 0, -1] >>>
另外一個值得關註的過濾工具就是 itertools.compress()
, 它以一個 iterable
對象和一個相對應的 Boolean
選擇器序列作為輸入參數。 然後輸出 iterable
對象中對應選擇器為 True
的元素。 當你需要用另外一個相關聯的序列來過濾某個序列的時候,這個函數是非常有用的。 比如,假如現在你有下麵兩列數據:
addresses = [ '5412 N CLARK', '5148 N CLARK', '5800 E 58TH', '2122 N CLARK', '5645 N RAVENSWOOD', '1060 W ADDISON', '4801 N BROADWAY', '1039 W GRANVILLE', ] counts = [ 0, 3, 10, 4, 1, 7, 6, 1]
現在你想將那些對應 count
值大於5的地址全部輸出,那麼你可以這樣做:
>>> from itertools import compress >>> more5 = [n > 5 for n in counts] >>> more5 [False, False, True, False, False, True, True, False] >>> list(compress(addresses, more5)) ['5800 E 58TH', '1060 W ADDISON', '4801 N BROADWAY'] >>>
這裡的關鍵點在於先創建一個 Boolean
序列,指示哪些元素符合條件。 然後 compress()
函數根據這個序列去選擇輸出對應位置為 True
的元素。
和 filter()
函數類似, compress()
也是返回的一個迭代器。因此,如果你需要得到一個列表, 那麼你需要使用 list()
來將結果轉換為列表類型。
1.17從字典中提取子集
prices = { 'ACME': 45.23, 'AAPL': 612.78, 'IBM': 205.55, 'HPQ': 37.20, 'FB': 10.75 } # Make a dictionary of all prices over 200 p1 = {key: value for key, value in prices.items() if value > 200} # Make a dictionary of tech stocks tech_names = {'AAPL', 'IBM', 'HPQ', 'MSFT'} p2 = {key: value for key, value in prices.items() if key in tech_names}
大多數情況下字典推導能做到的,通過創建一個元組序列然後把它傳給 dict()
函數也能實現。比如:
p1 = dict((key, value) for key, value in prices.items() if value > 200)
但是,字典推導方式表意更清晰,並且實際上也會運行的更快些 (在這個例子中,實際測試幾乎比 dcit()
函數方式快整整一倍)。
有時候完成同一件事會有多種方式。比如,第二個例子程式也可以像這樣重寫:
# Make a dictionary of tech stocks tech_names = { 'AAPL', 'IBM', 'HPQ', 'MSFT' } p2 = { key:prices[key] for key in prices.keys() & tech_names }
但是,運行時間測試結果顯示這種方案大概比第一種方案慢 1.6 倍。 如果對程式運行性能要求比較高的話,需要花點時間去做計時測試。
1.18映射名稱到序列元素
collections.namedtuple()
函數通過使用一個普通的元組對象來幫你解決這個問題。 這個函數實際上是一個返回 Python 中標準元組類型子類的一個工廠方法。 你需要傳遞一個類型名和你需要的欄位給它,然後它就會返回一個類,你可以初始化這個類,為你定義的欄位傳遞值等。 代碼示例:
>>> from collections import namedtuple >>> Subscriber = namedtuple('Subscriber', ['addr', 'joined']) >>> sub = Subscriber('[email protected]', '2012-10-19') >>> sub Subscriber(addr='[email protected]', joined='2012-10-19') >>> sub.addr '[email protected]' >>> sub.joined '2012-10-19' >>>
儘管 namedtuple
的實例看起來像一個普通的類實例,但是它跟元組類型是可交換的,支持所有的普通元組操作,比如索引和解壓。 比如:
>>> len(sub) 2 >>> addr, joined = sub >>> addr '[email protected]' >>> joined '2012-10-19' >>>
命名元組的一個主要用途是將你的代碼從下標操作中解脫出來。 因此,如果你從資料庫調用中返回了一個很大的元組列表,通過下標去操作其中的元素, 當你在表中添加了新的列的時候你的代碼可能就會出錯了。但是如果你使用了命名元組,那麼就不會有這樣的顧慮。
為了說明清楚,下麵是使用普通元組的代碼:
def compute_cost(records): total = 0.0 for rec in records: total += rec[1] * rec[2] return total
下標操作通常會讓代碼表意不清晰,並且非常依賴記錄的結構。 下麵是使用命名元組的版本:
from collections import namedtuple Stock = namedtuple('Stock', ['name', 'shares', 'price']) def compute_cost(records): total = 0.0 for rec in records: s = Stock(*rec) total += s.shares * s.price return total
命名元組另一個用途就是作為字典的替代,因為字典存儲需要更多的記憶體空間。 如果你需要構建一個非常大的包含字典的數據結構,那麼使用命名元組會更加高效。 但是需要註意的是,不像字典那樣,一個命名元組是不可更改的。比如:
>>> s = Stock('ACME', 100, 123.45) >>> s Stock(name='ACME', shares=100, price=123.45) >>> s.shares = 75 Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: can't set attribute >>>
如果你真的需要改變屬性的值,那麼可以使用命名元組實例的 _replace()
方法, 它會創建一個全新的命名元組並將對應的欄位用新的值取代。比如:
>>> s = s._replace(shares=75) >>> s Stock(name='ACME', shares=75, price=123.45) >>>
_replace()
方法還有一個很有用的特性就是當你的命名元組擁有可選或者缺失欄位時候, 它是一個非常方便的填充數據的方法。 你可以先創建一個包含預設值的原型元組,然後使用 _replace()
方法創建新的值被更新過的實例。比如:
from collections import namedtuple Stock = namedtuple('Stock', ['name', 'shares', 'price', 'date', 'time']) # Create a prototype instance stock_prototype = Stock('', 0, 0.0, None, None) # Function to convert a dictionary to a Stock def dict_to_stock(s): return stock_prototype._replace(**s)
下麵是它的使用方法:
>>> a = {'name': 'ACME', 'shares': 100, 'price': 123.45} >>> dict_to_stock(a) Stock(name='ACME', shares=100, price=123.45, date=None, time=None) >>> b = {'name': 'ACME', 'shares': 100, 'price': 123.45, 'date': '12/17/2012'} >>> dict_to_stock(b) Stock(name='ACME', shares=100, price=123.45, date='12/17/2012', time=None) >>>
最後要說的是,如果你的目標是定義一個需要更新很多實例屬性的高效數據結構,那麼命名元組並不是你的最佳選擇。 這時候你應該考慮定義一個包含 __slots__
方法的類
1.19轉換並同時計算數據
一個非常優雅的方式去結合數據計算與轉換就是使用一個生成器表達式參數。 比如,如果你想計算平方和,可以像下麵這樣做:
nums = [1, 2, 3, 4, 5] s = sum(x * x for x in nums)
下麵是更多的例子:
# Determine if any .py files exist in a directory import os files = os.listdir('dirname') if any(name.endswith('.py') for name in files): print('There be python!') else: print('Sorry, no python.') # Output a tuple as CSV s = ('ACME', 50, 123.45) print(','.join(str(x) for x in s)) # Data reduction across fields of a data structure portfolio = [ {'name':'GOOG', 'shares': 50}, {'name':'YHOO', 'shares': 75}, {'name':'AOL', 'shares': 20}, {'name':'SCOX', 'shares': 65} ] min_shares = min(s['shares'] for s in portfolio)
對於小型列表可能沒什麼關係,但是如果元素數量非常大的時候, 它會創建一個巨大的僅僅被使用一次就被丟棄的臨時數據結構。而生成器方案會以迭代的方式轉換數據,因此更省記憶體。
在使用一些聚集函數比如 min()
和 max()
的時候你可能更加傾向於使用生成器版本, 它們接受的一個 key 關鍵字參數或許對你很有幫助。 比如,在上面的證券例子中,你可能會考慮下麵的實現版本:
# Original: Returns 20 min_shares = min(s['shares'] for s in portfolio) # Alternative: Returns {'name': 'AOL', 'shares': 20} min_shares = min(portfolio, key=lambda s: s['shares'])
1.20合併多個字典或映射
假如你有如下兩個字典:
a = {'x': 1, 'z': 3 } b = {'y': 2, 'z': 4 }
現在假設你必須在兩個字典中執行查找操作(比如先從 a
中找,如果找不到再在 b
中找)。 一個非常簡單的解決方案就是使用 collections
模塊中的 ChainMap
類。比如:
from collections import ChainMap c = ChainMap(a,b) print(c['x']) # Outputs 1 (from a) print(c['y']) # Outputs 2 (from b) print(c['z']) # Outputs 3 (from a)
一個 ChainMap
接受多個字典並將它們在邏輯上變為一個字典。 然後,這些字典並不是真的合併在一起了, ChainMap
類只是在內部創建了一個容納這些字典的列表 並重新定義了一些常見的字典操作來遍歷這個列表。大部分字典操作都是可以正常使用的,比如:
>>> len(c) 3 >>> list(c.keys()) ['x', 'y', 'z'] >>> list(c.values()) [1, 2, 3] >>>
如果出現重覆鍵,那麼第一次出現的映射值會被返回。 因此,例子程式中的 c['z']
總是會返回字典 a
中對應的值,而不是 b
中對應的值。
對於字典的更新或刪除操作總是影響的是列表中第一個字典。比如:
>>> c['z'] = 10 >>> c['w'] = 40 >>> del c['x'] >>> a {'w': 40, 'z': 10} >>> del c['y'] Traceback (most recent call last): ... KeyError: "Key not found in the first mapping: 'y'" >>>
ChainMap
對於編程語言中的作用範圍變數(比如 globals
, locals
等)是非常有用的。 事實上,有一些方法可以使它變得簡單:
>>> values = ChainMap() >>> values['x'] = 1 >>> # Add a new mapping >>> values = values.new_child() >>> values['x'] = 2 >>> # Add a new mapping >>> values = values.new_child() >>> values['x'] = 3 >>> values ChainMap({'x': 3}, {'x': 2}, {'x': 1}) >>> values['x'] 3 >>> # Discard last mapping >>> values = values.parents >>> values['x'] 2 >>> # Discard last mapping >>> values = values.parents >>> values['x'] 1 >>> values ChainMap({'x': 1}) >>>
為 ChainMap
的替代,你可能會考慮使用 update()
方法將兩個字典合併。比如:
>>> a = {'x': 1, 'z': 3 } >>> b = {'y': 2, 'z': 4 } >>> merged = dict(b) >>> merged.update(a) >>> merged['x'] 1 >>> merged['y'] 2 >>> merged['z'] 3 >>>
這樣也能行得通,但是它需要你創建一個完全不同的字典對象(或者是破壞現有字典結構)。 同時,如果原字典做了更新,這種改變不會反應到新的合併字典中去。比如:
ChainMap
使用原來的字典,它自己不創建新的字典。所以它並不會產生上面所說的結果,比如:
>>> a = {'x': 1, 'z': 3 } >>> b = {'y': 2, 'z': 4 } >>> merged = ChainMap(a, b) >>> merged['x'] 1 >>> a['x'] = 42 >>> merged['x'] # Notice change to merged dicts 42 >>>