执行计数、排序/映射大字典

Posted

技术标签:

【中文标题】执行计数、排序/映射大字典【英文标题】:Performing Counts, Sorting/mapping Large Dicts 【发布时间】:2015-02-05 00:55:50 【问题描述】:

我正在 Reddit 上做这周的“简单”Daily Programmer Challenge。描述在链接上,但本质上的挑战是从 url 读取文本文件并进行字数统计。不用说,结果输出是一个相当大的字典对象。我有几个问题,主要是关于根据值访问或排序键。

首先,我根据我目前对 OOP 和良好 Python 风格的理解开发了代码。我希望它尽可能健壮,但我也希望使用最少数量的导入模块。我的目标是成为一名优秀的程序员,因此我认为打下坚实的基础并尽可能自己弄清楚如何做事很重要。话虽如此,代码:

from urllib2 import urlopen

class Word(object):

    def __init__(self):
        self.word_count =     

    def alpha_only(self, word):
        """Converts word to lowercase and removes any non-alphabetic characters."""
        x = ''
        for letter in word:
            s = letter.lower()
            if s in 'abcdefghijklmnopqrstuvwxyz':
                x += s
        if len(x) > 0:
            return x    

    def count(self, line):
        """Takes a line from the file and builds a list of lowercased words containing only alphabetic chars.
            Adds each word to word_count if not already present, if present increases the count by 1."""
        words = [self.alpha_only(x) for x in line.split(' ') if self.alpha_only(x) != None]
        for word in words:
            if word in self.word_count:
                self.word_count[word] += 1
            elif word != None:
                self.word_count[word] = 1

class File(object):

    def __init__(self,book):
        self.book = urlopen(book)               
        self.word = Word()

    def strip_line(self,line):
        """Strips newlines, tabs, and return characters from beginning and end of line. If remaining string > 1,
            splits up the line and passes it along to the count method of the word object."""
        s = line.strip('\n\r\t')
        if s > 1:
            self.word.count(s)

    def process_book(self):
        """Main processing loop, will not begin processing until the first line after the line containing "START".
            After processing it will close the file."""
        begin = False
        for line in self.book:
            if begin == True:
                self.strip_line(line)
            elif 'START' in line:
                begin = True
        self.book.close()

book = File('http://www.gutenberg.org/cache/epub/47498/pg47498.txt')

book.process_book()

count = book.word.word_count

所以现在我有一个相当准确和强大的字数统计,可能没有任何重复或空白条目,但仍然是一个包含超过 3k 键/值对的 dict 对象。我无法使用 for k,v in count 对其进行迭代,或者它给了我异常 ValueError: too many values to unpack,它排除了使用列表推导或映射到函数来执行任何类型的排序。

几分钟前我正在阅读此HowTo on Sorting 并使用它并注意到for x in count.items() 让我可以遍历键/值对列表而不会引发ValueError 异常,因此我删除了count = book.word.word_count 和添加了以下内容:

s_count = sorted(book.word.word_count.items(), key=lambda count: count[1], reverse=True)

# Delete the original dict, it is no longer needed
del book.word.word_count

现在我终于有了一个排序的单词列表,s_count。呸!所以,我的问题是:

    dict 甚至是执行原始计数的最佳数据类型吗?像count.items() 返回的元组列表会更好吗?但这可能会减慢速度,对吧?

    这似乎有点“笨拙”,因为我正在构建一个 dict,将其转换为包含元组的列表,然后对列表进行排序并返回一个新列表。但是,据我了解,字典允许我执行最快的查找,所以我在这里遗漏了什么吗?

    我简要地阅读了有关散列的内容。虽然我认为我理解这一点是散列将节省内存空间并允许我执行更快的查找和比较,但权衡不会是程序变得计算成本更高(更高的 CPU 负载),因为它会然后计算每个单词的哈希值?散列在这里有用吗?

    任何关于命名约定的反馈(我不擅长),或任何其他关于基本上任何事情(包括样式)的建议,将不胜感激。

【问题讨论】:

字典是存储 word:count 值的好方法。您可以通过执行 for k,v in count.iteritems() 来迭代 count CodeReview 是此类问题的最佳选择。与此同时,collections.Counter 可能会让您的事情变得更轻松。 【参考方案1】:

