LINUX.ORG.RU

Производительность генераторов и операций со списками

 ,


0

1

Как можно ещё ускорить выполнение такого скрипта:

#!/usr/bin/env python

import sys, difflib, re, os
from pathlib import Path


def regexp_compile (r):
    return re.compile(r, re.IGNORECASE)

def words_generator (buff):
    length = len(buff)
    start = 0
    while start < length:
        x = buff.find(b'\x00', start)
        yield buff[start:x].decode('utf-8')
        start = x + 9 # 1 byte for \0 + 4 bytes * 2 
        ## 32 bit for word index and 32 bit for word length


class RegexAccumulator:
    def __init__ (self, regexp):
        self.collection = list()
        f = lambda s: re.fullmatch(regexp, s) and s not in self.collection
        self.filter = f

    def accumulate (self, words):
        self.collection += list(filter(self.filter, words))

    def result (self):
        self.collection.reverse()
        return self.collection


class UsualAccumulator:
    def __init__ (self, word):
        self.word = word
        self.list1 = list()
        self.mlist = list()

        w = re.escape(word)
        regex1 = regexp_compile(".*" + w)
        self.regex2 = regexp_compile(w)

        self.rfilter = lambda s: re.match(regex1, s) and s not in self.list1
        self.mfilter = lambda s: s not in self.list1 and s not in self.mlist


    def accumulate (self, words):
        self.list1 += list(filter(self.rfilter, words))
        mlist = difflib.get_close_matches(self.word,
                                          words,
                                          n=400,
                                          cutoff=0.7)

        self.mlist += list(filter(self.mfilter, mlist))

    def result (self):
        result = list()
        list2 = list(filter(lambda s: re.match(self.regex2, s),
                            self.list1))

        if self.word in list2:
            result.append(word)
            list2 = list(filter(lambda s: s != word, list2))

        result += list2
        result += list(filter(lambda s: s not in result,
                              self.list1))
        result += list(filter(lambda s: s not in result,
                              self.mlist))

        result.reverse()
        return result


if __name__ == "__main__":

    word = sys.argv[1]
    if word == "-r" and len(sys.argv) > 2:
        accumulator = RegexAccumulator(sys.argv[2])

    else:
        accumulator = UsualAccumulator(word)


    files = Path(os.environ['STARDICT_DATA_DIR']).rglob("*.[iI][dD][xX]")

    for name in files:
        with open(name, 'rb') as f:
            ## My biggest .idx file ~ 11Mb so ...
            buff = f.read()
        accumulator.accumulate(list(words_generator(buff)))

    for word in accumulator.result():
        print(word)

Скрипт ищет слова в stardict словарях http://stardict-4.sourceforge.net/StarDictFileFormat секция {3}.

Очевидно что бутылочное горлышко в методах accumulate. Ещё может вместо words_generator и метода result у UsualAccumulator можно что-то более производительное приделать.

★★★★★

Убрать генератор несложно. Вы все равно его потом к списку приводите

def words_generator(buff):
    result = []
    length = len(buff)
    start = 0
    while start < length:
        x = buff.find(b'\x00', start)
        word = buff[start:x].decode('utf-8')
        result.append(word)
        start = x + 9 # 1 byte for \0 + 4 bytes * 2 
        ## 32 bit for word index and 32 bit for word length

    return result

Ну и местами вместо a_list += list(...), лучше делать a_list.extend(...)

Например

def accumulate (self, words):
        self.list1.extend(filter(self.rfilter, words))

Очевидно что бутылочное горлышко в методах accumulate

Очевидно же, что основные тормоза в регулярках.

Еще если слов достаточно много > 70-80+, то всякие self.list1, self.collection - лучше заменить на множествa(set).

В классе UsualAccumulator регулярки вообще не нужны. Все сводится примерно к этому

self.word_lower = self.word.lower()
... 
... = lambda s: self.word_lower == s.lower() and ... 
... = lambda s: self.word_lower in s.lower() and ...

Если words будет списком туплов вида

class Word(NamedTuple):
    value: str
    value_lower: str

то приведение регистра можно делать один раз, и в функциях фильтрации использовать value_lower, а в результате выводить исходное value, что чуть больше будет по памяти, но ускорит выполнение.

qaqa ★★
()
Ответ на: комментарий от qaqa

Убрать генератор несложно.

result.append(word)

Ну это очевидно, вопрос будет ли от этого плюс в производительности?

ados ★★★★★
() автор топика
Ответ на: комментарий от ados

