Pythonloopslistsmutability

Python: в цикле for меняю переменную, а список не меняется

Переменная цикла в Python — это отдельное имя, а не ячейка списка, поэтому присваивание внутри for ничего не меняет и при этом не выдаёт ошибку. Разбираем, чем переприсваивание имени отличается от изменения объекта и почему вложенный список в том же цикле изменить получается, а число нет. Плюс три рабочих способа: запись по индексу, списковое включение и срезовое присваивание.

9 мин чтенияСправочникPython · loops · lists · mutability · beginners

Переменная цикла — это отдельное имя, а не ячейка списка, поэтому строка price = price * 2 внутри for перенаправляет имя на новое число и до самого списка не доходит. Чтобы список действительно изменился, в него надо писать: по индексу через enumerate, пересобрать его списковым включением или заменить содержимое срезом prices[:] = ....

Вот код, с которого начинается вопрос.

prices = [120, 250, 90]
for price in prices:
    price = price * 2
print(prices)
[120, 250, 90]

Исключения нет, traceback не падает, программа отработала — и не сделала ничего. Это худший тип бага: молчаливый. Python выполнил ровно то, что написано, просто написано было не то.

Почему в Python в цикле for меняю переменную, а список не меняется?

Потому что for price in prices не выдаёт тебе ячейку списка. На каждой итерации Python берёт очередной элемент и привязывает к нему имя price — как если бы ты вручную написал price = prices[0], затем price = prices[1] и так далее. После этой привязки имя и ячейка списка живут отдельно друг от друга. Присваивание price = price * 2 меняет только то, куда смотрит имя price, а список о существовании этого имени ничего не знает.

Убедиться можно за две строки: имя price спокойно переживает цикл и хранит последний элемент.

prices = [120, 250, 90]
for price in prices:
    pass
print(price)
90

Если бы price был «окошком в список», после цикла такой переменной не существовало бы. А она есть — обычное имя.

Переменная цикла — это новое имя, а не ячейка списка

Формулировка «в Python for x in list — присваивание не работает» описывает симптом точно, но объясняет его наоборот. Присваивание как раз работает, и работает безупречно: оно связывает имя слева со значением справа. Просто имя слева — не часть списка.

В Python имя — это ярлык, наклеенный на объект. prices — ярлык на объект-список, price — ярлык на объект-число, который в этот момент лежит в списке. Когда ты пишешь price = price * 2, Python вычисляет новое число 240 и переклеивает ярлык price на него. Содержимого списка это не касается: там по-прежнему 120.

Вот почему изменения в цикле не сохраняются: Python записывает их в имя, а не в список. Ты каждый раз создаёшь новое значение и теряешь его на следующей итерации.

Так что жалоба «в Python в цикле for меняю переменную, а список не меняется» описывает не баг языка, а разницу между именем и объектом.

Присваивание против изменения: одна строка, два разных действия

В Python переприсваивание и изменение объекта — разница, которая решает всё; это главное, что стоит унести из статьи. Переприсваивание (имя = ...) трогает только имя. Изменение (объект[i] = ..., объект.append(...), объект.sort()) трогает сам объект, и это видно всем, кто на него ссылается.

Строка в теле цикла Что реально происходит Внешний список
price = price * 2 имя price переклеивается на новое число не меняется
price += 100 у чисел это то же самое, что price = price + 100 не меняется
prices[i] = price * 2 запись в ячейку списка по индексу меняется
row.append(0) метод правит сам объект-список меняется
row = row + [0] создаётся новый список, имя переклеивается на него не меняется
row += [0] для списка += — это расширение на месте меняется

Обрати внимание на две последние строки: row = row + [0] и row += [0] выглядят синонимами, но делают разное. Для чисел += действительно эквивалентен пересозданию, а для списка расширяет существующий объект.

Почему в Python вложенный список меняется в цикле, а числа нет?

Потому что элементы разного типа ведут себя по-разному. Число неизменяемо: с ним нельзя сделать ничего, кроме как вычислить новое число и привязать к имени. Список изменяем: у него есть методы, которые правят содержимое на месте, а переменная цикла указывает ровно на тот же объект, что лежит внутри внешнего списка. Поэтому row.append(0) виден снаружи, а price = price * 2 — нет. Один и тот же цикл, противоположные исходы.