你确定for k,v in count: 给出了异常ValueError: too many values to unpack 吗?我希望它能给ValueError: need more than 1 value to unpack

当您使用dict 作为迭代器(例如在for 循环中)时,您只获得键,而不会获得值。如果你想要键值对,你需要使用dictiteritems() 方法,如注释中的无花果所提到的(或在Python 3 中的items() 方法)。

当然,你总是可以这样做:

for k in count:
    print k, count[k]

...

我认为您的大多数问题更适合Code Review,而不是 Stack Overflow。但是既然你在这里问得很好,我会提几点。 :)

逐个字符构建字符串的效率相当低,因此如果您的alpha_only() 方法将字符收集到列表中然后使用str.join() 方法将它们连接成单个字符串会更好。通常的 Python 习惯用法会使用列表推导来做到这一点。

count() 方法中的列表推导对每个单词调用两次 alpha_only(),效率很高。

您可以使用默认参数使您的strip() 调用更简单,因为这会去除所有空白(并且您不需要在此应用程序中保留空格字符)。同样,使用 split() 及其默认 arg 将在任何运行的空白空间上拆分,这在此应用程序中可能更好,因为提供单个空格的 arg 意味着您将在 split 返回的列表中获得一些空字符串如果一行中有多个空格。

...

您在问题中提到了散列,以及它是否对这个应用程序有用。是的。 Python 字典实际上使用其键的散列,因此您无需担心细节。是的,字典是用于此任务的良好数据结构。有一些更奇特的字典形式可以让事情变得更简单,但要使用它们确实需要导入一个(标准)模块。但是使用字典(某种风格)来保存数据,然后从中生成一个元组列表以进行最终排序是 Python 中相当普遍的做法。如果程序即将终止,则无需在完成字典后专门删除它。

...

至于 alpha_only() 的重复调用,每当您发现自己在做这种事情时,这表明列表推导并不真正适合该任务,您应该只使用普通的 for 循环,所以您可以保存函数调用的结果,而不必重新计算它。例如,

words = []
for word in line.split():
    word = self.alpha_only(word)
    if word is not None:
        words.append(word) 

【讨论】:

谢谢!这些都是非常有用的信息,感谢您抽出宝贵时间做出回应。有没有办法可以在 Code Review 中重新发布,或者此时不值得? 关于我的列表理解,我不愿意在其中对alpha_only() 进行第二次调用,否则它会为我正在尝试的任何数字或符号列表项返回None避免。我想这比调用它两次效率低,而且我的for 循环不会将任何None 键添加到字典中,所以我将其取出,并根据您的建议重写代码。然后我会将更新后的代码发布到 Code Review :-) @JtheDude:这听起来是个好计划。 FWIW,您可以向其他社区阅读有关migration 的问题。至于alpha_only()的重复电话,我会在我的回答中回复。 既然我在我的count() 方法中使用...elif word != None 捕获了值为None 的列表项,那么用for 循环捕获它们是否是多余的?我更新了alpha_only() 以使用类似于x = ''.join([x.lower() for x in word if x.isalpha()]) 的列表理解,它仍然为某些列表项返回None,但一举完成所有操作。我只是将它嵌套在 if 语句中,以确保传入的单词实际上不是数字。 @JtheDude:是的,将Nones 放入列表理解并在跟踪中进一步捕捉它们没有任何问题,因为您正在逐行处理数据,所以列表不是反正会很大。但作为一般策略,最好避免收集无用的数据。 :) 顺便说一句,使用if word is not None 比使用if word != None 更好。关于这个here有一些讨论。

以上是关于执行计数、排序/映射大字典的主要内容,如果未能解决你的问题,请参考以下文章

大数据MapReduce(单词计数;二次排序;计数器;join;分布式缓存)

计数排序及其时间复杂度代码(C++实现)应用场景

多图计数排序竟如此简单!

计数排序(桶排序)----《程序员小灰》

排序5-计数排序

6. python 查漏补缺,namedtuple 命名元组,双向队列 deque,Counter 计数器,可排序字典