2018-03-01数据结构和算法(3)
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)] >>>
同样需要注意的是,这一小节用到的技术同样适用于像 min()
和 max()
之类的函数。比如:
>>> min(users, key=attrgetter('user_id')) User(3) >>> max(users, key=attrgetter('user_id')) User(99) >>>
1.15通过某个字段将记录分组
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 分组后的数据块上进行迭代。为了这样做,你首先需要按照指定的字段(这里就是 date
)排序, 然后调用 itertools.groupby()
函数:
from operator import itemgetter from itertools import 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 {'date': '07/01/2012', 'address': '5412 N CLARK'} {'date': '07/01/2012', 'address': '4801 N BROADWAY'} 07/02/2012 {'date': '07/02/2012', 'address': '5800 E 58TH'} {'date': '07/02/2012', 'address': '5645 N RAVENSWOOD'} {'date': '07/02/2012', 'address': '1060 W ADDISON'} 07/03/2012 {'date': '07/03/2012', 'address': '2122 N CLARK'} 07/04/2012 {'date': '07/04/2012', 'address': '5148 N CLARK'} {'date': '07/04/2012', 'address': '1039 W GRANVILLE'}
groupby()
函数扫描整个序列并且查找连续相同值(或者根据指定 key 函数返回值相同)的元素序列。 在每次迭代的时候,它会返回一个值和一个迭代器对象, 这个迭代器对象可以生成元素值全部等于上面那个值的组中所有对象。
一个非常重要的准备步骤是要根据指定的字段将数据排序。 因为 groupby()
仅仅检查连续的元素,如果事先并没有排序完成的话,分组函数将得不到想要的结果。
如果你仅仅只是想根据 date
字段将数据分组到一个大的数据结构中去,并且允许随机访问, 那么你最好使用 defaultdict()
来构建一个多值字典,关于多值字典已经在 1.6 小节有过详细的介绍。比如:
from collections import defaultdict rows_by_date = defaultdict(list) for row in rows: rows_by_date[row['date']].append(row)
这样的话你可以很轻松的就能对每个指定日期访问对应的记录:
>>> for r in rows_by_date['07/01/2012']: ... print(r) ... {'date': '07/01/2012', 'address': '5412 N CLARK'} {'date': '07/01/2012', 'address': '4801 N BROADWAY'} >>>
在上面这个例子中,我们没有必要先将记录排序。因此,如果对内存占用不是很关心, 这种方式会比先排序然后再通过 groupby()
函数迭代的方式运行得快一些。