rows = [[1, 2], [3, 4]]
for row in rows:
    row.append(0)
print(rows)
[[1, 2, 0], [3, 4, 0]]

А теперь то же самое, но через переприсваивание:

rows = [[1, 2], [3, 4]]
for row in rows:
    row = row + [0]
print(rows)
[[1, 2], [3, 4]]

Тот же цикл, тот же список, разница в одном символе — и противоположный результат. Из-за этой пары поведение Python кажется произвольным, пока не разведёшь в голове имена и объекты.

Тот же принцип объясняет, почему цикл по списку словарей из JSON правит данные без всяких ухищрений: словарь изменяем, поэтому item["итого"] = ... внутри for item in items доходит до самого списка. А вот обращение к внешнему списку по имени поля — items["цена"] вместо items[0]["цена"] — даёт совсем другую ошибку, list indices must be integers or slices, not str.

Кстати, изменяемость списка — это ровно то свойство, из-за которого его нельзя положить в множество или сделать ключом словаря. Там оно вылезает громко, как TypeError: unhashable type: 'list', а здесь — молча.

Как изменить элементы списка в цикле Python?

Рабочих способов три, и отличаются они не стилем, а тем, останется ли список тем же объектом. Запись по индексу и срезовое присваивание правят существующий список; списковое включение создаёт новый и вешает на него старое имя. Если на список ссылается кто-то ещё — например, он пришёл аргументом в функцию, — эта разница решающая.

Способ 1: изменить элемент по индексу в цикле

prices = [120, 250, 90]
for i, price in enumerate(prices):
    prices[i] = price * 2
print(prices)
[240, 500, 180]

Чтобы в Python изменить элемент по индексу в цикле, нужен сам индекс — его даёт enumerate, отдавая на каждой итерации пару «индекс, элемент». Индекс и есть недостающая ячейка: prices[i] = ... пишет в список, а не в имя. Справка — в разделе enumerate. Без него получится for i in range(len(prices)): работает, но читается хуже.

Потренироваться на индексах можно на задаче «Где большие буквы»: там нужны позиции заглавных букв, то есть индексы, а не сами символы.

Способ 2: пересобрать список включением

prices = [120, 250, 90]
prices = [price * 2 for price in prices]
print(prices)
[240, 500, 180]

Самый частый и читаемый вариант, когда правило одно на все элементы. Никакой магии: справа собирается новый список, слева имя prices переклеивается на него. Формы включений — в разделе Comprehensions.

Задача «Только неубывающие» — как раз про пересборку: оставить в новом списке только элементы не меньше предыдущего.

Способ 3: срезовое присваивание, когда нужен тот же объект

cart = [120, 250, 90]
backup = cart
cart[:] = [price * 2 for price in cart]
print(backup)
[240, 500, 180]

cart[:] = ... не создаёт новый список — он заменяет содержимое существующего. Поэтому имя backup, которое смотрит на тот же объект, видит изменения. Сравни с обычным присваиванием:

cart = [120, 250, 90]
backup = cart
cart = [price * 2 for price in cart]
print(backup)
[120, 250, 90]
Способ Код Тот же объект списка Когда брать
Запись по индексу for i, x in enumerate(a): a[i] = f(x) да нужна логика с if, continue, доступом к соседям
Списковое включение a = [f(x) for x in a] нет, новый список обычный случай: одно правило на все элементы
Срезовое присваивание a[:] = [f(x) for x in a] да на список ссылается кто-то ещё

Почему append() внутри цикла работает, а присваивание — нет?

Потому что append() — это метод, который меняет сам объект-список, а присваивание — операция над именем. Когда ты добавляешь элементы в отдельный список-накопитель, ты вызываешь метод на объекте, и результат остаётся в объекте. Противоречия с предыдущими примерами нет: работает то, что трогает объект, и не работает то, что трогает только имя.

result = []
for price in [120, 250, 90]:
    result.append(price * 2)
print(result)
[240, 500, 180]

