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

Mar*_*pez 2 python performance time search python-3.x

我正在用 python 编写这个搜索引擎来获取菜谱列表,它应该以每次搜索的特定速度运行,最多 0.1 秒。我一直在努力用我的代码达到这个速度。我的平均分是0.4。我想知道您是否知道如何使此代码更快。我已经尝试了很多事情,但我知道循环是让它变慢的原因。如果我能主要用 python 来改进它而不是添加那么多模块。

我已经在代码的其他部分使其速度更快了 0.005 avg。但在这部分有大量的食谱,它变得相当慢。

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
Run Code Online (Sandbox Code Playgroud)

在一点上下文中,我需要对上面四个描述符中查询的出现次数进行求和,前提是有它,这就是我有 if 的原因。

He3*_*xxx 5

我注意到了几点:

  • 每次调用 时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
Run Code Online (Sandbox Code Playgroud)

这对您有用吗?它足够快吗?

recipe["title"]编辑:在您的原始代码中,您将对和其他字符串的访问包装在另一个str()调用中。我猜它们已经是字符串了?如果不是,您需要在此处添加。


Edit2:您在评论中指出标点符号是一个问题。正如我在评论中所说,我认为您不必担心这一点,因为count如果查询词和食谱文本都包含标点符号,则调用只会关心标点符号字符,那么计数调用将仅计算出现的次数周围的标点符号与查询的内容相匹配。看看这些例子:

>>> "Some text, that...".count("text")
1
>>> "Some text, that...".count("text.")
0
>>> "Some text, that...".count("text,")
1
Run Code Online (Sandbox Code Playgroud)

如果您希望其行为有所不同,您可以执行与原始问题中相同的操作:创建一个转换表并应用它。请记住,将此翻译应用于食谱文本(正如您在问题中所做的那样)没有多大意义,从那时起,任何包含标点符号的查询词都不会匹配。通过忽略所有包含标点符号的查询词,可以更容易地完成此操作。您可能想要对查询词进行翻译,这样如果有人输入“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
Run Code Online (Sandbox Code Playgroud)

Edit3:在评论中,您表示您希望搜索 [“honey”, “lemon”] 来匹配“honey-lemon”,但您不希望“butter”匹配“butterfingers”。为此,您最初的方法可能是最好的解决方案,但请记住,搜索单数形式“potato”将不再与复数形式(“potatoes”)或任何其他派生形式匹配。

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
Run Code Online (Sandbox Code Playgroud)

如果您使用相同的食谱更频繁地调用此函数,则可以通过将结果存储.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
Run Code Online (Sandbox Code Playgroud)

Edit4:我用 Counter 替换了 defaultdict —— 不知道该类的存在。