будет ли от этуого плюс в производительности

Маловероятно, что будет заметно. Тема почему-то про генераторы и операции со списками. Поэтому написал.

qaqa ★★
()
Ответ на: комментарий от qaqa

то приведение регистра можно делать один раз

У меня этот скрипт не финальной версии и у окончательной будет выбор чувствительности к регистру.

ados ★★★★★
() автор топика
  1. Разве += не вызывает сначала +, потом =? Если да, то надо заменить на extend.

  2. a = list(filter(lambda s: s != word, b)) создание временного объекта(filter), двойная итерация (по b и по filter), вызов лямбды => a = [s for s in b if s != word] ну и с остальными так же

  3.      result += list(filter(lambda s: s not in result,
                               self.list1))
         result += list(filter(lambda s: s not in result,
                               self.mlist))
    

    Почему не сделать так?

    result = set()
    ...
    result.update(self.list1)
    result.update(self.list2)
    ...
    return sorted(result, lambda: <что тут тебе нужно>)
    

    Проверка на вхождение в список это O(N), вхождение в сет это O(1) в среднем случае.

  4.     self.rfilter = lambda s: re.match(regex1, s) and s not in self.list1
    ...
         def accumulate (self, words):
             self.list1 += list(filter(self.rfilter, words))
             mlist = difflib.get_close_matches(self.word,
                                           words,
                                           n=400,
                                           cutoff=0.7)
    
             self.mlist += list(filter(self.mfilter, mlist))
    
  5. Зачем вообще пихать в свойство объекта лямбду если там не меняющийся код? Сделай нормальный метод или вообще напиши код внутри accumulate

     def accumulate (self, words):
         # предварительно все же set используй
         self.list1.update(s for s in words if regex1.match(s))
         mlist = difflib.get_close_matches(self.word,
                                           words,
                                           n=400,
                                           cutoff=0.7)
         # То же самое - выкинуть лямбду и использовать сет вместо списка
         self.mlist += list(filter(self.mfilter, mlist))
    

Кароч тут кучу оптимизаций можно сделать. А если еще переменные называть не list1, list2, s, regex2 то наверное даже можно было бы понять что именно ты хотел сделать и написать нормальный код.

Aswed ★★★★★
()

Посмотри что занимает больше всего времени у тебя в программе. Сначала найди самое узкое место, а потом его оптимизируй. Чисто на глазок я думаю difflib.get_close_matches может медленно работать, если внутри питон а не батарейка. Но это не точно, надо смотреть.

А вообще перепиши всё на сеты.

peregrine ★★★★★
()
Последнее исправление: peregrine (всего исправлений: 1)
Ответ на: комментарий от qaqa

А что в интерпритатор не смогли завести += как extend в случае списков или там пара лишних тактов так существенна?

peregrine ★★★★★
()

А где можно самый толстый словарь взять? Чисто потестить? Как оно работает и где лагает.

peregrine ★★★★★
()
Ответ на: комментарий от WitcherGeralt

Первый достойный комментарий. Надо бы еще дождаться анонима с «забанься, …»

anonymous
()
Ответ на: комментарий от qaqa

Ну и местами вместо a_list += list(…), лучше делать a_list.extend(…)

На производительности никак не сказалось

ados ★★★★★
() автор топика
Ответ на: комментарий от peregrine

difflib

Да там чистый питон практически. Думал сложную логику там на C сделали.

ados ★★★★★
() автор топика
Ответ на: комментарий от ados

То есть тебе достаточно в списке уникальных слов из всех словарей сравнить каждое с каждым. Ты вроде примерно это и делаешь, только итерациями. Не проще будет «в лоб» реализовать?

i-rinat ★★★★★
()

А вообще кем работаешь/учишься если не секрет? Просто мне вообще подобная тема интересна в плане NLP задач и питона.

ЗЫ

Вообще обычно когда такой вопрос задают и хотят подробный ответ, то готовят минимально рабочий пример, чтобы можно было потыкать, походить отладчиком и посмотреть, а не то что весь алгоритм надо 3 раза прочитать чтобы правильно аргументы передать да и ещё прописать пути правильно чтобы работало всё.

peregrine ★★★★★
()
Последнее исправление: peregrine (всего исправлений: 1)
Ответ на: комментарий от qaqa

Спасибо всем за ответы. В результате снижение использования регулярок и, в особенности, перевод на множества дали заметное, но небольшое ускорение. Последнее значительно улучшило читаемость кода.

Но сильнее ускорил переход с difflib на rapidfuzz, благо последний получилось собрать на android. Получилось:

#!/usr/bin/env python

import sys, re, os
from pathlib import Path
from rapidfuzz import process as fuzz
from rapidfuzz import fuzz as fuzzmethod


def regexp_compile (r):
    return re.compile(r, re.IGNORECASE)

def words_generator (buff):
    length = len(buff)
    start = 0
    while start < length:
        x = buff.find(b'\x00', start)
        yield buff[start:x].decode('utf-8')
        start = x + 9 # 1 byte for \0 + 4 bytes * 2 
        ## 32 bit for word index and 32 bit for word length


class RegexAccumulator:
    def __init__ (self, regexp):
        self.collection = set()
        self.regexp = regexp_compile(regexp)

    def accumulate (self, words):
        self.collection.update(s for s in words if self.regexp.fullmatch(s))

    def result (self):
        result = sorted(self.collection)
        result.reverse()
        return result


class UsualAccumulator:
    def __init__ (self, word):
        self.word = word
        self.lword = word.lower()
        self.wlen = len(word)
        self.rset = set()
        self.dm_set = set()

        self.breg = regexp_compile(re.escape(word))

    def accumulate (self, words):
        self.rset.update(s for s in words if self.lword in s.lower())
        m = map(lambda x: x[0],
                fuzz.extract(self.word, words,
                             scorer=fuzzmethod.QRatio,
                             score_cutoff=70,
                             limit=400))

        self.dm_set.update(m)

    def result (self):
        result = list()
        self.dm_set.difference_update(self.rset)
        set2 = set()
        set2.update(s for s in self.rset if self.breg.match(s))
        self.rset.difference_update(set2)

        if self.word in set2:
            result.append(word)
            set2.discard(word)

        result.extend(sorted(set2))
        result.extend(sorted(self.rset))
        result.extend(sorted(self.dm_set))

        result.reverse()
        return result


if __name__ == "__main__":

    word = sys.argv[1]
    if word == "-r" and len(sys.argv) > 2:
        accumulator = RegexAccumulator(sys.argv[2])

    else:
        accumulator = UsualAccumulator(word)

    files = Path(os.environ['STARDICT_DATA_DIR']).rglob("*.[iI][dD][xX]")

    for name in files:
        with open(name, 'rb') as f:
            ## My biggest .idx file ~ 11Mb so ...
            buff = f.read()
        accumulator.accumulate(list(words_generator(buff)))

    for word in accumulator.result():
        print(word)

На тесте перевод на множество сократил время работы с 4 минут до чуть более 3 минут, а rapidfuzz довёл время выполнения до ~ 35 сек.

ados ★★★★★
() автор топика
Последнее исправление: ados (всего исправлений: 1)
Ответ на: комментарий от Aswed

А если еще переменные называть не list1, list2, s, regex2 то наверное даже можно было бы понять что именно ты хотел сделать и написать нормальный код.

Не понимаю, зачем заморачиваться если смысл идентификатора не лезет дальше одного экрана кода. Особенно если имя больше чем в 2-3 строках не используется.

ados ★★★★★
() автор топика
Ответ на: комментарий от peregrine

А вообще кем работаешь/учишься если не секрет?

Безработный

Вообще обычно когда такой вопрос задают и хотят подробный ответ, то готовят минимально рабочий пример, чтобы можно было потыкать, походить отладчиком и …

Прошу прощенья. Всё это написано здесь:

    word = sys.argv[1]
    if word == "-r" and len(sys.argv) > 2:
        accumulator = RegexAccumulator(sys.argv[2])

    else:
        accumulator = UsualAccumulator(word)

    files = Path(os.environ['STARDICT_DATA_DIR']).rglob("*.[iI][dD][xX]")

Скрипту нужно дать слово в аргументах и он будет прочёсывать словари которые, как и sdcv, ищет в директории по переменной окружения STARDICT_DATA_DIR. Просто считается, что python легко читать.

ados ★★★★★
() автор топика
Ответ на: комментарий от ados

Например что бы форумчанин, к которому ты обращаешься за помощью, понял что тут вообще происходит. Или что бы ты сам, через месяц заглядывая в код, понял, что тут вообще происходит.

Aswed ★★★★★
()

тред почти не читал, но если тебе нужен нечёткий поиск по списку из слов, то возможно тебе нужно это https://github.com/seatgeek/fuzzywuzzy

правда, без регулярок, зато быстро

anonymous
()

Что только не придумают лишь бы не пользоваться профайлером

cobold ★★★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.