【问题标题】:Python delete from list while iteratingPython在迭代时从列表中删除
【发布时间】:2021-03-04 21:33:13
【问题描述】:

我有一个字符串列表,我只想保留最独特的字符串。以下是我的实现方式(可能循环有问题),

def filter_descriptions(descriptions):
    MAX_SIMILAR_ALLOWED = 0.6  #40% unique and 60% similar
    i = 0
    while i < len(descriptions):
        print("Processing {}/{}...".format(i + 1, len(descriptions)))
        desc_to_evaluate = descriptions[i]
        j = i + 1
        while j < len(descriptions):
            similarity_ratio = SequenceMatcher(None, desc_to_evaluate, descriptions[j]).ratio()
            if similarity_ratio > MAX_SIMILAR_ALLOWED:
                del descriptions[j]
            j += 1
        i += 1
    return descriptions

请注意,该列表可能包含大约 110K 项,这就是我每次迭代都缩短列表的原因。

谁能指出当前的实现有什么问题?

编辑 1:

目前的结果“太相似了”。 filter_descriptions 函数返回 16 项(来自约 110K 项的列表)。当我尝试以下操作时,

SequenceMatcher(None, descriptions[0], descriptions[1]).ratio() 

比率为 0.99,SequenceMatcher(None, descriptions[1], descriptions[2]).ratio() 约为 0.98。但是SequenceMatcher(None, descriptions[0], descriptions[15]).ratio() 大约是 0.65(更好)

我希望这会有所帮助。

【问题讨论】:

  • 我无法理解您在第一次实施时遇到的问题?你能清楚地解释它有什么问题吗? “最终的字符串太相似”是什么意思?并帮助我们了解这是怎么回事
  • @JohnDoe 请参阅编辑 1

标签: python


【解决方案1】:

如果你颠倒你的逻辑,你可以避免不得不修改列表并仍然减少所需的比较次数。也就是说,从一个空的输出/唯一列表开始,然后遍历您的描述,看看您是否可以添加每一个。因此,对于第一个描述,您可以立即添加它,因为它与空列表中的任何内容都不相似。第二个描述只需要与第一个进行比较,而不是所有其他描述。以后的迭代一旦找到与它们相似的先前描述(并丢弃候选描述),就可以短路。即。

import operator

def unique(items, compare=operator.eq):
    # compare is a function that returns True if its two arguments are deemed similar to 
    # each other and False otherwise.

    unique_items = []
    for item in items:
        if not any(compare(item, uniq) for uniq in unique_items):
            # any will stop as soon as compare(item, uniq) returns True
            # you could also use `if all(not compare(item, uniq) ...` if you prefer
            unique_items.append(item)

    return unique_items

例子:

assert unique([2,3,4,5,1,2,3,3,2,1]) == [2, 3, 4, 5, 1]
# note that order is preserved

assert unique([1, 2, 0, 3, 4, 5], compare=(lambda x, y: abs(x - y) <= 1))) == [1, 3, 5]
# using a custom comparison function we can exclude items that are too similar to previous
# items. Here 2 and 0 are excluded because they are too close to 1 which was accepted
# as unique first. Change the order of 3 and 4, and then 5 would also be excluded.

使用您的代码,您的比较函数将如下所示:

MAX_SIMILAR_ALLOWED = 0.6  #40% unique and 60% similar

def description_cmp(candidate_desc, unique_desc):
    # use unique_desc as first arg as this keeps the argument order the same as with your filter 
    # function where the first description is the one that is retained if the two descriptions 
    # are deemed to be too similar
    similarity_ratio = SequenceMatcher(None, unique_desc, candidate_desc).ratio()
    return similarity_ratio > MAX_SIMILAR_ALLOWED

def filter_descriptions(descriptions):
    # This would be the new definition of your filter_descriptions function
    return unique(descriptions, compare=descriptions_cmp)

