프로그램을 만들다 보면 수학 도구가 필요할 때가 많다. 여기까지 학습한 여러분이라면 사칙연산, 비교, 거듭제곱, 절대값, 반올림, 합계 같은 기본 연산에 익숙할 것이다. 하지만 이들은 수많은 수학 도구 가운데 일부분에 불과하다. 파이썬이 모듈이라는 도구 상자에 담아 제공하는 다양한 수학 도구들을 만나보자.

이 절에서는 여러 가지 수학 도구를 모아 둔 math 모듈, 분수를 표현하는 fraction 모듈, 난수를 만들어내는 random 모듈, 순열과 조합 함수를 가진 itertools 모듈, 통계 함수를 담은 statistics 모듈을 소개한다.

11.1.1 수학 도구 상자

math 모듈은 다양한 수학 도구를 담은 도구 상자다. 제곱근, 로그, 계승, 삼각함수 등 수학 하면 쉽게 떠올릴 수 있는 다양한 함수와 원주율과 자연상수 같은 중요한 상수가 정의되어 있다.

다음 표는 math 모듈에서 자주 사용되는 함수를 몇 가지만 꼽아 정리한 것이다.

함수 값 또는 기능
math.factorial(x) x의 계승(팩토리얼)
math.gcd(a, b) ab의 최대공약수
math.floor(x) x의 소수점 아래를 버린다
math.ceil(x) x의 소수점 아래를 올린다
math.pow(x, y) xy
math.sqrt(x) x의 제곱근
math.log(x, base) base를 밑으로 하는 x의 로그
math.sin(x) x 라디안의 사인
math.cos(x) x 라디안의 코사인
math.tan(x) x 라디안의 탄젠트
math.degrees(x) x 라디안을 도(°) 단위로 변환한다
math.radians(x) x 도(°)를 라디안 단위로 변환한다

표 11-1 math 모듈에서 자주 사용되는 함수

원주율, 자연상수처럼 직접 구하기가 까다로운 무한소수, 그리고 무한대를 표현하는 상수도 미리 정의되어 있다.

상수
math.pi 원주율 (3.141592653589793...)
math.e 자연상수 (2.718281828459045...)
math.inf 양의 무한대

표 11-2 math 모듈에 정의된 대표적인 상수

다음은 math 모듈을 임포트하고 몇몇 함수를 사용해 본 예다.

코드 11-1 math 모듈 사용하기

>>> import math         # math 모듈 임포트
>>> math.factorial(8)   # 8의 계승 (8!)
40320

>>> math.gcd(21, 28)    # 21과 28의 최대공약수
7

>>> math.floor(math.e)  # 자연상수의 소수점 아래를 버림
2

>>> math.ceil(math.e)   # 자연상수의 소수점 아래를 올림
3

>>> math.pow(3, 4)      # 3의 4승
81.0

>>> math.log(81, 3)     # 3을 밑으로 하는 81의 로그
4.0

>>> math.radians(180) == math.pi    # 라디안과 원주율의 비교
True

>>> math.degrees(math.radians(90))  # 90 도 -> 라디안 -> 도
90.0

math 모듈은 수학 지식을 얼마나 갖췄는지에 따라 쉽거나 어렵게 느낄 수 있다. 제곱근이나 원주율은 별 문제 없곘지만, 삼각함수·로그·라디안에 대해서는 잘 모를 수도 있다. 학교에서 수학을 학습하다 보면 차차 알게될 내용이고, 설령 수학을 포기했더라도 필요할 때 충분히 찾아 배울 수 있으니 너무 걱정하지 말자.

연습문제

연습문제 11-1 몫 구하기

