상위 목록: 하위 목록: 작성 날짜: 읽는 데 11 분 소요

난수 모듈

Python에서는 난수 모듈(random)을 이용하여 특정한 순서나 규칙을 가지지 않은 무작위의 숫자를 발생시킬 수 있습니다.

임의의 숫자나 확률이 필요한 알고리즘이나 코드에 사용합니다.

주사위 게임, 카드 섞기, 데이터에서 표본 뽑기, 무작위 테스트 데이터 만들기, 확률 시뮬레이션 등이 대표적인 예입니다.


그런데 컴퓨터는 같은 입력에 대해 항상 같은 결과를 내도록 만들어진 결정적(deterministic) 기계입니다. 그러므로 순수한 계산만으로는 진짜 무작위 값을 만들 수 없습니다.

그래서 난수 모듈은 정해진 계산 규칙으로 숫자를 만들되, 그 결과가 사람이 보기에 규칙이 없는 것처럼 고르게 흩어지도록 설계된 의사 난수(Pseudo-random)를 생성합니다.

이번 강좌에서는 난수 모듈의 주요 함수와 함께, 의사 난수가 만들어지는 원리와 시드(seed)의 의미, 같은 난수를 다시 만들어 내는 방법, 그리고 보안이 필요할 때 사용하는 secrets 모듈을 알아봅니다.


import random

상단에 import random를 사용하여 난수 모듈을 포함시킵니다.

난수 함수의 사용방법은 random.*을 이용하여 사용이 가능합니다.


import random

print(random.random())
print(random.uniform(3.5, 3.6))
print(random.randrange(10))
print(random.randrange(3, 7))
print(random.randint(5, 9))
결과
0.7126416977536714
3.5199133430983447
7
6
8

random.*을 이용하여 무작위의 숫자를 발생시킵니다.

난수이므로 실행할 때마다 결괏값이 달라집니다.

특정 범위를 갖는 무작위 값을 반환하거나, 특정 분포의 형태를 갖는 값을 생성할 수도 있습니다.

random()은 0.0 이상 1.0 미만의 실수를 고르게(균등 분포) 반환하는 가장 기본적인 함수입니다. 균등 분포란 범위 안의 어떤 값이든 나올 가능성이 같다는 의미입니다.

uniform(3.5, 3.6)은 3.5와 3.6 사이의 실수를, randrange(10)은 0부터 9까지의 정수 중 하나를, randrange(3, 7)은 3부터 6까지의 정수 중 하나를, randint(5, 9)는 5부터 9까지의 정수 중 하나를 반환합니다.


함수 의미 반환 형식
random() 0.0 <= x < 1.0 실수형
uniform(a, b) a <= x <= b 실수형
randrange(a) 0 <= x < a 정수형
randrange(a, b) a <= x < b 정수형
randrange(a, b, step) a <= x < b 범위에서 step 간격의 값 정수형
randint(a, b) a <= x <= b 정수형
  • Tip : uniform(a, b)는 부동 소수점 반올림에 따라 b가 포함될 수 있습니다.

  • Tip : randrange는 정수만 인자로 사용할 수 있습니다. Python 3.12 이상에서는 randrange(10.0)과 같이 실수를 전달하면 TypeError가 발생합니다.


import random

random.seed(1)
print(sorted(set(random.randint(1, 3) for _ in range(100))))
print(sorted(set(random.randrange(1, 3) for _ in range(100))))
결과
[1, 2, 3]
[1, 2]

randint와 randrange는 끝값 포함 여부가 다르므로 가장 자주 혼동하는 함수입니다.

각 함수를 100번씩 호출해 나온 값의 종류를 집합으로 모아 보면, randint(1, 3)은 3이 포함되어 1, 2, 3이 나오지만, randrange(1, 3)은 range(1, 3)처럼 3이 제외되어 1, 2만 나옵니다.

randrange는 range 함수와 같은 규칙을 따르므로, range와 함께 생각하면 기억하기 쉽습니다. 예를 들어 리스트의 색인을 무작위로 고를 때는 randrange(len(L))을 사용합니다.



