Logo

파이썬의 range() 내장 함수로 정수 범위 만들기 (feat. for 루프)

다른 프로그래밍 언어를 쓰시다가 파이썬으로 넘어온 분들이 for 루프 때문에 적지 않게 당황하시는 것을 자주 보게 됩니다.

다른 언어에서는 일반적으로 for 루프를 작성할 때 항상 다음 3가지를 명시를 하면서 시작하죠?

  • 인덱스 변수의 초기 값
  • 반복 지속 조건
  • 인덱스 변수 갱신 방법

예를 들어, 자바의 경우 보통 다음과 같은 형태로 for 루프를 돌고요.

for (int i = 0; i < letters.length; i++) {
    System.out.println(letters[i]);
}

자바스크립트에서 for 루프를 돌리는 모습도 크게 다르지는 않습니다.

for (let i = 0; i < letters.length; i++) {
  console.log(letters[i]);
}

하지만 파이썬에서는 이러한 전형적인 for 문법을 제공하지 않고, 비교적 다른 언어에서는 나중에 추가된 for-in 문법이 기본으로 채택이 되었어요.

그래서 위에서 다른 언어로 작성된 for 루프를 굳이 파이썬의 문법으로 옮겨보면 아래와 같은 작성할 수 있을 것입니다.

for idx in range(len(letters)):
    print(letters[idx])

여기서 눈에 띄는 부분은 바로 in 키워드 바로 뒷 부분에 range()라는 함수가 사용되고 있다는 것인데요. 👀

이번 포스팅에서는 파이썬에서 이 range() 함수가 도대체 뭐길래 이렇게 for 루프에서 자주 보이는지 자세히 파해쳐보도록 할께요.

기본 문법

range() 함수는 파이썬이라는 언어 자체에 내장이 되어 있어서 마치 forin 키워드를 사용하듯이 어디서나 사용할 수 있습니다.

파이썬에서 range() 함수는 말 그대로 “범위”를 만들 때 사용하는 함수인데요. 좀 더 정확히 얘기하면 정수로 이뤄진 범위를 만들어주죠.

정수로 범위를 정의하려면 범위가 어느 숫자에서 시작하고 어느 숫자에서 끝나는지, 그리고 숫자 간에 간격을 설정해야 하겠죠? 그래서 range() 내장 함수는 최대 3개의 인자를 받는데요.

range(<시작값>, <종료값>, <증분>)

2개의 인자를 넘기면 각각 시작값과 종료값으로 사용되며, 증분은 기본값인 1이 적용되고요. (범위 내의 숫자가 1씩 커집니다.)

range(<시작값>, <종료값>)

1개의 인자를 넘기면 종료값으로 사용되며, 시작값은 기본값인 0이 적용됩니다.

range(<종료값>)

이렇게 가변 인자를 받는 range() 함수는 인자를 어떻게 넘기느냐에 따라 다양한 범위를 반환하게 됩니다.

범위의 시작과 종료

아마 제일 흔하게 볼 수 있는 range() 함수의 사용 방법은 범위가 종료되야 할 숫자 하나만 인자로 넘기는 건데요. 이럴 경우, 시작값은 자동으로 0이 됩니다.

range(<종료값>)

여기서 주의 사항은 range() 함수가 반환하는 범위에는 인자로 넘어간 종료값이 포함이 되지 않는다는 거에요. 다시 말해, 범위는 0에서 시작하고 종료값에서 1을 뺀 값에서 끝납니다.

예를 들어, 0부터 9까지의 범위를 만들고 싶다면 range() 함수에 10을 인자로 넘기면 되겠죠?

for num in range(10):
    print(num)
결과
0
1
2
3
4
5
6
7
8
9

만약에 0 대신에 다를 숫자에서 범위를 시작하고 싶다면 어떻게 해야 할까요? 그럴 때는 range() 함수에 첫번째 인자로 시작값를 넘기고, 두번째 인자로 종료값를 넘기면 됩니다.

range(<시작값>, <종료값>)