몫을 구하는 연산자(//)는 파이썬 3.5 이상에서만 사용할 수 있다. 그보다 파이썬 버전이 낮다면 이 연산자를 사용할 수 없다. 몫 연산자 없이 몫을 구하려면 어떻게 해야 할까?

10을 6으로 나눈 몫을 구하는 프로그램을 몫 연산자를 사용하지 않고 작성해 보아라.

11.1.2 분수 표현하기

파이썬에서 실수는 기본적으로 부동소수점 수 형식의 소수로 표현된다. 따라서 실수를 계산할 때 오차가 발생할 수밖에 없다. 분수를 나타내는 fractions 모듈은 이런 오차를 피하고 싶거나 분수 연산을 활용하고 싶을 때 유용하다.

fractions 모듈에는 분수를 나타내는 fractions.Fraction 클래스가 정의되어 있다. fractions 모듈을 사용할 때는 변수에 모듈 전체 대신 Fraction 클래스만 임포트하는 편이 편리하다.

코드 11-2 fractions 모듈에서 Fraction 클래스 임포트

>>> from fractions import Fraction   # Fraction 클래스 임포트

모듈을 임포트한 후에는 Fraction 클래스를 인스턴스화하여 분수 객체를 표현할 수 있다. 분수 객체를 만들 때는 다음과 같이 분모와 분자를 초기값으로 지정해야 한다. 이 때 인수를 전달받을 numerator(분자)와 denominator(분모) 매개변수를 직접 지정할 수도 있고 생략해도 된다.

코드 11-3 분수 객체 생성하기

>>> Fraction(1, 3)   # 3분의 1
Fraction(1, 3)

>>> Fraction(numerator=2, denominator=5)   # 2분의 5
Fraction(2, 5)

분수를 서로 계산하거나, 정수·실수 등과 계산할 수도 있다. 필요에 따라 통분과 약분이 자동으로 수행되어 편리하다.

코드 11-4 분수 객체의 연산

>>> Fraction(1, 3) + Fraction(2, 3)
Fraction(1, 1)

>>> Fraction(1, 6) + Fraction(2, 3)
Fraction(5, 6)

>>> Fraction(2, 3) * 5
Fraction(10, 3)

분수의 분자를 구하려면 numerator 속성을, 분모를 구하려면 denominator 속성을 읽으면 된다. 그러나 이 속성을 직접 수정하는 것은 허용되지 않는다.

코드 11-5 분수 객체의 분자·분모 구하기

>>> one_third = Fraction(1, 3)
>>> one_third.numerator     # 분자 구하기
1

>>> one_third.denominator   # 분모 구하기
3

>>> one_third.numerator = 10  # 분자·분모 속성을 직접 수정할 수 없다
AttributeError: can't set attribute

11.1.3 난수와 추첨

난수란 정해지지 않은 임의의 수로, 프로그래밍에서는 코드를 평가할 때마다 임의의 값으로 평가되는 데이터를 가리킨다. 난수는 게임에서 매번 다른 상황을 연출할 때, 통계 시뮬레이션에서 무작위로 표본을 추출할 때, 설거지 당번을 뽑을 때 등 여러가지 용도로 활용된다. 파이썬의 난수와 관련된 기능은 random 모듈에 모여 있다.

다음 표는 random 모듈에서 자주 사용되는 함수를 몇 가지만 꼽아 정리한 것이다.

함수 값 또는 기능
random.randint(a, b) a 이상 b 이하의 임의의 정수
random.random() 0 이상 1 미만의 임의의 실수
random.choice(seq) 시퀀스에서 원소 하나를 무작위로 선택한다
random.sample(seq, k) 시퀀스에서 원소 k 개를 무작위로 선택한다
random.shuffle(seq) 시퀀스를 무작위로 섞는다
random.seed(a) 난수의 씨앗 값을 a로 설정한다

표 11-3 random 모듈에서 자주 사용되는 함수

임의의 정수나 실수를 생성할 때는 random.randint() 함수 또는 random.random() 함수를 사용한다. 임의의 값이므로, 아래의 코드를 직접 실행해보면 책과는 다른 결과가 나올 확률이 높다. random.randint() 함수의 두번째 인자 값도 난수 생성 범위에 포함된다(‘미만’이 아니라 ‘이하’다)는 점에 유의하자.

코드 11-6 임의의 정수·실수 생성하기

>>> import random          # random 모듈 임포트
>>> random.randint(0, 9)   # 0 이상 9 이하의 임의의 정수
3

>>> random.randint(0, 9)   # 0 이상 9 이하의 임의의 정수
3

>>> random.random()        # 0 이상 1 미만의 임의의 실수
0.9523992311419316

시퀀스에 보관된 데이터 중에서 일부를 임의로 뽑아야 하는 경우도 있을 것이다. random.randint() 함수로 난수를 생성한 다음 이 수를 인덱스로 하여 시퀀스의 항목을 뽑을 수도 있겠지만, random.choice() 함수와 random.sample() 함수를 사용하는 편이 더 간결하고 직관적이다. random.sample() 함수는 선택한 원소를 중복으로 선택하지 않는다는 점에 유의하자.

코드 11-7 시퀀스의 원소를 무작위로 선택하기

>>> S = ['고양이', '곰', '돼지', '여우', '담비']
>>> S[random.randint(0, len(S) - 1)]  # 이렇게 하지 마시오
'여우'

>>> random.choice(S)       # S 에서 원소 하나를 무작위로 선택
'돼지'

>>> random.sample(S, 3)    # S 에서 원소 3개를 무작위로 선택
['돼지', '여우', '고양이']

random.choice() 함수나 random.sample() 함수는 전달받은 시퀀스를 수정하지는 않는다. 시퀀스 자체를 임의의 순서로 뒤섞고 싶다면 random.shuffle() 함수를 사용한다.

코드 11-8 시퀀스의 원소를 뒤섞기

>>> S                   # S의 원소의 순서는 그대로다
['고양이', '곰', '돼지', '여우', '담비']

>>> random.shuffle(S)   # S의 원소를 무작위로 섞는다
>>> S                   # S의 원소의 순서가 바뀌었다
['곰', '돼지', '담비', '여우', '고양이']

씨앗 값 설정

시뮬레이션을 진행할 때 난수가 발생하는 순서를 통제해야 할 때가 있다. random.seed() 함수로 난수가 발생하는 씨앗 값을 설정할 수 있다. 동일한 씨앗 값을 설정하면 난수의 발생 순서가 동일하게 재현된다. 다음 예를 보면 무슨 말인지 이해할 수 있을 것이다.

코드 11-9 난수 발생 순서 재현하기

>>> random.seed(1789)      # 씨앗 값을 1789로 설정
>>> random.randint(0, 9)   # 9, 0, 7, 0 순서로 난수가 발생한다
9

>>> random.randint(0, 9)
0

>>> random.randint(0, 9)
7

>>> random.randint(0, 9)
0

>>> random.seed(1789)      # 씨앗 값을 다시 1789로 설정
>>> random.randint(0, 9)   # 앞과 동일한 순서로 난수가 발생한다
9

>>> random.randint(0, 9)
0

>>> random.randint(0, 9)
7

>>> random.randint(0, 9)
0

11.1.4 시퀀스의 순열과 조합

itertools 모듈은 시퀀스의 원소를 조합해 곱집합·순열·조합을 구하는 함수를 제공한다. 이 연산들은 확률 계산에 유용하게 사용될 수 있다. 수학 시간에 순열과 조합에 대해 배우지 않았다면 생소할 수 있다. 이 책이 수학을 설명하기 위한 책은 아니니 간단하게만 살펴보자.

함수 값 또는 기능
itertools.product(seq1, ...) 여러 시퀀스들의 곱집합
itertools.permutations(p, r) p 시퀀스의 원소 r개를 나열하는 순열
itertools.combinations(p, r) p 시퀀스의 원소 r개를 선택하는 조합
itertools.combinations_with_replacement(p, r) p 시퀀스의 원소 r개를 중복을 허용해 선택하는 조합

표 11-4 itertools 모듈의 순열·조합 관련 함수

곱집합

곱집합(Cartesian product)은 각 집합의 원소를 하나씩 뽑아 담은 튜플을 모은 집합이다. 예를 들어,

  • S1 = ['사막', '북극']
  • S2 = ['곰', '여우', '고양이']
  • S3 = S1 과 S2의 곱집합

일 때, S1S2에서 원소를 하나씩 뽑아 모은 튜플 ('북극', '곰')은 곱집합(S3)의 한 원소가 된다. itertools.product() 함수를 사용하면 여러 시퀀스의 곱집합을 구할 수 있다.

코드 11-10 두 시퀀스의 곱집합 구하기

>>> import itertools           # itertools 모듈 임포트
>>> S1 = ['사막', '북극']
>>> S2 = ['곰', '여우', '고양이']
>>> S3 = list(itertools.product(S1, S2))  # S1과 S2의 곱집합

>>> from pprint import pprint  # pprint 모듈에서 pprint 함수 임포트
>>> pprint(S3)                 # 계산된 곱집합 확인
[('사막', '곰'),
 ('사막', '여우'),
 ('사막', '고양이'),
 ('북극', '곰'),
 ('북극', '여우'),
 ('북극', '고양이')]

복잡한 컬렉션을 출력할 때 pprint.pprint() 함수를 사용하면 구조를 알아보기 편하다.

순열

순열(permutation)은 여러 개의 항목 중 특정 개수만큼을 골라 나열하는 것이다. 이 때, 나열하는 순서가 다를 경우 다른 방법으로 친다. 예를 들어, S1의 원소 두 개를 골라 나열하는 방법은 ['사막', '북극'], ['북극', '사막'] 두 가지다. itertools.permutations() 함수를 사용하면 시퀀스에서 지정한 개수의 원소를 나열하는 순열을 모두 구할 수 있다.

코드 11-11 시퀀스의 원소 두 개를 나열하는 모든 순열 구하기

>>> list(itertools.permutations(S1, 2))
[('사막', '북극'), ('북극', '사막')]

>>> pprint(list(itertools.permutations(S2, 2)))
[('곰', '여우'),
 ('곰', '고양이'),
 ('여우', '곰'),
 ('여우', '고양이'),
 ('고양이', '곰'),
 ('고양이', '여우')]

조합

조합(combination)은 여러 개의 항목 중 특정 개수만큼을 순서를 고려하지 않고 고르는 것이다. 예를 들어, S1의 원소 두 개를 조합하는 방법은 ['사막', '북극'] 한 가지밖에 없다. itertools.combination() 함수를 사용하면 시퀀스에서 지정한 개수의 원소를 고르는 조합을 모두 구할 수 있다. 또한, itertools.combinations_with_replacement() 함수를 이용해 한 번 고른 원소를 다시 고르는 조합도 구할 수 있다.

코드 11-12 시퀀스의 원소 두 개를 고르는 모든 조합 구하기

>>> list(itertools.combinations(S1, 2))
[('사막', '북극')]

>>> list(itertools.combinations(S2, 2))
[('곰', '여우'), ('곰', '고양이'), ('여우', '고양이')]

>>> pprint(list(itertools.combinations_with_replacement(S2, 2)))
[('곰', '곰'),
 ('곰', '여우'),
 ('곰', '고양이'),
 ('여우', '여우'),
 ('여우', '고양이'),
 ('고양이', '고양이')]

itertools 모듈은 이 외에도 여러 가지 반복자 생성 함수를 제공한다. 하지만 파이썬 입문자의 입장에서는 반복자를 다루는 것이 다소 부담이 될 듯하여 굳이 소개하지 않겠다.

11.1.5 통계학 도구 상자

statistics 모듈은 산술평균, 표준편차 등 통계 계산에 자주 사용되는 함수를 모아놓은 모듈으로, 파이썬 버전 3.4 이상에서 사용할 수 있다.

다음 표는 statistics 모듈에서 자주 사용되는 함수를 몇 가지만 꼽아 정리한 것이다.

함수 값 또는 기능
statistics.median(seq) 시퀀스의 중앙값
statistics.mean(seq) 시퀀스의 산술 평균
statistics.harmonic_mean(seq) 시퀀스의 조화 평균
statistics.stdev(seq) 시퀀스의 표본 표준편차
statistics.variance(seq) 시퀀스의 표본 분산

표 11-5 statistics 모듈에서 자주 사용되는 함수

평균은 데이터의 중심점을 구하는 방법이고, 분산과 표준편차는 분포도를 측정하는 방법이다. 자세한 내용은 수학 또는 통계학 관련 서적을 참고하기 바란다. 다음 예와 같이 기본적인 통계 연산은 직접 함수를 작성할 필요 없이 statistics 모듈을 활용해 수행할 수 있다.

코드 11-13 데이터 표본의 평균과 분포 계산하기

>>> import statistics                 # statistics 모듈 임포트
>>> sample = [29, 54, 3, 56, 77, 84, 60, 33, 55]

>>> statistics.median(sample)         #  중앙값
54

>>> statistics.mean(sample)           #  산술 평균
50

>>> statistics.harmonic_mean(sample)  #  조화 평균
18.197562061457383

>>> statistics.stdev(sample)          #  표본 표준편차
24.979991993593593

>>> statistics.variance(sample)       #  표본 분산
624