바쁜 분은 이것만
- 딕셔너리를 for문으로 도는 도중에 키를 지우거나 새로 넣으면 나는 에러 (값만 바꾸는 건 괜찮음)
- 가장 쉬운 고치는 법은
for k in list(d):처럼 키 목록을 복사해서 반복하기 - 걸러 내는 거라면
d = {k: v for k, v in d.items() if 조건}, set도 똑같이Set changed size에러가 남
RuntimeError: dictionary changed size during iteration은 "딕셔너리를 하나씩 꺼내는 중인데 그사이 크기가 바뀌었다"는 뜻입니다. 파이썬은 반복하는 동안 딕셔너리에 키가 늘거나 줄면 어디까지 돌았는지 믿을 수 없어서 바로 멈춰 버려요. Python 3.14에서 원인과 고치는 방법을 직접 실행해 정리했습니다.
1. 반복 중에 키를 지울 때 (가장 흔한 경우)
재고가 0인 과일을 딕셔너리에서 빼려는 코드입니다.
stock = {"사과": 3, "배": 0, "귤": 5, "감": 0}
for name in stock:
if stock[name] == 0:
del stock[name]
print(stock)
Traceback (most recent call last):
File "~/work/p1.py", line 2, in <module>
for name in stock:
^^^^^
RuntimeError: dictionary changed size during iteration
"배"를 지운 순간 크기가 4에서 3으로 바뀌고, 다음 키를 꺼내려는 for문 줄에서 에러가 납니다. 에러 표시가 del 줄이 아니라 for 줄을 가리키는 게 특징이에요.
2. 반복 중에 키를 추가할 때
scores = {"kim": 90}
for name in scores:
scores[name + "_copy"] = scores[name]
Traceback (most recent call last):
File "~/work/p2.py", line 2, in <module>
for name in scores:
^^^^^^
RuntimeError: dictionary changed size during iteration
지우는 것뿐 아니라 새 키를 넣어도 똑같습니다. 만약 이게 막혀 있지 않다면 새로 넣은 키를 또 꺼내고, 또 넣고… 끝나지 않는 반복이 될 수도 있어요.
3. 값만 바꾸는 건 괜찮다
# 값만 바꾸는 건 괜찮다 (크기가 그대로)
prices = {"사과": 1000, "배": 2000}
for name in prices:
prices[name] = prices[name] * 2
print(prices)
# 지웠다가 다른 키를 넣어 크기를 맞추면?
d = {"a": 1, "b": 2}
try:
for k in d:
del d[k]
d[k + "!"] = 0
except RuntimeError as e:
print("RuntimeError:", e)
{'사과': 2000, '배': 4000}
RuntimeError: dictionary keys changed during iteration
이미 있는 키의 값을 바꾸는 것은 크기가 그대로라 문제없어요. 그런데 하나 지우고 하나 넣어서 크기를 억지로 맞춰도 Python 3.14는 알아챕니다. 이때는 메시지가 dictionary keys changed during iteration으로 조금 다르게 나와요. 요령으로 피하지 말고 아래 방법으로 고치는 게 맞습니다.
4. 고치는 법 ①: list(d)로 키 목록을 복사해서 반복
stock = {"사과": 3, "배": 0, "귤": 5, "감": 0}
for name in list(stock): # 키 목록을 복사해서 반복
if stock[name] == 0:
del stock[name]
print(stock)
stock2 = {"사과": 3, "배": 0, "귤": 5, "감": 0}
for name in list(stock2.keys()): # keys()를 list로 감싸도 같다
if stock2[name] == 0:
del stock2[name]
print(stock2)
{'사과': 3, '귤': 5}
{'사과': 3, '귤': 5}
list(stock)은 그 순간의 키들을 새 리스트로 복사합니다. for문은 이 리스트를 도니까 원래 딕셔너리에서 키를 지워도 상관없어요. 코드 한 줄만 바꾸면 돼서 가장 많이 쓰는 방법입니다.
주의할 점은 stock.keys()만 쓰면 복사가 아니라는 것이에요.
stock = {"사과": 3, "배": 0, "귤": 5, "감": 0}
for name in stock.keys(): # keys()만 쓰면 복사가 아니다
if stock[name] == 0:
del stock[name]
Traceback (most recent call last):
File "~/work/p5.py", line 2, in <module>
for name in stock.keys(): # keys()만 쓰면 복사가 아니다
~~~~~~~~~~^^
RuntimeError: dictionary changed size during iteration
파이썬 3의 keys(), values(), items()는 원래 딕셔너리를 그대로 비춰 보는 "뷰(view)"라서, 딕셔너리가 바뀌면 같이 바뀝니다. 그래서 반드시 list()로 감싸야 해요. 예전 파이썬 2 코드에서 for k in d.keys():로 지우던 코드가 3에서 에러가 나는 이유이기도 합니다.
5. 고치는 법 ②: 딕셔너리 컴프리헨션으로 새로 만들기
stock = {"사과": 3, "배": 0, "귤": 5, "감": 0}
stock = {k: v for k, v in stock.items() if v != 0}
print(stock)
# 지울 키를 먼저 모은 뒤 따로 지우기
stock2 = {"사과": 3, "배": 0, "귤": 5, "감": 0}
to_delete = [k for k, v in stock2.items() if v == 0]
print("지울 키:", to_delete)
for k in to_delete:
del stock2[k]
print(stock2)
{'사과': 3, '귤': 5}
지울 키: ['배', '감']
{'사과': 3, '귤': 5}
- 컴프리헨션: "남길 것만 골라 새 딕셔너리를 만든다"는 방식이라 원본을 건드리지 않아요. 걸러 내는 작업이면 이게 가장 읽기 쉽습니다.
- 지울 키 먼저 모으기: 다른 곳에서 같은 딕셔너리 객체를 쓰고 있어서 원본 자체를 고쳐야 할 때 좋아요. 컴프리헨션은 새 객체를 만들기 때문에 다른 변수가 가리키는 원래 딕셔너리는 그대로 남거든요.
6. 고치는 법 ③: 추가는 반복이 끝난 뒤 한 번에
scores = {"kim": 90, "lee": 80}
extra = {}
for name, s in scores.items():
extra[name + "_보너스"] = s + 5
scores.update(extra) # 반복이 끝난 뒤 한 번에 추가
print(scores)
{'kim': 90, 'lee': 80, 'kim_보너스': 95, 'lee_보너스': 85}
새로 넣을 것들은 다른 딕셔너리에 모아 두고, for문이 끝난 다음 update()로 합칩니다.
7. set도 같은 에러가 난다
nums = {1, 2, 3, 4}
for n in nums:
if n % 2 == 0:
nums.remove(n)
Traceback (most recent call last):
File "~/work/p8.py", line 2, in <module>
for n in nums:
^^^^
RuntimeError: Set changed size during iteration
set은 메시지 첫 글자가 대문자 Set인 것만 다르고 원인은 같아요. 고치는 법도 같습니다.
nums = {1, 2, 3, 4}
nums = {n for n in nums if n % 2 != 0}
print(nums)
nums2 = {1, 2, 3, 4}
for n in set(nums2): # 복사본으로 반복
if n % 2 == 0:
nums2.discard(n)
print(nums2)
{1, 3}
{1, 3}
8. 리스트는 에러 없이 틀린 값이 나온다 (더 위험)
리스트는 반복 중에 지워도 에러가 나지 않습니다. 대신 조용히 하나를 건너뛰어요.
# 리스트는 에러 없이 조용히 하나를 건너뛴다
nums = [2, 4, 5, 6]
for n in nums:
if n % 2 == 0:
nums.remove(n)
print(nums)
[4, 5]
짝수를 다 지우려 했는데 4가 남았어요. 2를 지우면 뒤 값들이 한 칸씩 당겨지는데, for문은 다음 칸(원래 5가 있던 자리)으로 넘어가서 4를 못 보고 지나칩니다. 딕셔너리·set은 에러로 알려 주기라도 하지만 리스트는 아니니, 리스트도 똑같이 for n in nums[:]처럼 복사본을 돌거나 컴프리헨션으로 새로 만드세요. 리스트 복사 방법은 파이썬 리스트 복사 글에 정리해 두었어요.
9. 덤: iter()로 직접 꺼낼 때와 popitem()
d = {"a": 1, "b": 2, "c": 3}
it = iter(d.items())
print(next(it))
d["d"] = 4
try:
print(next(it))
except RuntimeError as e:
print("RuntimeError:", e)
# pop으로 하나씩 빼며 비우는 건 반복이 아니라 괜찮다
while d:
k, v = d.popitem()
print("꺼냄:", k, v)
print(d)
('a', 1)
RuntimeError: dictionary changed size during iteration
꺼냄: d 4
꺼냄: c 3
꺼냄: b 2
꺼냄: a 1
{}
for문이 아니어도 iter()로 만든 반복자를 쓰는 중에 크기가 바뀌면 다음 next()에서 같은 에러가 납니다. 반대로 하나씩 빼면서 처리하고 비우는 일이라면 while d: d.popitem()처럼 반복자 없이 쓰면 돼요. popitem()은 마지막에 넣은 것부터 꺼냅니다.
정리
| 하려던 일 | 고친 코드 |
|---|---|
| 반복 중 키 삭제 | for k in list(d): (d.keys()만 쓰면 안 됨) |
| 조건에 맞는 것만 남기기 | d = {k: v for k, v in d.items() if 조건} |
| 원본 객체를 그대로 고쳐야 함 | 지울 키를 리스트에 모은 뒤 따로 del |
| 반복 중 키 추가 | 다른 딕셔너리에 모아서 끝난 뒤 update() |
| 값만 바꾸기 | 그대로 OK (크기가 안 바뀜) |
set (Set changed size) |
for n in set(s): 또는 {n for n in s if 조건} |
| 리스트 (에러 없이 건너뜀) | for n in nums[:]: 또는 리스트 컴프리헨션 |
참고: Python 공식 문서 — Built-in Types: Dictionary view objects("Iterating views while adding or deleting entries in the dictionary may raise a RuntimeError"), dict.popitem, set / Tutorial 5.5 Dictionaries, 4.2 for Statements