의사 난수의 원리

의사 난수 생성기는 내부에 상태(state)라 부르는 숫자를 가지고 있습니다. 난수를 요청할 때마다 다음 두 단계를 반복합니다.

  1. 정해진 계산식으로 상태를 다음 상태로 바꿉니다.
  2. 바뀐 상태로부터 반환할 값을 계산합니다.

계산식이 정해져 있으므로, 처음 상태가 같으면 이후에 나오는 모든 값도 같습니다. 이 처음 상태를 정하는 값이 시드(seed)입니다.

def lcg(seed, n):
    result = []
    state = seed

    for _ in range(n):
        state = (1103515245 * state + 12345) % 2 ** 31
        result.append(state % 100)

    return result


print(lcg(1, 5))
print(lcg(1, 5))
print(lcg(2, 5))
결과
[90, 75, 84, 81, 74]
[90, 75, 84, 81, 74]
[87, 92, 93, 38, 27]

위 예제는 원리를 설명하기 위해 직접 만든 아주 단순한 의사 난수 생성기입니다. 선형 합동 생성기(Linear Congruential Generator)라 부르는 오래된 방식입니다.

state = (1103515245 * state + 12345) % 2 ** 31이 상태를 바꾸는 계산식이고, state % 100이 상태로부터 0~99 사이의 반환값을 만드는 계산식입니다.

결과를 보면 90, 75, 84, 81, 74에는 눈에 띄는 규칙이 없어 보이지만, 시드가 1로 같은 첫 번째와 두 번째 호출은 완전히 같은 수열을 만듭니다. 시드를 2로 바꾸면 전혀 다른 수열이 나옵니다.

즉, 의사 난수는 “시드가 정해지면 미리 정해져 있는 긴 수열”을 처음부터 하나씩 꺼내는 것과 같습니다. 무작위처럼 보이는 것은 계산식이 수열을 충분히 뒤섞기 때문입니다.


상태가 가질 수 있는 값은 유한하므로, 계속 꺼내다 보면 언젠가 처음 상태로 돌아와 같은 수열이 반복됩니다. 이 반복되기까지의 길이를 주기(period)라 합니다.

Python의 난수 모듈은 이 예제보다 훨씬 정교한 메르센 트위스터(Mersenne Twister) 알고리즘을 사용합니다. 상태로 32비트 정수 624개를 사용하며, 주기는 2**19937 - 1(약 6,000자리의 수)로 실제 사용에서 반복을 걱정할 필요가 없을 만큼 깁니다.


import random

random.seed(0)
state = random.getstate()

print(type(state), len(state))
print(state[0], len(state[1]))
결과
<class ‘tuple’> 3
3 625

random.getstate()로 생성기의 현재 상태를 확인할 수 있습니다.

상태는 3개의 값으로 이루어진 Tuple이며, 두 번째 값이 실제 상태입니다. 길이 625는 메르센 트위스터의 상태인 정수 624개와, 그중 어디까지 사용했는지를 나타내는 위치 값 1개입니다.

random.random(), randint(), choice() 등 모듈의 모든 함수는 이 하나의 상태를 공유하며, 함수를 호출할 때마다 상태가 앞으로 진행합니다.



시드(seed)와 재현성

import random

random.seed(42)
print(random.random(), random.randint(1, 100))

random.seed(42)
print(random.random(), random.randint(1, 100))

random.seed("python")
print(random.random())
결과
0.6394267984578837 4
0.6394267984578837 4
0.8397936179230218

random.seed(42)로 상태를 초기화한 뒤 꺼낸 값과, 다시 random.seed(42)로 초기화한 뒤 꺼낸 값이 완전히 같습니다.

이 결과는 언제, 어느 컴퓨터에서 실행해도 같은 값이 나옵니다. 시드가 같으면 상태가 같고, 상태가 같으면 이후의 계산 결과도 같기 때문입니다.

시드에는 숫자뿐만 아니라 "python" 같은 문자열도 사용할 수 있으며, 문자열은 내부에서 숫자로 변환되어 상태를 초기화합니다.


