【问题标题】:Loop "Forgets" to Remove Some Items [duplicate]循环“忘记”以删除一些项目[重复]
【发布时间】:2013-06-22 09:11:11
【问题描述】:

在这段代码中,我试图创建一个函数 anti_vowel,它将从字符串中删除所有元音 (aeiouAEIOU)。我认为它应该可以正常工作,但是当我运行它时,示例文本“Hey look Words!”以“Hy lk Words!”的形式返回。它“忘记”删除最后一个'o'。这怎么可能?

text = "Hey look Words!"

def anti_vowel(text):

    textlist = list(text)

    for char in textlist:
        if char.lower() in 'aeiou':
            textlist.remove(char)

    return "".join(textlist)

print anti_vowel(text)

【问题讨论】:

  • 测试然后删除有 N^2 复杂性:只需删除字符,无论它是否存在......(或使用其他建议的解决方案)
  • @Don: O(n^2) 其中n是什么,输入文本的长度?
  • remove_vowels 会是比anti_vowel 更好的名字
  • 是的,但 'if' 并不那么有影响力(它的复杂度为“5”):N^2 是由于 'for' 和 '.remove'
  • 只是为了简化逻辑:for char in 'aeiouAEIOU': textlist.remove(char)

标签: python string list


【解决方案1】:

引用from the docs:

注意:当序列被 循环(这只能发生在可变序列,即列表)。一个 内部计数器用于跟踪接下来使用哪个项目,并且 这在每次迭代时递增。当这个计数器到达 循环终止的序列的长度。这意味着,如果 套件从序列中删除当前(或前一个)项目, 将跳过下一个项目(因为它获取当前项目的索引 已经处理过)。同样,如果套件插入 在当前项目之前的序列中的项目,当前项目将是 下次通过循环再次处理。这可能会导致令人讨厌的 可以通过使用切片创建临时副本来避免的错误 整个序列,例如,

for x in a[:]:
    if x < 0: a.remove(x)

使用[:] 遍历列表的浅表副本。您在迭代列表时正在修改列表,这将导致丢失一些字母。

for 循环跟踪索引,因此当您删除索引 i 处的项目时,i+1th 位置的下一个项目将移动到当前索引 (i),因此在下一次迭代中你实际上会选择i+2th 项。

举个简单的例子:

>>> text = "whoops"
>>> textlist = list(text)
>>> textlist
['w', 'h', 'o', 'o', 'p', 's']
for char in textlist:
    if char.lower() in 'aeiou':
        textlist.remove(char)

迭代 1:索引 = 0。

char = 'W',因为它在索引 0 处。因为它不满足那个条件,你会注意到。

迭代 2:索引 = 1。

char = 'h',因为它位于索引 1。这里没什么可做的。

迭代 3:索引 = 2。

char = 'o' 在索引 2 处。由于该项目满足条件,因此将从列表中删除,并且其右侧的所有项目将向左移动一位以填补空白。

现在textlist 变为:

   0    1    2    3    4
`['w', 'h', 'o', 'p', 's']`

正如您所见,另一个 'o' 已移至索引 2,即当前索引,因此它将在下一次迭代中被跳过。因此,这就是在您的迭代中跳过某些项目的原因。每当您删除一个项目时,都会从迭代中跳过下一个项目。

迭代 4:索引 = 3。

char = 'p' 在索引 3 处。

....


修复:

遍历列表的浅拷贝以解决此问题:

for char in textlist[:]:        #note the [:]
    if char.lower() in 'aeiou':
        textlist.remove(char)

其他选择:

列表理解:

使用str.joinlist comprehension 的单行代码:

vowels = 'aeiou'
text = "Hey look Words!"
return "".join([char for char in text if char.lower() not in vowels])

正则表达式:

>>> import re
>>> text = "Hey look Words!"
>>> re.sub('[aeiou]', '', text, flags=re.I)
'Hy lk Wrds!'

【讨论】:

  • re.sub('[aeiou]', '', flags=re.I) 更容易(特别是如果字符列表变长)
【解决方案2】:

您正在修改您正在迭代的数据。不要那样做。

''.join(x for x in textlist in x not in VOWELS)

