天天看點

cookbook:(1).資料結構和算法(2/3)1.8 字典的運算1.9 查找兩字典的相同點1.10 删除序列相同元素并保持順序1.11 命名切片1.12 序列中出現次數最多的元素1.13 通過某個關鍵字排序一個字典清單1.14 排序不支援原生比較的對象

  • 轉載自: http://python3-cookbook.readthedocs.io/zh_CN/latest/copyright.html
  • 1.8 字典的運算

    問題

    怎樣在資料字典中執行一些計算操作(比如求最小值、最大值、排序等等)?

    解決方案

    考慮下面的股票名和價格映射字典:
    prices = {
        'ACME': 45.23,
        'AAPL': 612.78,
        'IBM': 205.55,
        'HPQ': 37.20,
        'FB': 10.75
    }
          
    為了對字典值執行計算操作,通常需要使用 

    zip()

     函數先将鍵和值反轉過來。 比如,下面是查找最小和最大股票價格和股票值的代碼:
    min_price = min(zip(prices.values(), prices.keys()))
    # min_price is (10.75, 'FB')
    max_price = max(zip(prices.values(), prices.keys()))
    # max_price is (612.78, 'AAPL')
          
    類似的,可以使用 

    zip()

     和 

    sorted()

     函數來排列字典資料:
    prices_sorted = sorted(zip(prices.values(), prices.keys()))
    # prices_sorted is [(10.75, 'FB'), (37.2, 'HPQ'),
    #                   (45.23, 'ACME'), (205.55, 'IBM'),
    #                   (612.78, 'AAPL')]
          
    執行這些計算的時候,需要注意的是 

    zip()

     函數建立的是一個隻能通路一次的疊代器。 比如,下面的代碼就會産生錯誤:
    prices_and_names = zip(prices.values(), prices.keys())
    print(min(prices_and_names)) # OK
    print(max(prices_and_names)) # ValueError: max() arg is an empty sequence
          

    讨論

    如果你在一個字典上執行普通的數學運算,你會發現它們僅僅作用于鍵,而不是值。比如:
    min(prices) # Returns 'AAPL'
    max(prices) # Returns 'IBM'
          
    這個結果并不是你想要的,因為你想要在字典的值集合上執行這些計算。 或許你會嘗試着使用字典的 

    values()

     方法來解決這個問題:
    min(prices.values()) # Returns 10.75
    max(prices.values()) # Returns 612.78
          

    不幸的是,通常這個結果同樣也不是你想要的。 你可能還想要知道對應的鍵的資訊(比如那種股票價格是最低的?)。

    你可以在 

    min()

     和 

    max()

     函數中提供 

    key

     函數參數來擷取最小值或最大值對應的鍵的資訊。比如:
    min(prices, key=lambda k: prices[k]) # Returns 'FB'
    max(prices, key=lambda k: prices[k]) # Returns 'AAPL'
          

    但是,如果還想要得到最小值,你又得執行一次查找操作。比如:

    前面的 

    zip()

     函數方案通過将字典”反轉”為 (值,鍵) 元組序列來解決了上述問題。 當比較兩個元組的時候,值會先進行比較,然後才是鍵。 這樣的話你就能通過一條簡單的語句就能很輕松的實作在字典上的求最值和排序操作了。

    需要注意的是在計算操作中使用到了 (值,鍵) 對。當多個實體擁有相同的值的時候,鍵會決定傳回結果。 比如,在執行 

    min()

     和 

    max()

     操作的時候,如果恰巧最小或最大值有重複的,那麼擁有最小或最大鍵的實體會傳回:
    >>> prices = { 'AAA' : 45.23, 'ZZZ': 45.23 }
    >>> min(zip(prices.values(), prices.keys()))
    (45.23, 'AAA')
    >>> max(zip(prices.values(), prices.keys()))
    (45.23, 'ZZZ')
    >>>      

    1.9 查找兩字典的相同點

    問題

    怎樣在兩個字典中尋尋找相同點(比如相同的鍵、相同的值等等)?

    解決方案

    考慮下面兩個字典:
    a = {
        'x' : 1,
        'y' : 2,
        'z' : 3
    }
    
    b = {
        'w' : 10,
        'x' : 11,
        'y' : 2
    }
          
    為了尋找兩個字典的相同點,可以簡單的在兩字典的 

    keys()

     或者 

    items()

     方法傳回結果上執行集合操作。比如:
    # Find keys in common
    a.keys() & b.keys() # { 'x', 'y' }
    # Find keys in a that are not in b
    a.keys() - b.keys() # { 'z' }
    # Find (key,value) pairs in common
    a.items() & b.items() # { ('y', 2) }
          
    這些操作也可以用于修改或者過濾字典元素。 比如,假如你想以現有字典構造一個排除幾個指定鍵的新字典。 下面利用字典推導來實作這樣的需求:
    # Make a new dictionary with certain keys removed
    c = {key:a[key] for key in a.keys() - {'z', 'w'}}
    # c is {'x': 1, 'y': 2}
          

    讨論

    一個字典就是一個鍵集合與值集合的映射關系。 字典的 

    keys()

     方法傳回一個展現鍵集合的鍵視圖對象。 鍵視圖的一個很少被了解的特性就是它們也支援集合操作,比如集合并、交、差運算。 是以,如果你想對集合的鍵執行一些普通的集合操作,可以直接使用鍵視圖對象而不用先将它們轉換成一個 set。

    字典的 

    items()

     方法傳回一個包含 (鍵,值) 對的元素視圖對象。 這個對象同樣也支援集合操作,并且可以被用來查找兩個字典有哪些相同的鍵值對。

    盡管字典的 

    values()

     方法也是類似,但是它并不支援這裡介紹的集合操作。 某種程度上是因為值視圖不能保證所有的值互不相同,這樣會導緻某些集合操作會出現問題。 不過,如果你硬要在值上面執行這些集合操作的話,你可以先将值集合轉換成 set,然後再執行集合運算就行了。

    1.10 删除序列相同元素并保持順序

    問題

    怎樣在一個序列上面保持元素順序的同時消除重複的值?

    解決方案

    如果序列上的值都是 

    hashable

     類型,那麼可以很簡單的利用集合或者生成器來解決這個問題。比如:
    def dedupe(items):
        seen = set()
        for item in items:
            if item not in seen:
                yield item
                seen.add(item)
          
    下面是使用上述函數的例子:
    >>> a = [1, 5, 2, 1, 9, 1, 5, 10]
    >>> list(dedupe(a))
    [1, 5, 2, 9, 10]
    >>>
          
    這個方法僅僅在序列中元素為 

    hashable

     的時候才管用。 如果你想消除元素不可哈希(比如 

    dict

    類型)的序列中重複元素的話,你需要将上述代碼稍微改變一下,就像這樣:
    def dedupe(items, key=None):
        seen = set()
        for item in items:
            val = item if key is None else key(item)
            if val not in seen:
                yield item
                seen.add(val)
          
    這裡的key參數指定了一個函數,将序列元素轉換成 

    hashable

     類型。下面是它的用法示例:
    >>> a = [ {'x':1, 'y':2}, {'x':1, 'y':3}, {'x':1, 'y':2}, {'x':2, 'y':4}]
    >>> list(dedupe(a, key=lambda d: (d['x'],d['y'])))
    [{'x': 1, 'y': 2}, {'x': 1, 'y': 3}, {'x': 2, 'y': 4}]
    >>> list(dedupe(a, key=lambda d: d['x']))
    [{'x': 1, 'y': 2}, {'x': 2, 'y': 4}]
    >>>
          
    如果你想基于單個字段、屬性或者某個更大的資料結構來消除重複元素,第二種方案同樣可以勝任。

    讨論

    如果你僅僅就是想消除重複元素,通常可以簡單的構造一個集合。比如:
    >>> a
    [1, 5, 2, 1, 9, 1, 5, 10]
    >>> set(a)
    {1, 2, 10, 5, 9}
    >>>
          

    然而,這種方法不能維護元素的順序,生成的結果中的元素位置被打亂。而上面的方法可以避免這種情況。

    在本節中我們使用了生成器函數讓我們的函數更加通用,不僅僅是局限于清單處理。 比如,如果如果你想讀取一個檔案,消除重複行,你可以很容易像這樣做:

    with open(somefile,'r') as f:
    for line in dedupe(f):
        ...
          
    上述key函數參數模仿了 

    sorted()

     , 

    min()

     和 

    max()

     等内置函數的相似功能。 可以參考 1.8 和 1.13 小節了解更多。

    1.11 命名切片

    問題

    如果你的程式包含了大量無法直視的寫死切片,并且你想清理一下代碼。

    解決方案

    假定你要從一個記錄(比如檔案或其他類似格式)中的某些固定位置提取字段:
    ######    0123456789012345678901234567890123456789012345678901234567890'
    record = '....................100 .......513.25 ..........'
    cost = int(record[20:23]) * float(record[31:37])
          
    與其那樣寫,為什麼不想這樣命名切片呢:
    SHARES = slice(20, 23)
    PRICE = slice(31, 37)
    cost = int(record[SHARES]) * float(record[PRICE])
          
    在這個版本中,你避免了使用大量難以了解的寫死下标。這使得你的代碼更加清晰可讀。

    讨論

    一般來講,代碼中如果出現大量的寫死下标會使得代碼的可讀性和可維護性大大降低。 比如,如果你回過來看看一年前你寫的代碼,你會摸着腦袋想那時候自己到底想幹嘛啊。 這是一個很簡單的解決方案,它讓你更加清晰的表達代碼的目的。

    内置的 

    slice()

     函數建立了一個切片對象。所有使用切片的地方都可以使用切片對象。比如:
    >>> items = [0, 1, 2, 3, 4, 5, 6]
    >>> a = slice(2, 4)
    >>> items[2:4]
    [2, 3]
    >>> items[a]
    [2, 3]
    >>> items[a] = [10,11]
    >>> items
    [0, 1, 10, 11, 4, 5, 6]
    >>> del items[a]
    >>> items
    [0, 1, 4, 5, 6]
          
    如果你有一個切片對象a,你可以分别調用它的 

    a.start

     , 

    a.stop

     , 

    a.step

     屬性來擷取更多的資訊。比如:
    >>> a = slice(5, 50, 2)
    >>> a.start
    5
    >>> a.stop
    50
    >>> a.step
    2
    >>>
          
    另外,你還可以通過調用切片的 

    indices(size)

     方法将它映射到一個已知大小的序列上。 這個方法傳回一個三元組 

    (start, stop, step)

     ,所有的值都會被縮小,直到适合這個已知序列的邊界為止。 這樣,使用的時就不會出現 

    IndexError

     異常。比如:
    >>> s = 'HelloWorld'
    >>> a.indices(len(s))
    (5, 10, 2)
    >>> for i in range(*a.indices(len(s))):
    ...     print(s[i])
    ...
    W
    r
    d
    >>>      

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

    問題

    怎樣找出一個序列中出現次數最多的元素呢?

    解決方案

    collections.Counter

     類就是專門為這類問題而設計的, 它甚至有一個有用的 

    most_common()

     方法直接給了你答案。

    為了示範,先假設你有一個單詞清單并且想找出哪個單詞出現頻率最高。你可以這樣做:

    words = [
        'look', 'into', 'my', 'eyes', 'look', 'into', 'my', 'eyes',
        'the', 'eyes', 'the', 'eyes', 'the', 'eyes', 'not', 'around', 'the',
        'eyes', "don't", 'look', 'around', 'the', 'eyes', 'look', 'into',
        'my', 'eyes', "you're", 'under'
    ]
    from collections import Counter
    word_counts = Counter(words)
    # 出現頻率最高的3個單詞
    top_three = word_counts.most_common(3)
    print(top_three)
    # Outputs [('eyes', 8), ('the', 5), ('look', 4)]
          

    讨論

    作為輸入, 

    Counter

     對象可以接受任意的由可哈希(

    hashable

    )元素構成的序列對象。 在底層實作上,一個 

    Counter

     對象就是一個字典,将元素映射到它出現的次數上。比如:
    >>> word_counts['not']
    1
    >>> word_counts['eyes']
    8
    >>>
          
    如果你想手動增加計數,可以簡單的用加法:
    >>> morewords = ['why','are','you','not','looking','in','my','eyes']
    >>> for word in morewords:
    ...     word_counts[word] += 1
    ...
    >>> word_counts['eyes']
    9
    >>>
          
    或者你可以使用 

    update()

     方法:
    >>> word_counts.update(morewords)
    >>>
          

    Counter

     執行個體一個鮮為人知的特性是它們可以很容易的跟數學運算操作相結合。比如:
    >>> a = Counter(words)
    >>> b = Counter(morewords)
    >>> a
    Counter({'eyes': 8, 'the': 5, 'look': 4, 'into': 3, 'my': 3, 'around': 2,
    "you're": 1, "don't": 1, 'under': 1, 'not': 1})
    >>> b
    Counter({'eyes': 1, 'looking': 1, 'are': 1, 'in': 1, 'not': 1, 'you': 1,
    'my': 1, 'why': 1})
    >>> # Combine counts
    >>> c = a + b
    >>> c
    Counter({'eyes': 9, 'the': 5, 'look': 4, 'my': 4, 'into': 3, 'not': 2,
    'around': 2, "you're": 1, "don't": 1, 'in': 1, 'why': 1,
    'looking': 1, 'are': 1, 'under': 1, 'you': 1})
    >>> # Subtract counts
    >>> d = a - b
    >>> d
    Counter({'eyes': 7, 'the': 5, 'look': 4, 'into': 3, 'my': 2, 'around': 2,
    "you're": 1, "don't": 1, 'under': 1})
    >>>
          
    毫無疑問, 

    Counter

     對象在幾乎所有需要制表或者計數資料的場合是非常有用的工具。 在解決這類問題的時候你應該優先選擇它,而不是手動的利用字典去實作。

    1.13 通過某個關鍵字排序一個字典清單

    問題

    你有一個字典清單,你想根據某個或某幾個字典字段來排序這個清單。

    解決方案

    通過使用 

    operator

     子產品的 

    itemgetter

     函數,可以非常容易的排序這樣的資料結構。 假設你從資料庫中檢索出來網站會員資訊清單,并且以下列的資料結構傳回:
    rows = [
        {'fname': 'Brian', 'lname': 'Jones', 'uid': 1003},
        {'fname': 'David', 'lname': 'Beazley', 'uid': 1002},
        {'fname': 'John', 'lname': 'Cleese', 'uid': 1001},
        {'fname': 'Big', 'lname': 'Jones', 'uid': 1004}
    ]
          
    根據任意的字典字段來排序輸入結果行是很容易實作的,代碼示例:
    from operator import itemgetter
    rows_by_fname = sorted(rows, key=itemgetter('fname'))
    rows_by_uid = sorted(rows, key=itemgetter('uid'))
    print(rows_by_fname)
    print(rows_by_uid)
          
    代碼的輸出如下:
    [{'fname': 'Big', 'uid': 1004, 'lname': 'Jones'},
    {'fname': 'Brian', 'uid': 1003, 'lname': 'Jones'},
    {'fname': 'David', 'uid': 1002, 'lname': 'Beazley'},
    {'fname': 'John', 'uid': 1001, 'lname': 'Cleese'}]
    [{'fname': 'John', 'uid': 1001, 'lname': 'Cleese'},
    {'fname': 'David', 'uid': 1002, 'lname': 'Beazley'},
    {'fname': 'Brian', 'uid': 1003, 'lname': 'Jones'},
    {'fname': 'Big', 'uid': 1004, 'lname': 'Jones'}]
          

    itemgetter()

     函數也支援多個 keys,比如下面的代碼
    rows_by_lfname = sorted(rows, key=itemgetter('lname','fname'))
    print(rows_by_lfname)
          
    會産生如下的輸出:
    [{'fname': 'David', 'uid': 1002, 'lname': 'Beazley'},
    {'fname': 'John', 'uid': 1001, 'lname': 'Cleese'},
    {'fname': 'Big', 'uid': 1004, 'lname': 'Jones'},
    {'fname': 'Brian', 'uid': 1003, 'lname': 'Jones'}]
          

    讨論

    在上面例子中, 

    rows

     被傳遞給接受一個關鍵字參數的 

    sorted()

     内置函數。 這個參數是 

    callable

    類型,并且從 

    rows

     中接受一個單一進制素,然後傳回被用來排序的值。 

    itemgetter()

     函數就是負責建立這個 

    callable

     對象的。

    operator.itemgetter()

     函數有一個被 

    rows

     中的記錄用來查找值的索引參數。可以是一個字典鍵名稱, 一個整形值或者任何能夠傳入一個對象的 

    __getitem__()

     方法的值。 如果你傳入多個索引參數給 

    itemgetter()

     ,它生成的 

    callable

     對象會傳回一個包含所有元素值的元組, 并且 

    sorted()

     函數會根據這個元組中元素順序去排序。 但你想要同時在幾個字段上面進行排序(比如通過姓和名來排序,也就是例子中的那樣)的時候這種方法是很有用的。

    itemgetter()

     有時候也可以用 

    lambda

     表達式代替,比如:
    rows_by_fname = sorted(rows, key=lambda r: r['fname'])
    rows_by_lfname = sorted(rows, key=lambda r: (r['lname'],r['fname']))
          
    這種方案也不錯。但是,使用 

    itemgetter()

     方式會運作的稍微快點。是以,如果你對性能要求比較高的話就使用 

    itemgetter()

     方式。

    最後,不要忘了這節中展示的技術也同樣适用于 

    min()

     和 

    max()

     等函數。比如:
    >>> min(rows, key=itemgetter('uid'))
    {'fname': 'John', 'lname': 'Cleese', 'uid': 1001}
    >>> max(rows, key=itemgetter('uid'))
    {'fname': 'Big', 'lname': 'Jones', 'uid': 1004}
    >>>      

    1.14 排序不支援原生比較的對象

    問題

    你想排序類型相同的對象,但是他們不支援原生的比較操作。

    解決方案

    内置的 

    sorted()

     函數有一個關鍵字參數 

    key

     ,可以傳入一個 

    callable

     對象給它, 這個 

    callable

    對象對每個傳入的對象傳回一個值,這個值會被 

    sorted

     用來排序這些對象。 比如,如果你在應用程式裡面有一個 

    User

     執行個體序列,并且你希望通過他們的 

    user_id

     屬性進行排序, 你可以提供一個以 

    User

     執行個體作為輸入并輸出對應 

    user_id

     值的 

    callable

     對象。比如:
    class User:
        def __init__(self, user_id):
            self.user_id = user_id
    
        def __repr__(self):
            return 'User({})'.format(self.user_id)
    
    
    def sort_notcompare():
        users = [User(23), User(3), User(99)]
        print(users)
        print(sorted(users, key=lambda u: u.user_id))
          
    另外一種方式是使用 

    operator.attrgetter()

     來代替 lambda 函數:
    >>> from operator import attrgetter
    >>> sorted(users, key=attrgetter('user_id'))
    [User(3), User(23), User(99)]
    >>>
          

    讨論

    選擇使用 lambda 函數或者是 

    attrgetter()

     可能取決于個人喜好。 但是, 

    attrgetter()

     函數通常會運作的快點,并且還能同時允許多個字段進行比較。 這個跟 

    operator.itemgetter()

     函數作用于字典類型很類似(參考1.13小節)。 例如,如果 

    User

     執行個體還有一個 

    first_name

     和 

    last_name

     屬性,那麼可以向下面這樣排序:

    同樣需要注意的是,這一小節用到的技術同樣适用于像 

    min()

     和 

    max()

     之類的函數。比如:
    >>> min(users, key=attrgetter('user_id'))
    User(3)
    >>> max(users, key=attrgetter('user_id'))
    User(99)
    >>>      

繼續閱讀