77

在这段代码中,我试图创建一个函数 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)
4

10 回答 10

154

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

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 中的 nicefor x in y循环实际上只是语法糖:它仍然通过索引访问列表元素。因此,当您在迭代列表时从列表中删除元素时,您会开始跳过值(如上所示)。结果,您永远不会在 ; 中看到第二o"look"。你跳过它是因为当你删除前一个元素时索引已经“过去”了它。然后,当您到达oin 时"Words",您将删除第一次出现的'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')
于 2013-06-25T14:13:19.370 回答
66

其他答案告诉您为什么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()静态方法来生成该映射:

vowels = 'aeiou'
vowels += vowels.upper()
text.translate(text.maketrans('', '', vowels))
于 2013-06-25T14:11:51.080 回答
31

引用文档

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

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

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

for循环跟踪索引,因此当您删除 index 处的i项目时,第 th 位置的下一个项目i+1将移动到当前 index( 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.join和 的单线list 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!'
于 2013-06-25T14:10:11.920 回答
16

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

''.join(x for x in textlist in x not in VOWELS)
于 2013-06-25T14:10:40.910 回答
8
text = "Hey look Words!"

print filter(lambda x: x not in "AaEeIiOoUu", text)

输出

Hy lk Wrds!
于 2013-06-25T14:30:44.017 回答
7

您正在遍历列表并同时从中删除元素。

首先,我需要确保您清楚地了解charin的作用for 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)
于 2013-06-26T00:53:09.807 回答
4

列表理解

vowels = 'aeiou'
text = 'Hey look Words!'
result = [char for char in text if char not in vowels]
print ''.join(result)
于 2013-06-25T14:14:44.130 回答
3

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

>>> 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是最好的方法。

于 2013-06-25T17:25:51.407 回答
0

您不应该从您迭代的列表中删除项目:但是您可以使用列表理解语法从旧列表中创建新列表。列表理解在这种情况下非常有用。您可以在此处阅读有关列表理解的信息

因此,您的解决方案将如下所示:

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

于 2014-12-04T11:50:12.170 回答
0

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

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

for x in range(len(string)):
    if string[x].lower() in "aeiou":
        del string[x]
于 2015-05-25T17:24:09.067 回答