예를 들어, 3에서 시작해서 7에서 끝나는 범위를 만들고 싶다면, range() 함수의 첫번째 인자로 3을 넘기고 두번째 인자로 8을 넘기면 되겠죠?

for num in range(3, 8):
    print(num)
결과
3
4
5
6
7

종료값은 범위에 포함되지 않는다는 거 까먹지 않게 조심하세요 😛

범위의 증가와 감소

여태까지는 범위 내의 숫자가 1씩 커지고 있죠? range() 함수를 호출 할 때 세번째 인자까지 넘기면 숫자가 얼마큼씩 커질지도 조정할 수 있습니다.

range(<시작값>, <종료값>, <증분>)

예를 들어, 10부터 35까지 5씩 늘어나는 범위을 만들어볼까요?

for num in range(10, 36, 5):
    print(num)
결과
10
15
20
25
30
35

만약에 range() 함수의 세번째 인자로 음수를 넘기면 어떻게 될까요? 네, 맞습니다. 숫자가 줄어드는 간격이 만들어집니다.

for num in range(60, 34, -5):
    print(num)
결과
60
55
50
45
40
35

증분으로 음수를 사용할 때는 당연히 첫번째 인자가 두번째 인자보다 커야겠죠? 범위의 시작 값이 범위의 종료 값보다 커야 숫자가 점점 작아질 수 있을테니까요.

기본 응용

그럼 지금까지 배운 방법을 사용해서 range() 함수를 다양하게 응용해볼까요?

먼저 홀수로 이로어진 범위를 만들어 출력해보겠습니다.

for odd in range(1, 10, 2):
    print(odd, end=' ')
결과
1 3 5 7 9

그럼 짝수도 어렵지 않겠죠?

for even in range(2, 11, 2):
    print(even, end=' ')
결과
2 4 6 8 10

결과 값이 한줄이 출력이 되도록 print() 함수의 end 옵션을 사용했는데요. print() 함수에 대해서는 별도의 포스팅에서 자세히 다루었으니 참고 바랍니다.

이중 루프를 이용하면 구구단도 어렵지 않게 출력할 수 있겠네요!

for first in range(2, 10):
    for second in range(1, 10):
        print(f"{first} x {second} = {first * second}")
결과
2 x 1 = 2
2 x 2 = 4
2 x 3 = 6
2 x 4 = 8
2 x 5 = 10
2 x 6 = 12
2 x 7 = 14
2 x 8 = 16
2 x 9 = 18
3 x 1 = 3
3 x 2 = 6
3 x 3 = 9
3 x 4 = 12
3 x 5 = 15
3 x 6 = 18
3 x 7 = 21
3 x 8 = 24
3 x 9 = 27
4 x 1 = 4
4 x 2 = 8
4 x 3 = 12
4 x 4 = 16
4 x 5 = 20
4 x 6 = 24
4 x 7 = 28
4 x 8 = 32
4 x 9 = 36
5 x 1 = 5
5 x 2 = 10
5 x 3 = 15
5 x 4 = 20
5 x 5 = 25
5 x 6 = 30
5 x 7 = 35
5 x 8 = 40
5 x 9 = 45
6 x 1 = 6
6 x 2 = 12
6 x 3 = 18
6 x 4 = 24
6 x 5 = 30
6 x 6 = 36
6 x 7 = 42
6 x 8 = 48
6 x 9 = 54
7 x 1 = 7
7 x 2 = 14
7 x 3 = 21
7 x 4 = 28
7 x 5 = 35
7 x 6 = 42
7 x 7 = 49
7 x 8 = 56
7 x 9 = 63
8 x 1 = 8
8 x 2 = 16
8 x 3 = 24
8 x 4 = 32
8 x 5 = 40
8 x 6 = 48
8 x 7 = 56
8 x 8 = 64
8 x 9 = 72
9 x 1 = 9
9 x 2 = 18
9 x 3 = 27
9 x 4 = 36
9 x 5 = 45
9 x 6 = 54
9 x 7 = 63
9 x 8 = 72
9 x 9 = 81

위 코드에서 사용된 파이썬의 f-string에 대해서는 관련 포스팅을 참고 바랍니다.