【讨论】:

    【解决方案3】:

    其他答案告诉您为什么 for 在您更改列表时会跳过项目。这个答案告诉你应该如何在没有显式循环的情况下删除字符串中的字符。

    使用str.translate():

    vowels = 'aeiou'
    vowels += vowels.upper()
    text.translate(None, vowels)
    

    这会删除第二个参数中列出的所有字符。

    演示:

    >>> text = "Hey look Words!"
    >>> vowels = 'aeiou'
    >>> vowels += vowels.upper()
    >>> text.translate(None, vowels)
    'Hy lk Wrds!'
    >>> text = 'The Quick Brown Fox Jumps Over The Lazy Fox'
    >>> text.translate(None, vowels)
    'Th Qck Brwn Fx Jmps vr Th Lzy Fx'
    

    在 Python 3 中,str.translate() 方法(Python 2:unicode.translate())的不同之处在于它不需要 deletechars 参数;第一个参数是将 Unicode 序数(整数值)映射到新值的字典。对需要删除的任何字符使用None

    # Python 3 code
    vowels = 'aeiou'
    vowels += vowels.upper()
    vowels_table = dict.fromkeys(map(ord, vowels))
    text.translate(vowels_table)
    

    您还可以使用str.maketrans() static method 生成该映射:

    vowels = 'aeiou'
    vowels += vowels.upper()
    text.translate(text.maketrans('', '', vowels))
    

    【讨论】:

    • 对 python3 的注释可能有用:text.translate(dict.fromkeys(map(ord, vowels)))
    • @Bakuriu:确实;这同样适用于 Python 2 上的 unicode.translate(),在任何情况下都是相同的类型。
    【解决方案4】:

    您正在修改您正在迭代的列表,这势必会导致一些不直观的行为。相反,制作列表的副本,这样您就不会从正在迭代的内容中删除元素。

    for char in textlist[:]: #shallow copy of the list
        # etc
    

    要澄清您看到的行为,请查看此内容。将print char, textlist 放在(原始)循环的开头。您可能希望,这会在列表旁边垂直打印出您的字符串,但您实际得到的是:

    H ['H', 'e', 'y', ' ', 'l', 'o', 'o', 'k', ' ', 'W', 'o', 'r', 'd', 's', '!']
    e ['H', 'e', 'y', ' ', 'l', 'o', 'o', 'k', ' ', 'W', 'o', 'r', 'd', 's', '!']
      ['H', 'y', ' ', 'l', 'o', 'o', 'k', ' ', 'W', 'o', 'r', 'd', 's', '!'] # !
    l ['H', 'y', ' ', 'l', 'o', 'o', 'k', ' ', 'W', 'o', 'r', 'd', 's', '!']
    o ['H', 'y', ' ', 'l', 'o', 'o', 'k', ' ', 'W', 'o', 'r', 'd', 's', '!']
    k ['H', 'y', ' ', 'l', 'o', 'k', ' ', 'W', 'o', 'r', 'd', 's', '!'] # Problem!!
      ['H', 'y', ' ', 'l', 'o', 'k', ' ', 'W', 'o', 'r', 'd', 's', '!']
    W ['H', 'y', ' ', 'l', 'o', 'k', ' ', 'W', 'o', 'r', 'd', 's', '!']
    o ['H', 'y', ' ', 'l', 'o', 'k', ' ', 'W', 'o', 'r', 'd', 's', '!'] 
    d ['H', 'y', ' ', 'l', 'k', ' ', 'W', 'o', 'r', 'd', 's', '!']
    s ['H', 'y', ' ', 'l', 'k', ' ', 'W', 'o', 'r', 'd', 's', '!']
    ! ['H', 'y', ' ', 'l', 'k', ' ', 'W', 'o', 'r', 'd', 's', '!']
    Hy lk Words!
    

    那么发生了什么? Python 中漂亮的for x in y 循环实际上只是语法糖:它仍然通过索引访问列表元素。因此,当您在迭代列表时从列表中删除元素时,您会开始跳过值(如上所示)。结果,您永远不会在"look" 中看到第二个o;你跳过它是因为当你删除前一个元素时索引已经“过去”了它。然后,当您到达"Words" 中的o 时,您将删除第一次出现的'o',这是您之前跳过的那个。


    正如其他人所提到的,列表推导式可能是一种更好(更清晰、更清晰)的方式来做到这一点。利用 Python 字符串可迭代的事实:

    def remove_vowels(text): # function names should start with verbs! :)
        return ''.join(ch for ch in text if ch.lower() not in 'aeiou')
    

    【讨论】:

    • str 是可迭代的,filter 可以说比列表理解更干净。
    • @TC1 filter 有一个案例,当然str.translate 也是如此。我个人认为列表推导比这两者中的任何一个都更具可读性;因此我的选择:)
    【解决方案5】:

    List Comprehensions:

    vowels = 'aeiou'
    text = 'Hey look Words!'
    result = [char for char in text if char not in vowels]
    print ''.join(result)
    

    【讨论】:

      【解决方案6】:
      text = "Hey look Words!"
      
      print filter(lambda x: x not in "AaEeIiOoUu", text)
      

      输出

      Hy lk Wrds!
      

      【讨论】:

        【解决方案7】:

        其他人已经解释了您的代码存在的问题。对于您的任务,生成器表达式更容易且不易出错。

        >>> text = "Hey look Words!"
        >>> ''.join(c for c in text if c.lower() not in 'aeiou')
        'Hy lk Wrds!'
        

        >>> ''.join(c for c in text if c not in 'AaEeIiOoUu')
        'Hy lk Wrds!'
        

        不过,str.translate 是最好的选择。

        【讨论】:

          【解决方案8】:

          您正在迭代一个列表并同时从中删除元素。

          首先,我需要确保您清楚地了解charfor char in textlist: ... 中的作用。以我们到达字母“l”的情况为例。情况是这样的:

          ['H', 'e', 'y', ' ', 'l', 'o', 'o', 'k', ' ', 'W', 'o', 'r', 'd', 's', '!']
                                ^
                              char
          

          char 与列表中字母“l”的位置之间没有链接。如果修改char,列表将不会被修改。情况更像是这样的:

          ['H', 'e', 'y', ' ', 'l', 'o', 'o', 'k', ' ', 'W', 'o', 'r', 'd', 's', '!']
                                ^
          char = 'l'
          

          请注意,我保留了 ^ 符号。这是管理for char in textlist: ... 循环的代码用来跟踪其在循环中的位置的隐藏指针。每次进入循环体,指针前进,指针引用的字母被复制到char中。

          当您连续有两个元音时,就会出现问题。我会告诉你从你到达“l”的那一点会发生什么。请注意,我还将“look”一词更改为“leap”,以便更清楚地了解发生了什么:

          将指针前进到下一个字符 ('l') 并复制到 char

          ['H', 'e', 'y', ' ', 'l', 'e', 'a', 'p', ' ', 'W', 'o', 'r', 'd', 's', '!']
                             -> ^
          char = 'l'
          

          char ('l') 不是元音,所以什么都不做

          将指针前进到下一个字符 ('e') 并复制到 char

          ['H', 'e', 'y', ' ', 'l', 'e', 'a', 'p', ' ', 'W', 'o', 'r', 'd', 's', '!']
                                  -> ^
          char = 'e'
          

          char ('e') 是元音,所以删除char ('e') 的第一个匹配项

          ['H', 'e', 'y', ' ', 'l', 'e', 'a', 'p', ' ', 'W', 'o', 'r', 'd', 's', '!']
                                     ^
          
          ['H', 'e', 'y', ' ', 'l',      'a', 'p', ' ', 'W', 'o', 'r', 'd', 's', '!']
                                     ^
          
          ['H', 'e', 'y', ' ', 'l',   <- 'a', 'p', ' ', 'W', 'o', 'r', 'd', 's', '!']
                                     ^
          
          ['H', 'e', 'y', ' ', 'l', 'a', 'p', ' ', 'W', 'o', 'r', 'd', 's', '!']
                                     ^
          

          将指针前进到下一个字符 ('p') 并复制到 char

          ['H', 'e', 'y', ' ', 'l', 'a', 'p', ' ', 'W', 'o', 'r', 'd', 's', '!']
                                       -> ^
          char = 'p'
          

          当您删除“e”时,“e”之后的所有字符都向左移动了一位,所以就好像remove 已经推进了指针。结果是您跳过了“a”。

          一般来说,您应该避免在迭代列表时修改列表。最好从头开始构建一个新列表,Python 的列表推导式是执行此操作的完美工具。例如

          print ''.join([char for char in "Hey look Words" if char.lower() not in "aeiou"])
          

          但如果你还没有学过推导式,最好的方法可能是:

          text = "Hey look Words!"
          
          def anti_vowel(text):
          
            textlist = list(text)
            new_textlist = []
          
            for char in textlist:
              if char.lower() not in 'aeiou':
                new_textlist.append(char)
          
              return "".join(new_textlist)
          
          print anti_vowel(text)
          

          【讨论】:

            【解决方案9】:

            您不应该从您遍历的列表中删除项目: 但是您可以使用列表理解语法从旧列表中创建新列表。列表理解在这种情况下非常有用。你可以阅读列表理解here

            所以您的解决方案将如下所示:

            text = "Hey look Words!"
            
            def anti_vowel(text):
                return "".join([char for char in list(text) if char.lower() not in 'aeiou'])
            
            print anti_vowel(text)
            

            很漂亮,不是吗:P

            【讨论】:

            • 这并没有提供问题的答案。要批评或要求作者澄清,请在其帖子下方发表评论。
            • @RandomSeed 一开始我也是这么想的,但它确实回答了这个问题。
            • @EduardLuca 它可能会做 OP 想做的事情(我不知道),但它没有回答这个问题:“这怎么可能?”。事实上,这里很少有答案能真正回答这个问题。
            • 我认为提供替代解决方案没有任何问题。尤其是更干净更短的。所以即使不直接回答问题,也解决了根本问题。
            【解决方案10】:

            尽量不要对字符串使用 list() 函数。它会使事情变得复杂得多。

            与 Java 不同,在 Python 中,字符串被视为数组。然后,尝试对循环和 del 关键字使用索引。

            for x in range(len(string)):
                if string[x].lower() in "aeiou":
                    del string[x]
            

            【讨论】:

              猜你喜欢
              • 2012-02-15
              • 2023-03-05
              • 2011-11-05
              • 2011-11-12
              • 2019-03-14
              • 1970-01-01
              相关资源
              最近更新 更多