Есть ли способ сделать этот код быстрее? - PullRequest
2 голосов
/ 09 ноября 2019

Я пишу эту поисковую систему на python для списка рецептов, и она должна работать с определенной скоростью в течение поиска не более 0,1 секунды. Я изо всех сил пытался достичь этой скорости с моим кодом. Я получаю 0,4 в среднем. Я хотел бы знать, есть ли у вас какие-либо идеи о том, как сделать этот код быстрее. Я пробовал очень много вещей, но я знаю, что цикл - это то, что делает его медленнее. Если я смогу улучшить его, в основном с помощью Python и не добавляя столько модулей.

Я уже сделал это быстрее в других частях кода на 0,005 ср. но в этой части с большим количеством рецептов это становится довольно медленным.

def countTokens(token):
    token = str(token).lower()

    #make digits an punctuations white spaces
    tokens = token.translate(token.maketrans(digits + punctuation,\
            " "*len(digits + punctuation))) 

    return tokens.split(" ")

def normalOrder(recipes, queries):
    for r in recipes:
        k = r.keys()  
        parts, scores = [[],[],[],[]], 0
        parts[0] = countTokens(r["title"])
        parts[1] = countTokens(r["categories"]) if "categories" in k else []
        parts[2] = countTokens(r["ingredients"]) if "ingredients" in k else []
        parts[3] = countTokens(r["directions"]) if "directions" in k else []
        for q in queries:
            scores += 8 * parts[0].count(q) + 4 * parts[1].count(q) + 2 * parts[2].count(q) + 1 * parts[3].count(q)

        r["score"] = scores + r["rating"] if "rating" in k else 0
    return recipes

В небольшом контексте мне нужно суммировать количество вхождений запросов в четырех описанных выше дескрипторах, только если они есть, поэтому у меня есть if.

Ответы [ 2 ]

3 голосов
/ 09 ноября 2019

Я заметил несколько моментов:

  • Каждый раз, когда вы звоните countTokens, вы снова генерируете одну и ту же таблицу перевода (вызов maketrans). Я думаю, что это не будет оптимизировано, поэтому вы, вероятно, теряете производительность там.
  • tokens.split(" ") создает список всех слов в строке, что довольно дорого, например, когда строка составляет 100.000 слов. Вам это не нужно.
  • В целом, похоже, вы просто пытаетесь подсчитать, как часто слово содержится в строке. Используя string.count(), вы можете считать события с гораздо меньшими накладными расходами.

Если вы примените это, вам больше не нужна функция countTokens, а сНемного рефакторинга в итоге:

def normalOrder(recipes, queries):
    for recipe in recipes:
        recipe["score"] = recipe.get("rating", 0)

        for query in queries:
            recipe["score"] += (
                8 * recipe["title"].lower().count(query)
                + 4 * recipe["categories"].lower().count(query)
                + 2 * recipe["ingredients"].lower().count(query)
                + 1 * recipe["directions"].lower().count(query)
            )

    return recipes

Работает ли это для вас - и достаточно ли это быстро?

Редактировать: В исходном коде вы завернули доступ к recipe["title"] и другие строки в другом str() вызове. Я предполагаю, что они уже являются строкой? Если это не так, вам нужно добавить это здесь.


Edit2: Вы указали в комментариях, что пунктуация является проблемой. Как я уже говорил в комментариях, я думаю, вам не нужно об этом беспокоиться, поскольку вызовы count будут заботиться только о знаках препинания, если и слово запроса, и текст рецепта содержат один, то вызов count будет считать толькослучаи, когда окружающая пунктуация совпадает с тем, что запрашивается. Взгляните на эти примеры:

>>> "Some text, that...".count("text")
1
>>> "Some text, that...".count("text.")
0
>>> "Some text, that...".count("text,")
1

Если вы хотите, чтобы это происходило по-другому, вы можете сделать что-то похожее на исходный вопрос: создайте таблицу перевода и примените ее. Имейте в виду, что применение этого перевода к текстам рецептов (как вы сделали в своем вопросе) не имеет особого смысла, с тех пор любое слово запроса, которое содержит знаки препинания, просто никогда не будет совпадать. Это можно сделать намного проще, просто игнорируя все слова запроса, которые содержат знаки препинания. Вы, вероятно, захотите выполнить перевод для термина запроса, чтобы, если кто-то вводит слово «potato», вы обнаруживаете все случаи появления «potato». Это будет выглядеть так:

def normalOrder(recipes, queries):
    translation_table = str.maketrans(digits + punctuation, " " * len(digits + punctuation))
    for recipe in recipes:
        recipe["score"] = recipe.get("rating", 0)

        for query in queries:
            replaced_query = query.translate(translation_table)
            recipe["score"] += (
                8 * recipe["title"].lower().count(replaced_query)
                + 4 * recipe["categories"].lower().count(replaced_query)
                + 2 * recipe["ingredients"].lower().count(replaced_query)
                + 1 * recipe["directions"].lower().count(replaced_query)
            )

    return recipes

Edit3: В комментариях вы указали, что хотите, чтобы поиск ["honey", "lemon"] совпадал с "honey-lemon", ночто вы не хотите, чтобы «сливочное масло» соответствовало «сливочным пальцам». Для этого ваш первоначальный подход, вероятно, является лучшим решением, но имейте в виду, что поиск единственной формы «картошка» больше не будет соответствовать форме множественного числа («картошка») или любой другой производной форме.

def normalOrder(recipes, queries):
    transtab = str.maketrans(digits + punctuation, " " * len(digits + punctuation))
    for recipe in recipes:
        recipe["score"] = recipe.get("rating", 0)

        title_words = recipe["title"].lower().translate(transtab).split()
        category_words = recipe["categories"].lower().translate(transtab).split()
        ingredient_words = recipe["ingredients"].lower().translate(transtab).split()
        direction_words = recipe["directions"].lower().translate(transtab).split()

        for query in queries:
            recipe["score"] += (
                8 * title_words.count(query)
                + 4 * category_words.count(query)
                + 2 * ingredient_words.count(query)
                + 1 * direction_words.count(query)
            )

    return recipes

Если вы чаще вызываете эту функцию с одними и теми же получателями, вы можете повысить ее производительность, сохраняя результаты .lower().translate().split() в получателях, и вам не нужно повторно создавать этот список при каждом вызове.

В зависимости от ваших входных данных (сколько запросов у вас в среднем?), Также может иметь смысл просто выполнить хотя бы один результат split() и просто суммировать количество каждого слова. Это позволит быстро найти отдельное слово и его можно будет хранить между вызовами функций, но его сборка обходится дороже:

from collections import Counter

transtab = str.maketrans(digits + punctuation, " " * len(digits + punctuation))

def counterFromString(string):
    words = string.lower().translate(transtab).split()
    return Counter(words)

def normalOrder(recipes, queries):
    for recipe in recipes:
        recipe["score"] = recipe.get("rating", 0)

        title_counter = counterFromString(recipe["title"])
        category_counter = counterFromString(recipe["categories"])
        ingredient_counter = counterFromString(recipe["ingredients"])
        direction_counter = counterFromString(recipe["directions"])

        for query in queries:
            recipe["score"] += (
                8 * title_counter[query]
                + 4 * category_counter[query]
                + 2 * ingredient_counter[query]
                + 1 * direction_counter[query]
            )

    return recipes

Edit4: я заменил defaultdict на Counter -не знал, что класс существует.

0 голосов
/ 09 ноября 2019

Сначала вы можете использовать get вместо условия if.


def countTokens(token): 
     if token is None:
         return []
    token = str(token).lower() #make digits an punctuations white spaces
    tokens = token.translate(token.maketrans(digits + punctuation,\ " "*len(digits + punctuation)))
    return tokens.split(" ")

def normalOrder(recipes, queries): 
    for r in recipes: 
        parts, scores = [[],[],[],[]], 0 
        parts[0] = countTokens(r["title"]) 
        parts[1] = countTokens(r.get("categories", None )) 
        parts[2] = countTokens(r.get("ingredients", None)) 
        parts[3] = countTokens(r.get("directions", None)) 
     for q in queries: 
           scores += 8 * parts[0].count(q) + 4 * parts[1].count(q) + 2 * parts[2].count(q) + 1 * parts[3].count(q) 
      r["score"] = scores + r.get("rating", 0)
    return recipes
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...