美文网首页
Python Cookbook —— 数据结构技巧

Python Cookbook —— 数据结构技巧

作者: rollingstarky | 来源:发表于2019-10-27 03:06 被阅读0次

    一、序列展开与多重赋值

    任何数据序列(或可迭代对象)都可以只通过一个赋值操作展开自身并同时赋值给多个变量。只需要确保被赋值的变量的数目和结构与序列相符合即可。如:

    >>> p = (4, 5)
    >>> x, y = p
    >>> x
    4
    >>> y
    5
    

    对于嵌套的多层次序列,此种方式的多重赋值仍然适用:

    >>> data = [ 'ACME', 50, 91.1, (2012, 12, 21) ]
    >>> name, shares, price, date = data
    >>> name
    'ACME'
    >>> date
    (2012, 12, 21)
    >>> name, shares, price, (year, mon, day) = data
    >>> name
    'ACME'
    >>> year
    2012
    >>> mon
    12
    >>> day
    21
    

    序列展开同样适用于任何可迭代对象(即内部实现了 __iter__ 方法的对象,如字符串等),示例如下:

    >>> s = 'Hello'
    >>> a, b, c, d, e = s
    >>> a
    'H'
    >>> b
    'e'
    >>> e
    'o'
    

    忽略特定的值
    在序列展开并赋值时,可以忽略指定项目,方法如下:

    >>> data = [ 'ACME', 50, 91.1, (2012, 12, 21) ]
    >>> _, shares, price, _ = data
    >>> shares
    50
    >>> price
    91.1
    
    展开为固定长度

    在序列展开并赋值时,被赋值的变量数目和结构如果与序列本身不符合,则会报出 ValueError

    >>> p = (4, 5, 6)
    >>> x, y = p
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    ValueError: too many values to unpack (expected 2)
    

    如上面的示例,当展开的序列长度大于期望的变量数目时,可以对变量使用 * 操作符将多个项目保存在列表结构中,如:

    def drop_first_last(grades):
        first, *middle, last = sorted(grades)
        return sum(middle) / len(middle)
    
    grades = [ 100, 94, 96, 88, 70, 62, 80 ]
    print(f"{ drop_first_last(grades) }")
    # => 85.6
    print(sum([94, 96, 88, 70, 80]) / 5)
    # => 85.6
    

    其他应用示例如:

    >>> record = ('Dave', 'dave@example.com', '773-555-1212', '847-555-1212')
    >>> name, email, *phone_numbers = record
    >>> name
    'Dave'
    >>> email
    'dave@example.com'
    >>> phone_numbers
    ['773-555-1212', '847-555-1212']
    

    * 操作符的特性使其可以很方便地应用在字符串分割中:

    >>> line = 'nobody:*:-2:-2:Unprivileged User:/var/empty:/usr/bin/false'
    >>> uname, *fields, homedir, sh = line.split(':')
    >>> uname
    'nobody'
    >>> homedir
    '/var/empty'
    >>> sh
    '/usr/bin/false'
    

    结合 _ 还可以写出如下代码以在赋值时忽略序列中的多个项目:

    >>> record = ('ACME', 50, 123.45, (12, 18, 2012))
    >>> name, *_, (*_, year) = record
    >>> name
    'ACME'
    >>> year
    2012
    

    二、检索序列中最大或最小的 N 个项目

    Python 的 heapq 模块包含 nlargest()nsmallest() 函数,可以用来筛选某个数据序列中最大或最小的 N 个值。

    import heapq
    
    nums = [1, 8, 2, 23, 7, -4, 18, 23, 42, 37, 2]
    print(heapq.nlargest(3, nums))
    print(heapq.nsmallest(3, nums))
    
    # => [42, 37, 23]
    # => [-4, 1, 2]
    

    上面两个函数也可以接收一个名为 key 的参数,使其可以应用在更复杂的数据结构中:

    import heapq
    
    portfolio = [
        {'name': 'IBM', 'shares': 100, 'price': 91.1},
        {'name': 'AAPL', 'shares': 50, 'price': 543.22},
        {'name': 'FB', 'shares': 200, 'price': 21.09},
        {'name': 'HPQ', 'shares': 35, 'price': 31.75},
        {'name': 'YHOO', 'shares': 45, 'price': 16.35},
        {'name': 'ACME', 'shares': 75, 'price': 115.65}
    ]
    
    cheap = heapq.nsmallest(2, portfolio, key=lambda s: s['price'])
    expensive = heapq.nlargest(2, portfolio, key=lambda s: s['price'])
    
    print(cheap)
    # => [{'name': 'YHOO', 'shares': 45, 'price': 16.35}, {'name': 'FB', 'shares': 200, 'price': 21.09}]
    print(expensive)
    # => [{'name': 'AAPL', 'shares': 50, 'price': 543.22}, {'name': 'ACME', 'shares': 75, 'price': 115.65}]
    

    heapq 中的 nlargest()nsmallest() 两个函数的原理都是将数据序列转换为列表结构并且以heap)的形式进行组织。
    heap 最重要的属性为, heap[0] 永远是序列中最小的值。使用 heapq.heappop() 方法可以获取 heap 中的第一个值(即最小值),而之前第二小的值则移动到第一的位置。即不断调用 heappop() 可以一直获取当前序列中最小的值。

    >>> nums = [1, 8, 2, 23, 7, -4, 18, 23, 42, 37, 2]
    >>> import heapq
    >>> heapq.heapify(nums)
    >>> nums
    [-4, 2, 1, 23, 7, 2, 18, 23, 42, 37, 8]
    >>> heapq.heappop(nums)
    -4
    >>> heapq.heappop(nums)
    1
    >>> heapq.heappop(nums)
    2
    >>> heapq.heappop(nums)
    2
    >>> heapq.heappop(nums)
    7
    

    PS:如果只想检索某一个最大值或最小值,min() 或者 max() 更快;
    如果 nlargest(N, items)nsmallest(N, items) 中的 N 与 items 集合的大小很接近,则先对集合进行排序再分片的方式更快一点,即 sorted(itmes)[:N]
    (实际 nlargestnsmallest 内部本身也是这样实现的)

    三、实现一个加权队列

    以下的代码实现了一种支持加权的队列,即队列中的项目会按指定的权重排序,并且每次调用 pop 方法都可以返回权重最高的项目。

    import heapq
    
    class PriorityQueue:
        def __init__(self):
            self._queue = []
            self._index = 0
    
        def push(self, item, priority):
            heapq.heappush(self._queue, (-priority, self._index, item))
            self._index += 1
    
        def pop(self):
            return heapq.heappop(self._queue)[-1]
    
    # test code
    q = PriorityQueue()
    q.push('foo', 1)
    q.push('bar', 5)
    q.push('spam', 4)
    q.push('grok', 1)
    
    print(q.pop())
    # => bar
    print(q.pop())
    # => spam
    print(q.pop())
    # => foo
    print(q.pop())
    # => grok
    

    其中 heapq.heappush() 函数可以向 _queue 序列中插入数据,之后再使用 heapq.heappop() 函数获取序列中的数据时,总能保证取出的数据是当时队列中最小的那个。pop 和 push 操作的复杂度为 O(logN),比普通列表形式的操作(复杂度为 O(N))效率更高。
    同时数据是以元组 (-priority, _index, item) 的形式存入到 _queue 队列中的,元组可以依据自身的每个数据项依次进行比对并确定大小关系。因而权重 -priority 可以作为排序的首要依据(加负号是为了使权重高的值更小,可以优先被 pop() 返回)。当权重一样即 -priority 的值相同时,则根据插入的顺序(_index)返回数据。

    四、比较字典中的 Value

    参考下面的代码示例:

    prices = {
        'ACME': 45.23,
        'AAPL': 612.78,
        'IBM': 205.55,
        'HPQ': 37.20,
        'FB': 10.75
    }
    
    print(min(prices))  # => 'AAPL'
    print(max(prices))  # => 'IBM'
    

    从输出中可以看出,min(prices)max(prices) 函数只是处理字典 prices 中的 keys,对 values 则不做任何操作。
    可以将其改为如下形式:

    min(prices.values())  # => 10.75
    max(prices.values())  # => 612.78
    

    则此时上述两个函数又只对字典中的 values 有效,输出的结果中也只包含 values,不包含与之关联的 key 的值。

    如果想根据 values 对字典中的数据进行排序,同时输出的结果中既包含 value,又包含与之关联的 key。则可以使用 zip() 函数。
    zip() 函数以多个可迭代的对象作为参数,将各对象中位置对应的元素打包成一个个元组。
    回到前面的需求,则可以将字典的 keys 和 values 拆分到两个列表中,再通过 zip() 函数将其中的数据合并成一个个元组(等同于之前的键值对),而 value 作为元组的第一个元素。

    prices = {
        'ACME': 45.23,
        'AAPL': 612.78,
        'IBM': 205.55,
        'HPQ': 37.20,
        'FB': 10.75
    }
    
    for item in zip(prices.values(), prices.keys()):
        print(item)
    # => (45.23, 'ACME')
    # => (612.78, 'AAPL')
    # => (205.55, 'IBM')
    # => (37.2, 'HPQ')
    # => (10.75, 'FB')
    
    max_price = max(zip(prices.values(), prices.keys()))
    print(max_price)
    # => (612.78, 'AAPL')
    

    五、去除序列中的重复元素

    集合(set)是 Python 中的一种数据结构,它包含了一系列无序不重复元素。因此可以通过将其他类型的数据转为 set 类型,为序列中的数据去重。
    但此种方法不能保留原序列中数据项原本的排列顺序(因为 set 是无序的)。

    >>> a = [1, 5, 2, 1, 9, 1, 5, 10]
    >>> set(a)
    {1, 2, 5, 9, 10}
    

    下面的函数 dedupe 则实现了去重并保留原本的排列顺序:

    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]
    print(list(dedupe(a)))
    
    # => [1, 5, 2, 9, 10]
    

    而对于更复杂的数据结构,比如对如下的列表进行去重操作:
    [{'x':1, 'y':2}, {'x':1, 'y':3}, {'x':1, 'y':2}, {'x':2, 'y':4}]

    则可以将 dedupe 函数改为如下形式:

    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)
    
    a = [{'x':1, 'y':2}, {'x':1, 'y':3}, {'x':1, 'y':2}, {'x':2, 'y':4}]
    print(list(dedupe(a, key=lambda d: (d['x'],d['y']))))
    # => [{'x': 1, 'y': 2}, {'x': 1, 'y': 3}, {'x': 2, 'y': 4}]
    
    print(list(dedupe(a, key=lambda d: (d['x']))))
    # => [{'x': 1, 'y': 2}, {'x': 2, 'y': 4}]
    

    额,自行体会吧。。。

    六、找出序列中最常出现的项

    collections.Counter 类可以用来寻找序列中出现次数最多的几个项目。

    from collections import Counter
    
    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'
    ]
    
    word_counts = Counter(words)
    top_three = word_counts.most_common(3)
    print(top_three)
    # => [('eyes', 8), ('the', 5), ('look', 4)]
    
    morewords = ['why', 'are', 'you', 'not', 'looking', 'in', 'my', 'eyes']
    word_counts.update(morewords)
    print(word_counts.most_common(3))
    # => [('eyes', 9), ('the', 5), ('look', 4)]
    
    a = Counter(words)
    b = Counter(morewords)
    print(a + b)
    # => Counter({'eyes': 9, 'the': 5, 'look': 4, 'my': 4, 'into': 3, 'not': 2, 'around': 2, "don't": 1, "you're": 1, 'under': 1, 'why': 1, 'are': 1, 'you': 1, 'looking': 1, 'in': 1})
    

    参考资料

    Python Cookbook, 3rd Edition

    相关文章

      网友评论

          本文标题:Python Cookbook —— 数据结构技巧

          本文链接:https://www.haomeiwen.com/subject/thcopctx.html