이렇게 같은 실행 결과를 다시 얻을 수 있는 성질을 재현성(reproducibility)이라 합니다.

무작위 데이터로 프로그램을 시험하다 오류가 발생했을 때, 시드를 고정해 두면 같은 데이터를 다시 만들어 오류를 재현하고 고쳤는지 확인할 수 있습니다. 시뮬레이션이나 머신러닝 실험의 결과를 다른 사람이 똑같이 확인할 수 있게 할 때도 시드를 고정합니다.

  • Tip : 공식 문서에서는 같은 시드에 대해 random()이 이후 버전에서도 같은 수열을 만들도록 보장합니다. 단, randint, choice, shuffle 등 다른 함수의 내부 알고리즘은 버전에 따라 바뀔 수 있으므로, 버전이 다르면 같은 시드라도 결과가 달라질 수 있습니다.


import random

for _ in range(3):
    random.seed(1)
    print(random.randint(1, 100))
결과
18
18
18

시드를 사용할 때 가장 흔한 실수입니다. 반복문 안에서 매번 같은 시드를 설정하면, 매번 상태가 처음으로 되돌아가므로 항상 같은 값만 나옵니다.

시드는 프로그램 시작 부분에서 한 번만 설정하고, 이후에는 상태가 자연스럽게 진행하도록 둡니다.



목록을 사용하는 난수 함수

import random

L = [1, 10, 100, 1000]

print(random.choice(L))
print(random.sample(L, 2))
random.shuffle(L)
print(L)
결과
10
[1, 1000]
[100, 10, 1000, 1]

random.*을 이용하여 목록(List)의 값을 추출하거나 변경할 수 있습니다.

choice는 목록에서 원소 하나를, sample은 서로 다른 위치의 원소 여러 개를 골라 새 목록으로 반환하며, shuffle은 목록의 순서를 무작위로 바꿉니다.

이 함수들도 내부에서는 앞에서 살펴본 생성기로 무작위 색인을 만들어 원소를 고르는 방식으로 동작합니다.


함수 의미 반환 형식
choice(L) 임의의 원소값 하나를 반환 원소의 데이터 형식
sample(L, n) 중복 없이 임의의 원소값 n개를 반환 목록
shuffle(L) 목록을 무작위로 재배열 None (원본 목록을 변경)


shuffle 함수는 새로운 목록을 반환하지 않고, 원본 목록 자체를 변경합니다.

그러므로, print(random.shuffle(L))과 같이 사용하면 None이 출력됩니다.

  • Tip : sample 함수는 시퀀스(목록, 튜플, 문자열, range 등)만 지원합니다. Python 3.11 이상에서 집합(set)을 전달하면 TypeError가 발생하므로, sorted(집합)처럼 시퀀스로 변환해 사용합니다.


import random

random.seed(3)
L = [1, 2, 3, 4, 5]

print(random.sample(L, k=len(L)))
print(L)

try:
    random.sample(L, k=6)
except ValueError as e:
    print(e)
결과
[2, 5, 4, 3, 1]
[1, 2, 3, 4, 5]
Sample larger than population or is negative

원본 목록을 유지한 채 섞인 목록이 필요하다면 sample(L, k=len(L))을 사용합니다. 모든 원소를 중복 없이 뽑으므로 섞인 새 목록이 반환되며, 두 번째 줄처럼 원본 L은 그대로입니다.

sample은 중복 없이 뽑으므로, 원소 개수보다 많이 뽑으려 하면 ValueError가 발생합니다.

  • Tip : random.choice([])처럼 빈 목록에서 고르려 하면 IndexError: Cannot choose from an empty sequence가 발생합니다.


import random

L = ["A", "B", "C"]

print(random.choices(L, k=5))
print(random.choices(L, weights=[8, 1, 1], k=5))
print(random.sample(L, k=4, counts=[2, 1, 1]))
print(random.randrange(0, 100, 10))
print(random.shuffle(L))
결과
[‘A’, ‘C’, ‘B’, ‘C’, ‘B’]
[‘A’, ‘A’, ‘A’, ‘A’, ‘B’]
[‘A’, ‘A’, ‘C’, ‘B’]
70
None

