【问题标题】:Is there any way I can make this code faster?有什么办法可以让这段代码更快吗?
【发布时间】:2020-03-05 20:19:12
【问题描述】:

我正在用 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

在一点点上下文中,我需要总结上面四个描述符中查询的出现次数,只有当有它时,这就是我有 if 的原因。

【问题讨论】:

  • 你能展示一些示例数据吗?
  • 您检查一个键是否在字典中,如果存在则检索该值。这需要 2 次查找。您可以使用dict.get 并提供默认值。这只会导致一次查找。

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


【解决方案1】:

我注意到几点:

  • 每次调用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:您在 cmets 中指出标点符号是一个问题。正如我在 cmets 中所说,我认为您不必担心,因为 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:在 cmets 中,您声明要搜索 ["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

如果您更频繁地使用相同的接收方调用此函数,则可以通过将.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——我不知道该类存在。

【讨论】:

  • 我需要先确保单词没有数字或标点符号。我怎样才能解决这个问题,同时保持这个解决方案
  • 如果查询不包含任何标点符号,那么配方文本中的标点符号不是问题——它根本不会被计算在内:"Cut the potatoes, broccoli and peppers".count("potato") 将返回 1。所以这只是一个问题,如果查询包含标点符号,配方文本也包含标点符号。您能否提供一个示例并解释在这种情况下应该做什么?一个解决方案可能是使用您在问题中使用的方法删除所有标点符号,只需确保您只生成一次翻译表。
  • 我在答案中添加了对我的想法的更深入的解释,请查看。
  • 当我的查询是柠檬和蜂蜜时,就柠檬蜂蜜而言。那不会忽略单词吗?
  • 不,"lemon-honey".count("lemon") 是 1,"lemon-honey".count("honey") 也是 1。这两个查询都会被找到并匹配。 python 中的str.count() 不是基于单词,而是基于字符序列。它不需要您正在搜索的单词被空格包围。如果您执行"psychotherapist".count("other"),它将返回 1,因为字符串按顺序包含这些字符。
【解决方案2】:

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

【讨论】:

  • get 帮了我很多,它让它更快了一点,但绝对更整洁。谢谢!
猜你喜欢
  • 1970-01-01
  • 2012-01-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-05-13
  • 2023-04-10
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多