Это четвёртый рабочий способ, и для новичка он часто самый понятный: исходный список не трогаем вообще, а ответ собираем рядом.

Строки и числа неизменяемы: то же правило, другой симптом

Со строками ловушка убедительнее: там вызывается метод, а значит кажется, что объект меняется.

words = ["хлеб", "молоко"]
for word in words:
    word = word.upper()
print(words)
['хлеб', 'молоко']

word.upper() не меняет строку, а возвращает новую. Строки в Python неизменяемы: любой «изменяющий» метод строки конструирует другой объект. Дальше срабатывает знакомый механизм — результат привязывается к имени word и теряется на следующей итерации. Чинится так же: words[i] = word.upper() через enumerate или words = [w.upper() for w in words].

У кортежа то же свойство доведено до конца: у него нет ни append, ни записи по индексу, и попытка point[0] = 999 немедленно даёт TypeError: 'tuple' object does not support item assignment. Кортеж хотя бы честно ругается — список и число молчат.

Почему то же самое ломает функции, которым передали список?

Потому что параметр функции — тоже просто имя. При вызове double_all(cart) имя prices внутри функции привязывается к тому же объекту, что и cart снаружи. Если внутри написать prices = [...], имя переклеится на новый список, а внешний cart останется прежним. Это ровно та же ошибка, что и с переменной цикла, просто на другом уровне.

def double_all(prices):
    prices = [p * 2 for p in prices]

cart = [120, 250, 90]
double_all(cart)
print(cart)
[120, 250, 90]

Срезовое присваивание чинит это одной правкой:

def double_all(prices):
    prices[:] = [p * 2 for p in prices]

cart = [120, 250, 90]
double_all(cart)
print(cart)
[240, 500, 180]

Второй честный вариант — вернуть новый список и присвоить его на вызывающей стороне: cart = double_all(cart). Он предпочтительнее, когда функция не обязана ничего портить у вызывающего.

Частые ошибки

  1. Добавили enumerate, но пишут всё равно в переменную. for i, price in enumerate(prices): price = price * 2 не меняет список — индекс получен, но не использован. Правильно: prices[i] = price * 2.
  2. Считают, что += всегда работает на месте. Для списка row += [0] расширяет объект, для числа price += 100 создаёт новое число. Одна и та же запись, разные последствия.
  3. Присваивают новый список внутри функции. prices = [...] в теле функции не виден снаружи. Нужно либо prices[:] = ..., либо return и присваивание у вызывающего.
  4. Путают backup = prices с копией. Это второе имя на тот же объект: prices[0] = 999 изменит и backup. Настоящая копия — prices.copy() или prices[:]; подводные камни собраны в разделе Shallow and deep list copies.
  5. Создают сетку через [row] * 3. Так получаются три ссылки на один список, и grid[0][0] = 5 даёт [[5, 0], [5, 0], [5, 0]]. Нужно [[0, 0] for _ in range(3)].
  6. Удаляют элементы прямо во время итерации. Это другой баг с другим механизмом: цикл идёт по индексу, а список сжимается, и часть элементов пропускается — разбор в статье про удаление из списка в цикле.
  7. Ждут, что цикл по dict.items() изменит словарь. for name, count in totals.items(): count = count * 10 не меняет ничего — работает только totals[name] = count * 10.

Мини-резюме

  • for x in a привязывает к x очередной элемент; x — обычное имя, а не ячейка списка.
  • Присваивание меняет имя, изменение меняет объект — от этого зависит, увидит ли список правку.
  • Числа и строки неизменяемы, поэтому с ними «работает» только пересборка списка.
  • Вложенный список меняется в цикле, а числа нет — потому что у списка есть методы, правящие его на месте.
  • Три рабочих фикса: a[i] = ... через enumerate, a = [f(x) for x in a], a[:] = [f(x) for x in a].
  • Срезовое присваивание — единственный из трёх, который сохраняет тот же объект: бери его, когда список пришёл в функцию аргументом.
  • Внутри функции prices = [...] не влияет на аргумент вызывающего; либо prices[:] = ..., либо return.

Закрепи на практике

Решай задачи в Python-тренажёре с мгновенной проверкой и подсказками.

Открыть тренажёр