比较的次数应该完全相同。也就是说,在您的实现中,第一个元素与所有其他元素进行比较,第二个元素仅与被认为与第一个元素不相似的元素进行比较,依此类推。在这个实现中,第一个项目最初不与任何项目进行比较,但所有其他项目必须与它进行比较才能被允许添加到唯一列表中。只有被认为与第一个项目不相似的项目才会与第二个唯一项目进行比较,依此类推。

unique 实现将减少复制,因为它只需要在后备数组空间不足时复制唯一列表。而对于 del 语句,列表的部分必须在每次使用时复制(将所有后续项目移动到新的正确位置)。不过,这可能对性能的影响可以忽略不计,因为瓶颈可能是序列匹配器中的比率计算。

【讨论】:

  • 感谢您采用不同的方法来处理此@Dunes。我将对这两种方法进行基准测试并在这里分享结果。
  • SequenceMatcher 耗时约 3 分 10 秒,反转逻辑耗时约 100 微秒
  • 微秒?这听起来太快了...... CPU 只能以这种速度执行输入列表中的每个项目大约 1-10 条指令。让我觉得我的逻辑或你进行的测试有问题。
  • 我自己的 microbench 标记显示 unique 版本在所有内容相似的列表中快 10 倍,在所有内容不相似的列表中快 2 倍。与您报告的 7 个数量级完全不同。您的版本具有破坏性(修改输入列表)。在运行您的版本后,您是否在同一个列表中运行了我的版本?
  • 在兴奋中我可能弄错了单位:) 这是datetime.now() 的输出。开始 SequenceMatcher:2018-10-23 16:43:37.221874 结束 SequenceMatcher:2018-10-23 16:46:47.316437 开始反转:2018-10-23 16:46:47.316519 结束反转:2018-10-23 16:46 :47.333900
【解决方案2】:

当列表中的项目被删除时j 的值不应更改(因为在下一次迭代中该位置将出现不同的列表项目)。每次删除一个项目时,执行j=i+1 都会重新开始迭代(这不是我们想要的)。更新后的代码现在仅在 else 条件下增加 j

def filter_descriptions(descriptions):
    MAX_SIMILAR_ALLOWED = 0.6  #40% unique and 60% similar
    i = 0
    while i < len(descriptions):
        print("Processing {}/{}...".format(i + 1, len(descriptions)))
        desc_to_evaluate = descriptions[i]
        j = i + 1
        while j < len(descriptions):
            similarity_ratio = SequenceMatcher(None, desc_to_evaluate, descriptions[j]).ratio()
            if similarity_ratio > MAX_SIMILAR_ALLOWED:
                del descriptions[j]
            else:
                j += 1
        i += 1
    return descriptions

【讨论】:

    【解决方案3】:

    您的逻辑的问题是,每次从数组中删除一个项目时,索引都会重新排列并在其间跳过一个字符串。例如:

    假设这是数组: 描述:["A","A","A","B","C"]

    迭代1:

    i=0                      -------------0
    description[i]="A"
    j=i+1                    -------------1
    description[j]="A"
    similarity_ratio>0.6
    del description[j]
    

    现在数组被重新索引,如下所示: 描述:["A","A","B","C"]。下一步是:

     j=j+1                   ------------1+1= 2
    

    描述[2]="B"

    您已跳过说明[1]="A"


    要解决这个问题: 替换

    j+=1 
    

    j=i+1
    

    如果被删除。否则进行正常的 j=j+1 迭代

    【讨论】:

    • 是的,这似乎是问题所在。但我认为j=i+1 只需要在从列表中删除项目时完成。如果没有删除任何项目,那么 j 应该只增加(加一),不这样做会将其变成无限循环。
    • 是的,您必须保留“j=j+1”以循环遍历数组元素。除此之外,您必须添加“j=j+i”以防删除项目。
    • @MJB 如果有用,请接受/支持答案
    猜你喜欢
    • 2011-09-23
    • 2017-09-17
    • 2011-03-18
    • 2018-09-26
    • 2020-05-27
    • 2011-11-26
    • 2014-03-15
    相关资源
    最近更新 更多