为什么我可以在 for 循环中使用列表索引作为索引变量?

我有以下密码:

a = [0,1,2,3]


for a[-1] in a:
print(a[-1])

输出结果是:

0
1
2
2

我对为什么列表索引可以用作 for 循环中的索引变量感到困惑。

7678 次浏览

a[-1] refers to the last element of a, in this case a[3]. The for loop is a bit unusual in that it is using this element as the loop variable. It's not evaluating that element upon loop entry, but rather it is assigning to it on each iteration through the loop.

So first a[-1] gets set to 0, then 1, then 2. Finally, on the last iteration, the for loop retrieves a[3] which at that point is 2, so the list ends up as [0, 1, 2, 2].

A more typical for loop uses a simple local variable name as the loop variable, e.g. for x .... In that case, x is set to the next value upon each iteration. This case is no different, except that a[-1] is set to the next value upon each iteration. You don't see this very often, but it's consistent.

It is an interesting question, and you can understand it by that:

for v in a:
a[-1] = v
print(a[-1])


print(a)

actually a becomes: [0, 1, 2, 2] after loop

Output:

0
1
2
2
[0, 1, 2, 2]

I hope that helps you, and comment if you have further questions. : )

List indexes such as a[-1] in the expression for a[-1] in a are valid as specified by the for_stmt (and specifically the target_list) grammar token, where slicing is a valid target for assignment.

"Huh? Assignment? What has that got to do with my output?"

Indeed, it has everything to do with the output and result. Let's dive into the documentation for a for-in loop:

for_stmt ::=  "for" target_list "in" expression_list ":" suite

The expression list is evaluated once; it should yield an iterable object. An iterator is created for the result of the expression_list. The suite is then executed once for each item provided by the iterator, in the order returned by the iterator. Each item in turn is assigned to the target list using the standard rules for assignments (see Assignment statements), and then the suite is executed.

(emphasis added)
N.B. the suite refers to the statement(s) under the for-block, print(a[-1]) in our particular case.

Let's have a little fun and extend the print statement:

a = [0, 1, 2, 3]
for a[-1] in a:
print(a, a[-1])

This gives the following output:

[0, 1, 2, 0] 0    # a[-1] assigned 0
[0, 1, 2, 1] 1    # a[-1] assigned 1
[0, 1, 2, 2] 2    # a[-1] assigned 2
[0, 1, 2, 2] 2    # a[-1] assigned 2 (itself)

(comments added)

Here, a[-1] changes on each iteration and we see this change propagated to a. Again, this is possible due to slicing being a valid target.

A good argument made by Ev. Kounis regards the first sentence of the quoted doc above: "The expression list is evaluated once". Does this not imply that the expression list is static and immutable, constant at [0, 1, 2, 3]? Shouldn't a[-1] thus be assigned 3 at the final iteration?

Well, Konrad Rudolph asserts that:

No, [the expression list is] evaluated once to create an iterable object. But that iterable object still iterates over the original data, not a copy of it.

(emphasis added)

The following code demonstrates how an iterable it lazily yields elements of a list x.

x = [1, 2, 3, 4]
it = iter(x)
print(next(it))    # 1
print(next(it))    # 2
print(next(it))    # 3
x[-1] = 0
print(next(it))    # 0

(code inspired by Kounis')

If evaluation was eager, we could expect x[-1] = 0 to have zero effect on it and expect 4 to be printed. This is clearly not the case and goes to show that by the same principle, our for-loop lazily yields numbers from a following assignments to a[-1] on each iteration.

The left expression of a for loop statement gets assigned with each item in the iterable on the right in each iteration, so

for n in a:
print(n)

is just a fancy way of doing:

for i in range(len(a)):
n = a[i]
print(n)

Likewise,

for a[-1] in a:
print(a[-1])

is just a fancy way of doing:

for i in range(len(a)):
a[-1] = a[i]
print(a[-1])

where in each iteration, the last item of a gets assigned with the next item in a, so when the iteration finally comes to the last item, its value got last assigned with the second-last item, 2.

(This is more of a long comment than an answer - there are a couple of good ones already, especially @TrebledJ's. But I had to think of it explicitly in terms of overwriting variables that already have values before it clicked for me.)

If you had

x = 0
l = [1, 2, 3]
for x in l:
print(x)

you wouldn't be surprised that x is overridden each time through the loop. Even though x existed before, its value isn't used (i.e. for 0 in l:, which would throw an error). Rather, we assign the values from l to x.

When we do

a = [0, 1, 2, 3]


for a[-1] in a:
print(a[-1])

even though a[-1] already exists and has a value, we don't put that value in but rather assign to a[-1] each time through the loop.

The answer by TrebledJ explains the technical reason of why this is possible.

Why would you want to do this though?

Suppose I have an algorithm that operates on an array:

x = np.arange(5)

And I want to test the result of the algorithm using different values of the first index. I can simply skip the first value, reconstructing an array every time:

for i in range(5):
print(np.r_[i, x[1:]].sum())

(np.r_)

This will create a new array on every iteration, which is not ideal, in particular if the array is large. To reuse the same memory on every iteration, I can rewrite it as:

for i in range(5):
x[0] = i
print(x.sum())

Which is probably clearer than the first version too.

But that is exactly identical to the more compact way to write this:

for x[0] in range(5):
print(x.sum())

all of the above will result in:

10
11
12
13
14

Now this is a trivial "algorithm", but there will be more complicated purposes where one might want to test changing a single (or multiple, but that complicating things due to assignment unpacking) value in an array to a number of values, preferably without copying the entire array. In this case, you may want to use an indexed value in a loop, but be prepared to confuse anyone maintaining your code (including yourself). For this reason, the second version explicitly assigning x[0] = i is probably preferable, but if you prefer the more compact for x[0] in range(5) style, this should be a valid use case.