자료구조 루프 돌기

range() 함수는 여러 원소를 담고 있는 리스트나 튜플, 문자열을 대상으로 루프 돌릴 때도 유용하게 사용할 수 있는데요. 기본 아이디어는 range() 함수로 각 원소에 접근하기 위한 인덱스를 얻는 것입니다.

예를 들어, 알파벳 대문자 첫 6개를 담은 리스트를 대상으로 루프를 돌려볼께요.

letters = ["A", "B", "C", "D", "E", "F"]

리스트에 저장된 모든 요소를 출력해보겠습니다.

for idx in range(len(letters)):
    print(letters[idx])
결과
A
B
C
D
E
F

물론 원하시다면 명시적으로 3개의 인자를 모두 넘겨도 상관은 없겠죠?

for idx in range(0, len(letters), 1):
    print(letters[idx])
결과
A
B
C
D
E
F

이번에는 리스트를 역방향으로 루프를 돌려보겠습니다.

for idx in range(len(letters) - 1, -1, -1):
    print(letters[idx])
결과
F
E
D
C
B
A

부연 설명을 드리면, range() 함수에 첫번째 인자로 리스트에 담긴 마지막 원소의 인덱스를 넘기고 있고요, 두번째 인덱스로 -1을 넘겨 0에서 종료되도록 하고 있습니다. (아직 기억하시죠? 종료 값은 범위에 포함되지 않는다 것!) 세번째 인자로 -1을 넘겨서 범위 내의 숫자가 1씩 감소되도록 하고 있습니다.

이번에는 처음 3개의 원소만 출력해볼까요?

for idx in range(3):
    print(letters[idx])
결과
A
B
C

반대로 마지막 3개의 원소만 출력해보겠습니다.

for idx in range(3, len(letters)):
    print(letters[idx])
결과
D
E
F

마지막으로 리스트로 부터 만들 수 있는 모든 문자의 쌍을 한 번 출력해볼께요.

for i in range(len(letters) - 1):
    for j in range(i, len(letters)):
        print(letters[i] + letters[j])
결과
AA
AB
AC
AD
AE
AF
BB
BC
BD
BE
BF
CC
CD
CE
CF
DD
DE
DF
EE
EF

이와 같이 range() 함수를 이용하면 여러 개의 원소를 담고 있는 자료구조를 자유롭게 순방향/역방향으로 전체/부분 순회를 할 수가 있습니다.

주의 사항

어떤가요? range() 함수 정말 강력하죠? 🦸

하지만 파이썬 커뮤니티에서는 여러 개의 원소가 담긴 자료구조를 대상으로 for 루프를 돌 때 range() 함수를 사용하는 것이 아주 좋은 코드라고 보지는 않습니다. 왜냐하면 더 파이썬답게(Pythonic) for 루프를 작성할 수 있는 방법들이 있기 때문이죠.

예를 들어, 리스트에 담긴 모든 원소를 출력하는 코드는 사실 range() 함수가 없이 아래처럼 작성할 수 있으며 이렇게 짜는 것이 오히려 더 깔끔합니다.

for letter in letters:
    print(letter)
결과
A
B
C
D
E
F

리스트를 역방향으로 루프를 돌고 싶을 때는 어떻게 하냐고요? reversed() 내장 함수를 사용하면 되죠.

for letter in reversed(letters):
    print(letter)
결과
F
E
D
C
B
A

reversed() 내장 함수에 대한 자세한 내용은 관련 포스팅을 참고 바랍니다.

루프를 돌 때 원소 뿐만 아니라 반드시 인덱스도 필요한 상황에서도 enmerate() 내장 함수를 사용하는 것이 권장됩니다.

for idx, letter in enumerate(letters):
    print(idx, letter)
결과
0 A
1 B
2 C
3 D
4 E
5 F

파이썬의 enumerate() 내장 함수로 for 루프를 돌리는 방법에 대해서는 별도 포스팅에서 자세히 다루었으니 참고 바랍니다.

효율적인 게으름

