有什么办法可以使这段代码更快?

时间:2019-11-09 12:45:51

标签: python python-3.x performance search time

我正在用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 个答案:

答案 0 :(得分:4)

我注意到了几点:

  • 每次调用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

如果您希望这种行为有所不同,则可以执行与原始问题相同的操作:创建转换表并应用它。 请记住,将此翻译应用于配方文本(就像您在问题中所做的那样)没有多大意义,因为从那时起,任何包含标点符号的查询词都永远不会匹配。只需忽略所有包含标点符号的查询词,就可以轻松得多。 您可能希望对查询词进行翻译,以便在有人输入“马铃薯”的情况下,发现所有出现的“马铃薯”。看起来像这样:

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”,但不希望“ butter”匹配“ butterfingers”。为此,您最初的方法可能是最好的解决方案,但请记住,搜索单数形式的“马铃薯”将不再匹配复数形式(“马铃薯”)或任何其他派生形式。

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:我用一个Counter替换了defaultdict-不知道该类存在。

答案 1 :(得分:1)

首先,您可以使用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