Python列表和for-each访问(在内置列表中查找/替换)

30

我最初认为Python是一种纯传递引用的语言。

来自C/C++的我无法不考虑内存管理,很难把它从我的脑海中抛开。因此,我试图以Java的视角思考,并将除基本类型外的所有内容视为传递引用。

问题:我有一个列表,其中包含许多用户定义类的实例。

如果我使用for-each语法,例如:

for member in my_list:
    print(member.str);

member是指向对象实际引用的等价物吗?

这相当于执行以下操作吗:

i = 0
while i < len(my_list):
    print(my_list[i])
    i += 1

我认为不是这样的,因为当我试图进行替换时,它不起作用,也就是说,下面这个不起作用:

for member in my_list:
    if member == some_other_obj:
        member = some_other_obj

如何在列表中进行简单的查找和替换?是否可以使用 for-each 循环完成,如果可以,具体如何操作?否则,我是否只能使用随机访问语法(方括号),或者两种方法都不可行,需要删除条目并插入新条目?

i = 0
for member in my_list:
   if member == some_other_obj:
      my_list.remove(i)
      my_list.insert(i, member)
   i += 1

在列表迭代过程中,值不会被复制。原因类似于将对象作为参数传递给函数时不会被复制的原因。 - jfs
3个回答

49
回答这个问题很好,因为评论让我更好地理解了Python变量。正如评论中所指出的那样,当你使用类似于for member in my_list的循环遍历列表时,member变量会绑定到每个连续的列表元素。然而,在循环内重新分配该变量并不直接影响列表本身。例如,此代码不会改变列表:
my_list = [1,2,3]
for member in my_list:
    member = 42
print my_list

输出:

[1, 2, 3]

如果你想改变包含不可变类型的列表,你需要做类似这样的事情:

my_list = [1,2,3]
for ndx, member in enumerate(my_list):
    my_list[ndx] += 42
print my_list

输出:

[43, 44, 45]

如果你的列表包含可变对象,你可以直接修改当前的member对象:

class C:
    def __init__(self, n):
        self.num = n
    def __repr__(self):
        return str(self.num)

my_list = [C(i) for i in xrange(3)]
for member in my_list:
    member.num += 42
print my_list

[42, 43, 44]

请注意,您仍未更改列表,只是修改了列表中的对象。

您可能会受益于阅读名称和绑定


很不幸,它被重新定义为列表成员的副本。我认为将其显示为引用会更有用,因为如果你设置它,可能会以某种方式操纵结构。我测试了语法: for idx in range(0, len(my_list)): my_list[idx] = new_obj 它符合我的要求。谢谢。 - Syndacate
@Syndacate: 不确定C++如何处理这样的事情,但Java的foreach循环(或者用他们的术语说是enhanced for loop)在效果上类似。 - GreenMatt
@J.F. Sebastian:我的术语可能不是完美的,但我相信我提供了一个很好的思考方式。答案已经被修改和扩展。 - GreenMatt
@GreenMatt:除非你编辑回答(这是 SO 的一个功能),否则我无法撤销我的投票。 - jfs
顺便提一下,与Java不同,Python中没有原始数据类型。整数是不可变的对象。 - jfs
显示剩余2条评论

16

Python与Java、C/C++不同,你需要停止这种思维方式才能真正利用Python的威力。

Python不是按值传递也不是按引用传递,而是使用按名称(或按对象)传递——换句话说,几乎所有东西都绑定到一个可以使用的名称上(两个明显的例外是元组和列表索引)。

当你执行spam = "green"时,你将名称spam绑定到字符串对象"green";如果你接着执行eggs = spam,你没有复制任何东西,也没有做参考指针;你只是将另一个名称eggs绑定到相同的对象(在这种情况下是"green")。如果然后将spam绑定到其他内容(例如spam = 3.14159),eggs仍将绑定到"green"

当for循环执行时,它依次将你给定的名称绑定到可迭代对象中的每个对象;当你调用一个函数时,它将函数头中的名称绑定到传递的参数;重新分配名称实际上是重新绑定名称(这可能需要一段时间来吸收——至少对我来说是这样)。

