cookbook:(1).数据结构和算法(2/3)

  • 转载自: 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'
    

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

    min_value = prices[min(prices, key=lambda k: prices[k])]
    

    前面的 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 属性,那么可以向下面这样排序:

    by_name = sorted(users, key=attrgetter('last_name', 'first_name'))
    

    同样需要注意的是,这一小节用到的技术同样适用于像 min() 和 max() 之类的函数。比如:

    >>> min(users, key=attrgetter('user_id'))
    User(3)
    >>> max(users, key=attrgetter('user_id'))
    User(99)
    >>>

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值