혹시 range() 함수가 지금까지 항상 for 문 안에서만 사용되는 것을 눈치채셨나요? 왜 그럴까요? range() 함수를 단독으로 사용하면 안 될까요?

사실 range() 함수에 대해서 오해하기 쉬운 부분 중 하나가 숫자가 담긴 리스트를 반환한다고 생각하는 것인데요.

사실 range() 함수는 리스트가 아니라 range라는 별도의 객체를 반환하는데요.

range(9)
결과
range(0, 9)

따라서 range() 함수로 부터 리스트를 얻으려면 list() 함수로 감싸줘야 하지요.

list(range(9))
결과
[0, 1, 2, 3, 4, 5, 6, 7, 8]

여기서 range() 함수의 중요한 특징은 범위에 내에 있는 숫자들을 필요할 때 마다 하나씩 만들어낸다는 건데요. 이러한 특징이 range() 함수와 for 반복문의 궁합을 좋게 만들어요. for 루프를 돌 때는 각 단계마다 하나의 숫자만 필요하니까요.

이러한 range() 함수의 특징을 게으르다고 볼 수도 있지만 범위가 큰 데이터를 다룰 때 큰 이점으로 작용을 하는데요. 예를 들어, range() 함수에 종료값으로 1조를 넘기더라도 범위 내의 숫자는 0부터 필요할 때 마다 하나씩 차례로 만들어집니다.

num_iter = iter(range(1_000_000_000_000))

print(next(num_iter))
print(next(num_iter))
print(next(num_iter))
결과
0
1
2

만약에 range() 함수를 사용하는 대신에 1조 크기의 범위를 리스트로 만들어서 메모리에 몽땅 올리고 같은 작업을 한다고 상상해보세요. 메모리 활용 측면에서 매우 비효율적이겠죠?

특히 for 루프를 돌다가 범위의 끝까지 도달할 필요가 없이 중간에 루프를 빠져나올 수 있는 상황이라면 더욱 그렇겠죠?

for num in range(1_000_000_000_000):
    if num == 1000:
        break

(오로지 예제를 위한 멍청한 코드입니다 😂)

고급 활용

range() 함수가 반환하는 range 객체는 마치 리스트와 같은 유연한 문법을 제공하기 때문에 활용 가능성이 무궁무진해요.

더 큰 범위로 부터 더 작은 범위를 얻고 싶다면 리스트처럼 대괄호를 뒤에 붙여서 범위를 좁힐 수 있습니다.

예를 들어, 0부터 9까지의 범위에서 처음 절반인 0부터 4까지의 범위만 얻고 싶다면

for num in range(10)[:5]:
    print(num, end=' ')
결과
0 1 2 3 4

반대로 동일한 범위에서 나중 절반인 5부터 9까지의 범위만 얻고 싶다면

for num in range(10)[5:]:
    print(num, end=' ')
결과
5 6 7 8 9

심지어 기존 범위를 역순으로 뒤짚을 수도 있지요.

for num in range(10)[::-1]:
    print(num, end=' ')
결과
9 8 7 6 5 4 3 2 1 0

위에서 설명드린 것처럼 이렇게 range 객체로 부터 새로운 range 객체를 도출하더라도 실제로 숫자를 담은 리스트가 메모리에 올라가는 것은 아니기 때문에 메모리 걱정없이 큰 범위를 상대로도 이러한 작업들을 부담없이 할 수 있습니다.

전체 코드

본 포스팅에서 제가 작성한 전체 코드는 아래에서 직접 확인하고 실행해보실 수 있습니다.

https://dales.link/kwb

마치면서

반복 작업을 위해서 루프를 도는 것은 어느 프로그래밍 언어로 코딩을 하든 빠질 수 없는 부분일텐데요. 사실 for 루프에서 range() 함수를 사용하는 것은 워낙 빈번하게 하는 작업이라서 파이썬을 오래 사용한 분들도 별 생각없이 코딩을 하기 쉬운 것 같습니다. 이 글이 습관적으로 사용하는 range() 내장 함수에 대해서 한 번 깊게 생각하게 보는 기회가 되었으면 좋겠습니다.