random.choices(L, k=개수)는 중복을 허용하여 임의의 원소값 k개를 목록으로 반환합니다.

weights 매개변수로 각 원소가 선택될 가중치를 설정할 수 있습니다.

예제는 A의 가중치가 8, B와 C의 가중치가 1이므로, A가 선택될 확률이 80%가 됩니다.

가중치는 비율로 해석되므로 [8, 1, 1]과 [0.8, 0.1, 0.1]은 같은 의미이며, 합이 1이나 100이 될 필요는 없습니다.


random.sample(L, k=개수, counts=반복 횟수)는 각 원소가 counts의 값만큼 반복된 목록에서 중복 없이 추출합니다. (Python 3.9 이상)

예제는 ["A", "A", "B", "C"]에서 4개를 추출한 것과 같습니다.

choices는 뽑은 원소를 다시 넣고 뽑는 방식(복원 추출)이고, sample은 뽑은 원소를 빼고 뽑는 방식(비복원 추출)입니다. 주사위를 여러 번 던지는 상황에는 choices, 카드를 여러 장 뽑는 상황에는 sample이 맞습니다.



분포와 바이트 난수

import random

print(random.gauss(0, 1))
print(random.normalvariate(mu=170, sigma=5))
print(random.binomialvariate(n=10, p=0.5))
print(random.randbytes(4))
결과
-0.6576018537632183
170.88240386021033
5
b’\xd6}\x9c\xb0’

난수 모듈은 균등 분포 외에도 다양한 확률 분포를 따르는 난수를 생성할 수 있습니다.

정규 분포는 평균 근처의 값이 가장 많이 나오고, 평균에서 멀어질수록 드물게 나오는 종 모양의 분포입니다. 키, 시험 점수, 측정 오차처럼 자연스러운 데이터를 흉내 낼 때 사용합니다.


함수 의미 반환 형식
gauss(mu, sigma) 평균 mu, 표준 편차 sigma인 정규 분포 실수형
normalvariate(mu, sigma) 평균 mu, 표준 편차 sigma인 정규 분포 실수형
binomialvariate(n, p) 성공 확률이 p인 시행을 n번 했을 때의 성공 횟수 (이항 분포) 정수형
randbytes(n) n 바이트 길이의 무작위 바이트 bytes


random.binomialvariate(n, p)는 Python 3.12 이상에서 사용할 수 있습니다.

random.randbytes(n)는 Python 3.9 이상에서 사용할 수 있습니다.


import random
import statistics

random.seed(10)
data = [random.gauss(170, 5) for _ in range(10000)]

print(round(statistics.mean(data), 2))
print(round(statistics.stdev(data), 2))
결과
169.95
4.97

평균 170, 표준 편차 5인 정규 분포에서 값을 10000개 뽑은 뒤, statistics 모듈로 평균과 표준 편차를 계산했습니다.

뽑은 값의 평균은 169.95, 표준 편차는 4.97로 지정한 값에 매우 가깝습니다. 값 하나하나는 무작위이지만, 많이 모으면 지정한 분포의 성질이 드러납니다.


python -m random -c 가위 바위 보
python -m random -i 6
python -m random 10.0
결과
가위
4
8.96490544351099

Python 3.13 이상에서는 명령 줄에서 python -m random으로 난수를 바로 생성할 수 있습니다.

-c는 입력한 값 중 하나를 선택하며, -i N은 1 이상 N 이하의 정수를, 실수를 입력하면 0 이상 해당 값 이하의 실수를 반환합니다.



난수 상태 설정

import random

random.seed(0)

state = random.getstate()
print(random.sample(range(10), k=5))
print(random.sample(range(10), k=5))
random.setstate(state)
print(random.sample(range(10), k=5))
결과
[6, 9, 0, 2, 4]
[7, 6, 4, 3, 2]
[6, 9, 0, 2, 4]

난수 모듈은 시드값(seed)을 기준으로 정해진 규칙에 따라 숫자를 생성하는 의사 난수(Pseudo-random) 생성기입니다.

