Выполнение подсчета, сортировки / сопоставления больших словарных статей

На этой неделе я участвую в "легком" Ежедневном испытании программистов на Reddit. . Описание находится по ссылке, но по сути задача состоит в том, чтобы прочитать текстовый файл по URL-адресу и подсчитать количество слов. Излишне говорить, что в результате получается довольно большой объект словаря. У меня есть несколько вопросов, в основном касающихся доступа или сортировки ключей по их значению.

Во-первых, я разработал код в соответствии с тем, что я сейчас понимаю в ООП и хорошем стиле 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

Итак, теперь у меня есть довольно точный и надежный подсчет слов, который, вероятно, не имеет дубликатов или пустых записей, но, тем не менее, является объектом dict, содержащим более 3k пар ключ / значение. Я не могу перебирать его, используя for k,v in count, или это дает мне исключение ValueError: too many values to unpack, которое исключает использование понимания списка или сопоставления с функцией для выполнения любого вида сортировки.

Я читал это HowTo on Sorting" rel="nofollow"> 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. PHEW! Итак, мои вопросы:

  1. Является ли dict лучшим типом данных для первоначального подсчета? Может ли быть предпочтительнее список кортежей, подобных тому, который возвращается count.items()? Но это, вероятно, замедлит его, не так ли?

  2. Это кажется «неуклюжим», поскольку я создаю dict, конвертирую его в список, содержащий кортежи, затем сортирую список и возвращаю новый список. Однако, насколько я понимаю, словари позволяют мне выполнять самый быстрый поиск, так что я что-то здесь упускаю?

  3. Я прочитал кратко о хешировании. Хотя я думаю, что понимаю, что суть в том, что хеширование позволит сэкономить место в памяти и позволит мне выполнять более быстрые поиски и сравнения, не будет ли компромисс в том, что программа станет более затратной в вычислительном отношении (более высокая загрузка ЦП), потому что это будет тогда будет вычислять хеши для каждого слова? Здесь уместно хеширование?

  4. Мы будем очень благодарны за любые отзывы о соглашениях об именах (в которых я ужасен) или любые другие предложения практически обо всем (включая стиль).


person JtheDude    schedule 07.12.2014    source источник
comment
Словарь - это хороший способ хранить значения word: count. Вы можете перебрать count, выполнив for k,v in count.iteritems().   -  person all or None    schedule 07.12.2014
comment
CodeReview - лучшее место для этого типа вопросов. А пока collections.Counter может упростить вам задачу.   -  person wwii    schedule 07.12.2014


Ответы (1)


Вы уверены, что for k,v in count: дает исключение ValueError: too many values to unpack? Я ожидаю, что это даст ValueError: need more than 1 value to unpack.

Когда вы используете dict в качестве итератора (например, в цикле for), вы просто получаете ключи, а не значения. Если вам нужны пары ключ-значение, вам нужно использовать метод dict iteritems(), как указано фиг в комментарии (или в Python 3 метод items()).

Конечно, всегда можно сделать что-то вроде:

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

...

Я думаю, что большинство ваших вопросов больше подходят для проверки кода, чем для переполнения стека. Но поскольку вы так хорошо спросили здесь, я упомяну несколько моментов. :)

Создавать строковый char с помощью char довольно неэффективно, поэтому для вашего метода alpha_only() было бы лучше, если бы он собирал символы в списке, а затем использовал метод str.join(), чтобы объединить их в одну строку. Обычная идиома Python сделает это, используя понимание списка.

Понимание списка в вашем count() методе вызывает alpha_only() дважды для каждого слова, что эффективно.

Вы можете упростить вызов strip(), используя аргумент по умолчанию, так как он удаляет все пробелы (и вам не нужно сохранять пробелы в этом приложении). Точно так же использование split() со своим аргументом по умолчанию будет разделяться на любые пробеги пустого пространства, что, вероятно, лучше в этом приложении, поскольку указание аргумента одного пробела означает, что вы получите несколько пустых строк в списке, возвращаемом с помощью 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) 
person PM 2Ring    schedule 07.12.2014
comment
Спасибо! Все это очень полезная информация, и я ценю, что вы нашли время ответить. Есть ли способ сделать репост в Code Review, или на данном этапе это не стоит того? - person JtheDude; 07.12.2014
comment
Что касается моего понимания списка, я не хотел помещать туда второй вызов alpha_only(), но в противном случае он возвращает None для любых элементов списка, которые являются числовыми или символами, чего я пытался избежать. Я полагаю, что это менее неэффективно, чем вызов его дважды, и мой цикл for не добавит никаких None ключей в dict, поэтому я вытащу его и перепишу код с вашими предложениями. Затем я отправлю обновленный код в Code Review :-) - person JtheDude; 07.12.2014
comment
@JtheDude: Звучит как хороший план. FWIW, вы можете прочитать о переносе вопросов на другие сообщества. Что касается дублированного звонка alpha_only(), отвечу. - person PM 2Ring; 07.12.2014
comment
Поскольку я в любом случае ловлю элементы списка со значением None в моем count() методе с ...elif word != None, будет ли излишним также ловить их с помощью цикла for? Я обновил alpha_only(), чтобы использовать понимание списка, которое выглядит как x = ''.join([x.lower() for x in word if x.isalpha()]), которое по-прежнему возвращает None для некоторых элементов списка, но выполняет все одним махом. Я просто вложил его в оператор if, который проверяет, что входящее слово на самом деле не является числом. - person JtheDude; 07.12.2014
comment
@JtheDude: Да, нет ничего плохого в том, чтобы поместить Nones в понимание списка и отловить их дальше по дорожке, поскольку вы обрабатываете данные построчно, так что список в любом случае не будет очень большим. Но в качестве общей стратегии полезно избегать сбора бесполезных данных. :) Кстати, лучше использовать if word is not None, чем if word != None. Обсуждается здесь. - person PM 2Ring; 07.12.2014
comment
Я старался не забывать об использовании памяти, поэтому не хотел выгружать всю электронную книгу в память с помощью read() или readlines(). Правильно ли я рассуждаю? Или все это уже сидит в памяти, как только я передаю его функции open()? Я прочитал документацию по open() и не помню, было ли что-нибудь, что указывало бы на тот или иной путь. Изменить: нашел ответ на stackoverflow.com/questions/2239888/ - person JtheDude; 07.12.2014
comment
@JtheDude: ОС действительно выполняет некоторую буферизацию файлов, поэтому, если файл достаточно мал, он может находиться в оперативной памяти после его открытия. Однако, что касается Python, данные файлов не считываются в память, пока вы их явно не прочитаете. Таким образом, более эффективно с точки зрения памяти обрабатывать текстовый файл построчно, как вы это делаете, вместо того, чтобы заглатывать все это с помощью read() или readlines(). OTOH, если вы не обрабатываете огромные электронные книги, они, вероятно, достаточно малы, чтобы прочитать их целиком, если вы захотите, и это может быть немного быстрее. - person PM 2Ring; 07.12.2014