最优雅的方法来原地修改嵌套列表中的元素

28

我有一个二维列表,长这样:

table = [['donkey', '2', '1', '0'], ['goat', '5', '3', '2']]

我想将最后三个元素转换为整数,但下面的代码感觉非常丑陋:

for row in table:
    for i in range(len(row)-1):
        row[i+1] = int(row[i+1])

但我更希望得到像这样的东西:

for row in table:
    for col in row[1:]:
        col = int(col)

我认为应该有一种方法可以编写上述代码,但是切片会创建一个与原始列表分开的迭代器/新列表,因此引用无法传递。

是否有更Pythonic的解决方案?

7个回答

20
for row in table:
    row[1:] = [int(c) for c in row[1:]]

以上的代码更符合Pythonic风格吗?


13
虽然这在技术上是一个原地操作,但循环内部创建了两个额外的列表。第一个列表由row[1:]切片(作为map函数的参数)创建。第二个由列表推导式创建。 - Wesley
这被称为“列表推导式”[https://docs.python.org/2/tutorial/datastructures.html#list-comprehensions]。 - Kalle Richter

13

尝试:

>>> for row in table:
...     row[1:]=map(int,row[1:])
... 
>>> table
[['donkey', 2, 1, 0], ['goat', 5, 3, 2]]
据我所知,对于一个列表的切片赋值操作强制在原地进行,而不是创建一个新的列表。

1
使用map而不是推导式仍然被认为是Pythonic的吗? - Nicholas Mancuso
2
@Nicholas Mancuso,“map”完全符合Python的风格。不太Pythonic的是,当你试图将太多内容塞入一个与之一起使用的“lambda”中时,你经常会得到混乱的结果。 - Michael J. Barber
@ajmartin:只需执行table[::]=,然后用任何表达式即可。这样可以原地完成操作。 - MAK
1
@MAK:为什么你在最后一条评论和回答的第二行多了一个冒号?这是不必要的,而且会干扰阅读。 - gurney alex
2
虽然这在技术上是一个原地操作,但循环内部创建了两个额外的列表。第一个列表是由row[1:]切片(传递给map函数)创建的。第二个是由map函数创建的。对于长度为nrow而言,空间使用量为3n-1。随着n的增加,使用简单的内部循环会变得更加节省空间。 - Wesley
显示剩余4条评论

8

我非常喜欢Shekhar的回答。

一般来说,在编写Python代码时,如果你发现自己正在写for i in range(len(somelist)),那么你做错了:

  • 如果只有一个列表,尝试使用enumerate
  • 如果有两个或更多列表要并行迭代,请尝试使用zipitertools.izip

在您的情况下,第一列不同,因此您无法优雅地使用enumerate

for row in table:
    for i, val in enumerate(row):
        if i == 0: continue
        row[i] = int(val)

7
如果你发现自己写了 for i in range(len(somelist)) 这样的代码,那么你可能做错了些什么。这可能是给学习Python语言习惯最好的建议了。Python在可读性方面有着很强的优势,当从类似Java这样的语言转换过来时,如果使用这种构造方式,将会失去在Python中工作的真正优势。+1 - Conrad.Dean
你可以使用 for i, val in enumerate(row[1:]): 来改进它,从而摆脱 if i == 0 - erickrf
@erickrf 这将创建一行的浅拷贝,之后您需要使用row[i+1] = int(val)。不确定这是否会有很大改进。 - gurney alex
“如果你发现自己写了 for i in range(len(somelist)),那么你可能做错了什么”这种说法的正确性是有问题的。使用枚举不仅更慢,而且也可能被认为不够易读。请参考这个答案 - virtualxtc

3

通过使用两个参数调用range,可以改进您的“丑陋”代码:

for row in table:
    for i in range(1, len(row)):
        row[i] = int(row[i])

这可能是您坚持不分配新临时列表的情况下,直接更改项目的最佳方法(可以使用列表理解式、map和/或切片)。请参阅Python中是否有类似于'map'的原地等效函数? 虽然我不建议这样做,但您也可以通过引入自己的原地映射函数使此代码更加通用:
def inplacemap(f, items, start=0, end=None):
    """Applies ``f`` to each item in the iterable ``items`` between the range
    ``start`` and ``end``."""
    # If end was not specified, make it the length of the iterable
    # We avoid setting end in the parameter list to force it to be evaluated on
    # each invocation
    if end is None:
        end = len(items)
    for i in range(start, end):
        items[i] = f(items[i])

for row in table:
    inplacemap(int, row, 1)

个人而言,我认为这种做法不够Pythonic。最好只有一种明显的方法来做到它,而这并不是其中之一。


1

使用列表推导式:

table = [row[0] + [int(col) for col in row[1:]] for row in table]

+1 我不知道你可以像这样链接列表推导式!我可能不会使用这么嵌套的东西,因为我与许多人一起工作,他们可能会觉得这很难读懂,但我一定会记住这个方法用于个人项目。谢谢! - Conrad.Dean

0

这个会起作用:

table = [[row[0]] + [int(v) for v in row[1:]] for row in table]

然而,您可能希望考虑在创建列表的时候进行转换。


你说得对。在我的其他算法中,将我的表格与一个特殊的子表格一起处理非常麻烦,所以现在我有了一个原始数据表。 - Conrad.Dean

-1

这可以实现你所寻求的功能。它是一个易读的解决方案。你也可以使用列表推导式来得到类似的结果。

>>> for row in table:
...     for i, elem in enumerate(row):
...             try:
...                     int(elem)
...             except ValueError:
...                     pass
...             else:
...                     row[i] = int(elem)
... 

有了适当的验证,这是唯一正确的答案。尽管两次执行int转换是浪费的,并且违背了try-except块背后的思想。 - Muposat

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接