시드값을 설정하지 않으면 운영체제가 제공하는 무작위 값(사용할 수 없다면 현재 시스템 시간)으로 초기화되므로, 실행할 때마다 결괏값이 달라지게 됩니다.

하지만, 시드값을 동일하게 맞춘다면 항상 같은 순서의 난수를 생성하게 됩니다.

시드값 설정 함수(random.seed)로 기준값을 설정할 수 있습니다.

random.seed(시드값)을 통해 동일한 난수를 발생시킬 수 있습니다.

만약, 시드값에 None을 입력하거나 작성하지 않을 경우 위와 같이 운영체제의 무작위 값 또는 현재 시간으로 설정됩니다.

  • Tip : 시드값으로는 None, int, float, str, bytes, bytearray만 사용할 수 있습니다. (Python 3.11 이상)


시드값은 프로그램 단위로 적용되며, 코드의 줄 단위로는 일련의 규칙을 따라가게 됩니다.

즉, 첫 번째, 두 번째, … N 번째의 전체 패턴은 같아지지만, 첫 번째와 두 번째의 값은 서로 다르게 됩니다.

첫 번째와 두 번째 또는 임의의 난수 패턴을 동일하게 맞추려면, 난수 생성 상태를 저장하고 불러오는 방법이 있습니다.

난수 상태 가져오기 함수(random.getstate)를 통해 난수 생성기의 현재 상태를 가져옵니다.

난수 상태 적용하기 함수(random.setstate)로 저장한 난수 상태를 적용할 수 있습니다.

난수 상태를 가져온 뒤, 특정 구문에 적용한다면 언제 실행하더라도 항상 동일한 결과를 얻을 수 있습니다.


예제의 결과를 순서대로 보면, 시드 0으로 초기화한 직후의 상태를 state에 저장한 뒤 두 번 추출하여 서로 다른 [6, 9, 0, 2, 4]와 [7, 6, 4, 3, 2]를 얻었습니다.

이후 setstate(state)로 상태를 저장했던 시점으로 되돌렸으므로, 세 번째 추출은 첫 번째와 같은 [6, 9, 0, 2, 4]가 됩니다.

seed()는 상태를 시드로부터 새로 만드는 함수이고, setstate()는 저장해 둔 임의 시점의 상태로 되돌리는 함수라는 차이가 있습니다. 프로그램 실행 도중의 특정 지점부터 다시 재현하고 싶을 때 getstate()와 setstate()를 사용합니다.


import random

r1 = random.Random(7)
r2 = random.Random(7)

print(r1.randint(1, 100), r1.randint(1, 100))
print(r2.randint(1, 100), r2.randint(1, 100))
결과
42 20
42 20

random.random()처럼 모듈의 함수를 직접 호출하면 프로그램 전체가 하나의 상태를 공유합니다. 그러므로 코드의 다른 부분에서 난수를 하나 더 꺼내면, 시드를 고정했더라도 이후의 값이 모두 밀려 달라집니다.

random.Random(시드)로 독립적인 생성기 객체를 만들면, 각 객체가 자신만의 상태를 가집니다. 객체는 randint, choice, shuffle 등 모듈과 같은 메서드를 제공합니다.

예제의 r1과 r2는 같은 시드 7로 만들어졌으므로 같은 수열을 만들며, 서로의 상태에 영향을 주지 않습니다. 게임의 맵 생성과 아이템 드롭처럼 용도별로 난수를 분리하고 싶을 때 사용합니다.



실용 예제 : 주사위 시뮬레이션

import random
from collections import Counter

random.seed(2026)
rolls = [random.randint(1, 6) for _ in range(6000)]

print(sorted(Counter(rolls).items()))
print(sum(rolls) / len(rolls))
결과
[(1, 1020), (2, 974), (3, 1067), (4, 1011), (5, 964), (6, 964)]
3.4695

주사위를 6000번 던져 각 눈이 나온 횟수와 평균을 계산했습니다.

이론적으로 각 눈은 1/6 확률이므로 약 1000번씩 나와야 하며, 결과도 964~1067번으로 1000 근처에 고르게 분포합니다. 정확히 1000번씩이 아닌 이유는 실제 무작위 실험에서도 생기는 자연스러운 오차입니다.

