파일 세 개에 1번부터 번호를 붙이려는데 마지막 파일만 빠졌다. 반복문은 에러 없이 끝났고, 출력도 얼핏 멀쩡하다. 이런 오류가 더 얄밉다. range()가 받는 두 번째 숫자는 마지막으로 출력할 값이 아니라 멈출 경계이기 때문이다.
range(1, 4)는 왜 4를 출력하지 않을까?
range(start, stop)은 start에서 시작해 stop 직전에서 끝난다. 숫자로 먼저 보면 단순하다.
print(list(range(1, 4))) # [1, 2, 3]
print(list(range(4))) # [0, 1, 2, 3]stop은 포함하지 않는다. 그래서 1부터 4까지 필요하면 range(1, 5)라고 쓴다. range(4)처럼 인수를 하나만 주면 시작값은 0이다.
파일 세 개에 번호를 붙이면 어디에서 빠질까?
파일 목록 하나를 끝까지 사용해 보자. len(files)는 3이지만, 아래 반복의 끝값 3은 포함되지 않는다.
files = ["app.log", "db.log", "web.log"]
for number in range(1, len(files)):
print(number, files[number - 1])1 app.log
2 db.log마지막 파일에 번호가 붙지 않은 이유는 web.log가 사라져서가 아니다. 반복할 번호 자체가 [1, 2]에서 멈췄다. 1부터 파일 개수까지 필요하다면 끝 경계에 1을 더한다.
for number in range(1, len(files) + 1):
print(number, files[number - 1])1 app.log
2 db.log
3 web.log여기서 number - 1도 중요하다. 사람에게 보여 줄 번호는 1부터, 리스트 인덱스는 0부터 시작한다. 이 차이를 놓고 files[number]라고 쓰면 처음 파일을 건너뛰고 끝에서 IndexError를 만난다.
번호만 필요하다면 enumerate()가 더 간단할까?
목적이 ‘파일마다 1부터 번호 붙이기’라면 인덱스 계산을 직접 하지 않아도 된다.
for number, filename in enumerate(files, start=1):
print(number, filename)출력은 앞의 수정된 반복문과 같다. enumerate()는 번호와 해당 파일을 함께 주므로 len(files) + 1이나 number - 1을 잘못 조합할 일이 줄어든다. 반대로 인덱스 자체가 필요하면 range(len(files))가 0, 1, 2를 만들며, 그 값은 files[index]에 그대로 사용할 수 있다.
경계 오류는 항목이 여러 개일 때만 생기지 않는다. 빈 목록과 한 항목도 확인해 두면 좋다.
for count in (0, 1, 3):
numbers = list(range(1, count + 1))
assert len(numbers) == count이 검사는 ‘원하는 개수만큼 반복했는가’를 확인한다. 출력 내용이 중요한 작업이라면 마지막 항목까지 처리됐는지도 함께 검사해야 한다.
핵심 요약
range(start, stop)의 stop은 포함되지 않는 끝 경계다. 1부터 N까지는 range(1, N + 1), 0부터 시작하는 리스트 인덱스는 range(N)을 쓴다. 목록의 항목에 번호를 붙이는 목적이라면 enumerate(items, start=1)이 더 읽기 쉽고 실수할 자리도 적다.

