
range는 정수 집합을 순회할 때 쓰라고 만든 도구입니다. 난수를 여덟 번 뽑거나 0부터 9까지 더하는 코드라면 range가 정확히 제자리를 찾은 셈입니다.
문제는 이 도구가 리스트 순회에까지 끌려 나올 때 생깁니다. "인덱스도 같이 필요하다"는 이유로 range(len(some_list))를 쓰는 순간, 코드는 갑자기 세 가지 일을 동시에 하게 됩니다. 리스트의 길이를 재고, 그 길이만큼 정수를 만들고, 그 정수로 다시 원소를 꺼냅니다. 정작 하고 싶었던 일은 "원소를 하나씩 보는 것" 하나뿐인데 말입니다. 다른 언어에서 넘어온 분들이 가장 오래 끌고 오는 습관이기도 합니다.
flavor_list = ['바닐라', '초콜릿', '피칸', '딸기']
for i in range(len(flavor_list)):
flavor = flavor_list[i] # 인덱스로 다시 꺼내는 한 줄이 매번 따라붙습니다
print(f'{i + 1}: {flavor}')
읽는 사람은 flavor_list[i]를 보고 나서야 이 루프가 리스트 순회라는 사실을 알게 됩니다. 인덱스 자체는 출력에만 쓰이는데 루프 구조 전체가 인덱스를 중심으로 짜여 있는 셈입니다. 원소를 꺼내는 줄 하나가 더 필요하다는 점도 군더더기입니다.
flavor_list = ['바닐라', '초콜릿', '피칸', '딸기']
for i, flavor in enumerate(flavor_list):
print(f'{i + 1}: {flavor}')
# 시작값을 두 번째 인자로 넘기면 +1 보정도 사라집니다
for i, flavor in enumerate(flavor_list, 1):
print(f'{i}: {flavor}')
한 줄이 줄어든 것보다 중요한 것은 의도가 드러난다는 점입니다. enumerate가 보이는 순간 "인덱스와 값을 함께 쓰는 루프"라는 사실이 즉시 읽힙니다. 시작값 인자를 활용하면 i + 1 같은 보정이 사라져 실수의 여지도 함께 줄어듭니다.
성능 면에서도 손해가 없습니다. enumerate는 원본을 통째로 복사하지 않고 지연 계산 제너레이터(generator)로 감싸서, 요청받을 때마다 인덱스와 값의 쌍을 하나씩 만들어 냅니다.
it = enumerate(flavor_list)
print(next(it)) # (0, '바닐라')
print(next(it)) # (1, '초콜릿')
덕분에 길이를 미리 알 수 없는 이터레이터(iterator)나 파일 객체에도 그대로 쓸 수 있습니다. range(len(...))은 len을 지원하는 대상에만 쓸 수 있으니, 적용 범위 자체가 다릅니다.
enumerate를 쓰면 인덱스와 원소를 한 번에 받아 루프가 간결해집니다.range 위에서 인덱스로 원소를 다시 꺼내는 패턴은 대부분 enumerate로 바꿀 수 있습니다.enumerate는 지연 계산 제너레이터이므로 메모리를 추가로 크게 쓰지 않습니다.range와의 결정적 차이입니다.대용량 로그 파일을 한 줄씩 처리하면서 오류 위치를 알려 줘야 하는 스크립트에서 특히 잘 맞습니다. for line_no, line in enumerate(f, 1)로 열어 두면 파일 전체를 메모리에 올리지 않고도 사람이 읽는 방식 그대로의 줄 번호를 예외 메시지에 담을 수 있습니다.
배치 작업의 진행률 로깅에도 유용합니다. for i, item in enumerate(items, 1) 뒤에 if i % 1000 == 0 조건을 두면 별도 카운터 변수를 만들고 증가시키는 코드 없이 진행 상황을 남길 수 있습니다. 루프 밖에서 관리하는 상태가 하나 줄어드는 것만으로도 실수가 눈에 띄게 줄어듭니다.