평균 3.4695도 이론적인 기댓값 (1 + 2 + 3 + 4 + 5 + 6) / 6 = 3.5에 가깝습니다.

시드를 2026으로 고정했으므로, 이 예제를 실행하면 Python 3.14.6에서 항상 위와 같은 결과를 얻습니다. 시드 줄을 지우면 실행할 때마다 횟수가 달라지지만, 모두 1000 근처라는 성질은 유지됩니다.



보안용 난수(secrets)

import secrets

print(secrets.randbelow(10))
print(secrets.choice(["rock", "paper", "scissors"]))
print(secrets.token_hex(8))
print(secrets.token_urlsafe(8))
결과
6
paper
7574050cbba61073
5kBRFMnjbyY

random 모듈은 시드값을 알면 결과를 예측할 수 있으므로, 비밀번호, 인증 토큰, 보안 키와 같은 보안 용도에는 사용하면 안 됩니다.

앞에서 살펴본 것처럼 의사 난수는 상태와 계산식만 알면 이후의 모든 값이 정해지는 구조입니다. 메르센 트위스터의 계산식은 공개되어 있으므로, 공격자가 시드를 추측하거나 출력값을 충분히 관찰해 상태를 알아내면 다음에 나올 값을 계산할 수 있습니다.

보안이 필요한 경우 운영체제의 안전한 난수 생성기를 사용하는 secrets 모듈을 사용합니다.

운영체제의 난수 생성기는 하드웨어 동작 시점 등 예측하기 어려운 정보를 모아 난수를 만들고, 출력값으로부터 다음 값을 추측할 수 없도록 설계되어 있습니다. 대신 시드를 지정할 수 없으므로 재현이 불가능합니다.


함수 의미 반환 형식
randbelow(n) 0 <= x < n 정수형
choice(L) 임의의 원소값 하나를 반환 원소의 데이터 형식
token_hex(n) n 바이트 난수를 16진수 문자열로 반환 문자열
token_urlsafe(n) n 바이트 난수를 URL에 사용 가능한 문자열로 반환 문자열


token_hex(8)은 8바이트를 16진수로 표현하므로 바이트당 2글자씩 16글자의 문자열이 됩니다. token_urlsafe(8)은 같은 8바이트를 영문자, 숫자, -, _로 표현하므로 더 짧은 문자열이 됩니다.

  • Tip : 게임, 시뮬레이션, 표본 추출처럼 재현성이 필요한 경우에는 random, 보안이 필요한 경우에는 secrets를 사용합니다.

  • Tip : random.SystemRandom() 객체는 random 모듈과 같은 메서드를 제공하면서 운영체제의 난수 생성기를 사용합니다. 이 객체는 seed()를 호출해도 무시되므로 결과를 재현할 수 없습니다.



정리

이름 설명 반환값/특징
random() 0.0 이상 1.0 미만의 실수 모든 난수 함수의 기본
randint(a, b) / randrange(a, b) 범위 안의 정수 randint는 b 포함, randrange는 b 제외
choice / choices / sample 원소 하나 / 복원 추출 / 비복원 추출 원소 / 목록 / 목록
shuffle(L) 원본 목록을 섞음 None
seed(값) 생성기의 상태를 시드로 초기화 같은 시드면 같은 수열
getstate() / setstate(상태) 현재 상태 저장 / 복원 특정 시점부터 재현
random.Random(시드) 독립적인 생성기 객체 상태를 공유하지 않음
secrets 모듈 보안용 난수 예측 불가, 재현 불가


  • 의사 난수는 상태를 정해진 계산식으로 바꾸며 값을 만들므로, 시드(처음 상태)가 같으면 결과도 같습니다.
  • 시드는 프로그램 시작 시 한 번만 설정하며, 반복문 안에서 다시 설정하지 않습니다.
  • 재현성이 필요하면 random과 시드를, 비밀번호나 토큰처럼 예측되면 안 되는 값에는 secrets를 사용합니다.

댓글 남기기