对于使用列表的for循环,有两种基本的将值赋回给列表的方法:

for i, item in enumerate(some_list):
    some_list[i] = process(item)
或者
new_list = []
for item in some_list:
    new_list.append(process(item))
some_list[:] = new_list

注意最后的some_list中的[:] -- 这会导致对some_list的元素进行突变(将整个列表设为new_list的元素),而不是将名称some_list重新绑定到new_list。这重要吗?这要看情况而定!如果除了some_list之外,还有其他名称绑定到同一个列表对象,并且您希望它们看到更改,则需要使用切片方法;如果您不需要或者不希望它们看到更改,则重新绑定--some_list = new_list


名称绑定的方式与Java相同。 我看不出任何区别。 - r.v
1
在Java中,像int和boolean这样的原始类型是按值传递的,而所有非原始类型都是按引用传递的。绑定到名称类似于按引用传递,减去类型安全和可能的其他一些细节。在Java中,引用至少在编译时保留有关其可以引用的类型数据,而Python名称没有此限制。 - Sqeaky

6
你可以通过获取索引和项目来替换其中的某些内容。
>>> foo = ['a', 'b', 'c', 'A', 'B', 'C']
>>> for index, item in enumerate(foo):
...     print(index, item)
...
(0, 'a')
(1, 'b')
(2, 'c')
(3, 'A')
(4, 'B')
(5, 'C')
>>> for index, item in enumerate(foo):
...     if item in ('a', 'A'):
...         foo[index] = 'replaced!'
...
>>> foo
['replaced!', 'b', 'c', 'replaced!', 'B', 'C']

请注意,如果您想从列表中删除某些内容,则必须迭代列表的副本,否则您将会出现错误,因为您正在尝试更改正在迭代的内容的大小。这可以使用切片轻松完成。

错误示例:

>>> foo = ['a', 'b', 'c', 1, 2, 3]
>>> for item in foo:
...     if isinstance(item, int):
...         foo.remove(item)
...
>>> foo 
['a', 'b', 'c', 2]

2仍然存在,因为我们在迭代过程中修改了列表的大小。正确的方法应该是:

>>> foo = ['a', 'b', 'c', 1, 2, 3]
>>> for item in foo[:]:
...     if isinstance(item, int):
...         foo.remove(item)
...
>>> foo 
['a', 'b', 'c']

@neurino:Pythonic 的方法是:foo = [c for c in foo if condition(c)] - jfs
@J.F. Sebastian:老实说,我不认为所有能写在一行的代码都比三行代码更符合 Python 风格,我想强调的是 Gilder 在使用 enumerate 而 GreenMat 困住了(他编辑了自己的答案)。干杯! - neurino
1
@neurino:我是一名“老派”的程序员,正在努力更新自己的技能;因此,我倾向于首先考虑for x in xrange(len(l))。因此,我认为对于来自C/C++(正如OP所说)的人来说,for x in xrange(len(l))for x in enumerate(l)更为熟悉。是更Pythonic还是专注于核心问题并在必要的情况下使用更熟悉的语法来解决问题?(我认为这没有一个正确答案。) - GreenMatt
@neurino: 我使用列表推导式替换了第3个示例中对foo[:]进行O(N**2)循环的过程,而是使用其中的foo.remove()。我并不反对第一个示例中使用enumerate()。显然,由于第1个和第3个示例执行不同的操作,因此列表推导式生成了与第3个示例相似的结果。 - jfs
1
@GreenMatt: 我同意,成为Pythonic并不是强制性的,但是如果我从来没有听说过_javanic_或_Cinic_,那是因为Python提供了一种略微不同的关于编程和编写代码的思考方式,我认为尽早理解这些思想是更好的。但是,在某些情况下,你只需要一个索引而已,这种情况下我使用 xrange(len(l)),并不觉得自己不够Pythonic... :) - neurino
显示剩余3条评论

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