Python 강좌 : 제 7강 - 수학 모듈
수학 모듈
Python에서는 수학 모듈을 이용하여 C/C++ 에서 while, for 등을 이용하여 구현해야 했던 함수들이 기본적으로 구현되어 있습니다.
수학 모듈을 사용한다면, 별도의 함수를 구성하지 않아도 수학적인 계산을 쉽게 해결할 수 있습니다.
모듈(Module)은 관련된 함수와 상수를 모아 둔 코드 묶음입니다. Python은 모든 기능을 처음부터 불러오지 않고, 필요한 기능을 import로 가져와서 사용합니다. math 모듈은 Python에 기본으로 포함된 표준 라이브러리이므로 별도로 설치할 필요가 없으며, 내부는 C언어로 구현되어 있어 빠르게 동작합니다.
이번 강좌에서는 모듈을 가져오는 방법과, math 모듈의 올림·내림, 삼각 함수, 로그 함수, 정수 함수 등을 다룹니다. 각 함수가 왜 그런 결과를 내는지를 이해하면 실수 오차나 범위 오류를 만났을 때 원인을 쉽게 찾을 수 있습니다.
import math
print(math.sqrt(16))
print(math.pi)- 결과
- 4.0
3.141592653589793
상단에 import math를 사용하여 수학 모듈을 포함시킵니다. 수학 함수의 사용방법은 math.*을 이용하여 사용이 가능합니다.
import math를 실행하면 math라는 이름의 모듈 객체가 만들어지고, 모듈 안의 함수와 상수는 math.sqrt, math.pi처럼 점(.)으로 꺼내어 사용합니다. math.sqrt(16)은 제곱근 4.0을, math.pi는 원주율 π를 실수로 근사한 값을 반환합니다.
math.sqrt()는 입력이 정수여도 항상 실수(float)를 반환하므로 4가 아닌 4.0이 출력됩니다. math 모듈의 대부분의 함수는 C언어의 실수 계산을 사용하므로 결과가 실수입니다.
from math import *위와 같이 import시킬 시 수학 함수를 사용할 때 math.를 입력하지 않아도 사용이 가능합니다.
* 대신 함수를 직접 적는다면 해당 함수만 사용이 가능합니다.
- Tip :
from math import *는 모듈의 모든 이름을 가져오므로 내장 함수pow()가math.pow()로 덮어쓰이는 것처럼 기존 이름과 충돌할 수 있습니다. 일반적으로는import math또는 필요한 함수만 가져오는 방식을 권장합니다.
print(pow(2, 3))
from math import *
print(pow(2, 3))- 결과
- 8
8.0
위 Tip의 이름 충돌을 직접 확인하는 예제입니다. 처음의 pow(2, 3)은 내장 함수이므로 정수 8을 반환하지만, from math import * 이후의 pow는 math.pow로 바뀌어 실수 8.0을 반환합니다. 코드가 길어지면 어떤 pow가 사용되는지 알기 어려우므로 *로 가져오는 방식은 피하는 것이 좋습니다.
from math import sqrt, pi
print(sqrt(16))
print(pi)- 결과
- 4.0
3.141592653589793
from math import sqrt, pi처럼 쉼표(,)로 구분하여 필요한 함수와 상수만 가져올 수 있습니다.
이 방식은 math.를 매번 적지 않아도 되면서, 어떤 이름을 가져왔는지 코드 맨 위에서 확인할 수 있다는 장점이 있습니다.
다음 예제는 가져오지 않은 함수를 사용하면 오류가 발생하는 것을 보여주기 위한 예제입니다.
from math import pow
print(pow(3, 2))
print(sqrt(3))- 결과
- 9.0
Traceback (most recent call last):
…
NameError: name ‘sqrt’ is not defined
pow만 가져왔으므로 pow()는 정상 출력이 되지만, sqrt()는 포함시키지 않아 NameError가 발생합니다.
sqrt()를 사용하려면 from math import pow, sqrt처럼 함께 가져와야 합니다.
import math as m
print(m.sqrt(25))- 결과
- 5.0
import 모듈 as 별칭을 사용하면 모듈을 짧은 이름으로 사용할 수 있습니다. 이름이 긴 모듈을 자주 사용할 때 유용하며, 이후 다루는 외부 라이브러리에서도 자주 사용하는 방식입니다.
import math
try:
math.sqrt(-4)
except ValueError as err:
print(err)- 결과
- expected a nonnegative input, got -4.0
math 모듈의 함수는 실수 범위의 수학 함수이므로, 결과가 실수로 정의되지 않는 입력을 받으면 ValueError가 발생합니다. 음수의 제곱근은 실수가 아닌 허수이므로 오류가 발생하며, 오류 메시지가 기대하는 입력(0 이상)과 실제 입력값을 알려줍니다. 복소수 결과가 필요하다면 복소수용 수학 모듈인 cmath의 cmath.sqrt()를 사용합니다.
표현 함수
실수를 정수로 바꾸는 방법은 여러 가지이며, 어느 방향으로 정수를 맞추는가에 따라 결과가 달라집니다. 수직선 위에서 실수의 양옆에 있는 두 정수 중 어느 쪽을 고르는지로 생각하면 이해하기 쉽습니다.
| 연산 | 의미 |
|---|---|
| ceil(x) | 올림 |
| floor(x) | 내림 |
| trunc(x) | 절사 |
import math
print(math.ceil(3.2), math.ceil(-3.2))
print(math.floor(3.8), math.floor(-3.8))
print(math.trunc(3.8), math.trunc(-3.8))
print(type(math.ceil(3.2)))- 결과
- 4 -3
3 -4
3 -3
<class ‘int’>
ceil()은 양의 무한대 방향, floor()는 음의 무한대 방향으로 정수를 만들며, trunc()는 0 방향으로 소수점 이하를 버립니다.
음수에서는 floor()와 trunc()의 결과가 다르므로 주의합니다. 세 함수 모두 정수(int)를 반환합니다.
ceil은 천장(Ceiling), floor는 바닥(Floor)이라는 뜻으로, 실수보다 크거나 같은 정수 중 가장 작은 값과 작거나 같은 정수 중 가장 큰 값을 의미합니다. -3.2의 천장은 수직선에서 오른쪽에 있는 -3이고, -3.8의 바닥은 왼쪽에 있는 -4입니다. trunc()는 부호와 관계없이 소수 부분을 떼어내므로 항상 0에 가까운 쪽을 고릅니다.
import math
for x in [2.5, -2.5, 3.5]:
print(x, math.ceil(x), math.floor(x), math.trunc(x), round(x), int(x))
print(math.floor(-7 / 2), -7 // 2, int(-7 / 2))- 결과
- 2.5 3 2 2 2 2
-2.5 -2 -3 -2 -2 -2
3.5 4 3 3 4 3
-4 -4 -3
같은 값을 다섯 가지 방법으로 정수로 바꾸어 비교한 결과입니다. 각 줄은 x, ceil, floor, trunc, round, int 순서입니다.
int()는trunc()와 같이 0 방향으로 버립니다.round()는 제 5강에서 다룬 것처럼 중간값을 가장 가까운 짝수로 반올림하므로2.5는2,3.5는4가 됩니다.floor()는 제 4강의//와 같은 방향(음의 무한대)이므로math.floor(-7 / 2)와-7 // 2는 모두-4입니다.
음수가 나올 수 있는 계산이라면 어느 방향으로 맞춰야 하는지를 먼저 정하고 함수를 선택해야 합니다.
import math
items = 23
per_page = 10
print(math.ceil(items / per_page), -(-items // per_page))- 결과
- 3 3
ceil()이 자주 쓰이는 예로, 23개의 항목을 한 페이지에 10개씩 보여줄 때 필요한 페이지 수는 2.3을 올림한 3입니다.
-(-items // per_page)는 부호를 두 번 바꾸어 //의 내림을 올림으로 바꾸는 방법으로, 실수를 거치지 않으므로 제 4강에서 다룬 큰 정수의 정밀도 문제가 생기지 않습니다.
삼각 함수
| 연산 | 의미 |
|---|---|
| cos(x) | 코사인 |
| sin(x) | 사인 |
| tan(x) | 탄젠트 |
| acos(x) | 아크코사인 |
| asin(x) | 아크사인 |
| atan(x) | 아크탄젠트 |
| atan2(y, x) | y/x 아크탄젠트 |
- Tip :
cos(),sin(),tan()은 각도를 라디안으로 입력받으며,acos(),asin(),atan(),atan2()는 결과를 라디안으로 반환합니다.
삼각 함수는 각도와 직각삼각형의 변의 비율을 연결하는 함수입니다. 반지름이 1인 원 위에서 각도 θ만큼 회전한 점의 좌표가 (cos θ, sin θ)이며, tan θ는 sin θ / cos θ입니다. 게임에서 캐릭터를 원하는 방향으로 이동시키거나, 그래프에서 회전과 진동을 계산할 때 사용합니다.
acos(), asin(), atan()은 반대로 비율을 받아 각도를 구하는 역함수이며, 이름 앞의 a는 arc를 의미합니다.
라디안
일상에서는 원 한 바퀴를 360°로 나누는 60분법(Degree)을 사용하지만, 수학과 프로그래밍에서는 호도법(Radian)을 사용합니다.
1 라디안은 원의 반지름과 같은 길이의 호(원의 둘레 일부)가 만드는 각도입니다. 원의 둘레는 2πr이므로 한 바퀴는 반지름 2π개 분량, 즉 2π 라디안이 됩니다. 따라서 360° = 2π, 180° = π이며, 1 라디안은 약 57.3°입니다.
라디안을 사용하는 이유는 호의 길이 = 반지름 × 각도(라디안)처럼 각도와 길이가 단위 변환 없이 바로 연결되기 때문입니다. 삼각 함수의 미분과 급수 계산도 라디안을 기준으로 할 때 가장 간단해지므로, math 모듈을 포함한 대부분의 프로그래밍 언어는 라디안을 사용합니다.
import math
r = 2
theta = math.pi / 3
print(r * theta)
print(math.radians(1), math.degrees(1))- 결과
- 2.0943951023931953
0.017453292519943295 57.29577951308232
반지름이 2인 원에서 π/3(60°) 라디안만큼의 호의 길이는 2 × π/3 ≈ 2.094입니다. 각도가 라디안이므로 곱하기만 하면 됩니다.
1°는 π/180 ≈ 0.01745 라디안이고, 1 라디안은 180/π ≈ 57.3°입니다.
import math
print(math.sin(math.pi / 2))
print(math.cos(math.radians(60)))
print(math.degrees(math.atan2(1, 1)))
print(math.degrees(math.atan2(-1, -1)))
print(math.degrees(math.atan(-1 / -1)))- 결과
- 1.0
0.5000000000000001
45.0
-135.0
45.0
atan2(y, x)는 y를 먼저 입력하며, y와 x의 부호를 모두 고려하여 -π ~ π 범위의 각도를 반환합니다.
atan(-1 / -1)은 atan(1)과 같아 45°가 되지만, atan2(-1, -1)은 점 (-1, -1)이 있는 3사분면의 각도인 -135°를 반환합니다.
cos(60°)가 정확히 0.5가 아닌 이유는 π를 실수로 정확하게 표현할 수 없어 생기는 부동소수점 오차입니다.
math.sin(math.pi / 2)는 90°의 사인이므로 1.0입니다. math.radians(60)은 60°를 라디안으로 바꾸어 cos()에 전달하는데, 이때 사용하는 π가 이미 근사값이므로 결과에도 마지막 자리 오차가 남습니다.
atan()은 y / x라는 비율 하나만 받기 때문에, (1, 1)과 (-1, -1)처럼 비율이 같은 두 점을 구별할 수 없어 결과 범위가 -90° ~ 90°로 제한됩니다. atan2()는 y와 x를 따로 받아 점이 어느 사분면에 있는지 알 수 있으므로, 원점에서 점을 바라보는 방향을 정확히 구할 수 있습니다. 또한 x가 0이어도 나눗셈을 하지 않으므로 오류 없이 90°를 반환합니다.
삼각 함수의 주의사항
import math
print(math.sin(30))
print(math.sin(math.radians(30)))
print(math.sin(math.pi))
print(math.tan(math.pi / 2))- 결과
- -0.9880316240928618
0.49999999999999994
1.2246467991473532e-16
1.633123935319537e+16
가장 흔한 실수는 각도를 그대로 전달하는 것입니다. math.sin(30)은 30°가 아니라 30 라디안(약 1719°)의 사인을 계산하므로 -0.988...이 됩니다. 30°의 사인은 math.radians(30)으로 변환한 뒤 계산해야 0.5에 가까운 값이 나옵니다.
sin(π)는 수학적으로 0이지만, math.pi가 실제 π와 아주 조금 다르기 때문에 1.22e-16처럼 0에 매우 가까운 값이 나옵니다. 같은 이유로 tan(π/2)는 무한대가 아니라 매우 큰 수가 됩니다. 삼각 함수의 결과를 0이나 특정 값과 비교할 때는 == 대신 아래의 isclose()를 사용합니다.
import math
try:
math.asin(2)
except ValueError as err:
print(err)
print(math.degrees(math.asin(0.5)))
print(math.degrees(math.acos(-1)))- 결과
- expected a number in range from -1 up to 1, got 2.0
30.000000000000004
180.0
사인과 코사인의 값은 항상 -1에서 1 사이이므로, 역함수인 asin(), acos()는 이 범위를 벗어난 값을 받으면 ValueError가 발생합니다. 계산 오차로 1.0000000000000002처럼 범위를 아주 조금 넘는 값이 전달되어 오류가 발생하는 경우도 있으므로 주의합니다.
asin(0.5)는 30°, acos(-1)은 180°(π)입니다. 역함수의 결과도 라디안이므로 degrees()로 변환하여 확인합니다.
하이퍼볼릭 함수
| 연산 | 의미 |
|---|---|
| cosh(x) | 하이퍼볼릭 코사인 |
| sinh(x) | 하이퍼볼릭 사인 |
| tanh(x) | 하이퍼볼릭 탄젠트 |
| acosh(x) | 하이퍼볼릭 아크코사인 |
| asinh(x) | 하이퍼볼릭 아크사인 |
| atanh(x) | 하이퍼볼릭 아크탄젠트 |
- Tip : 하이퍼볼릭 함수는 각도가 아닌 실수를 입력받으며, 지수 함수로 정의됩니다. 예를 들어
cosh(x)는(e^x + e^-x) / 2입니다.
삼각 함수가 원(x² + y² = 1) 위의 점을 나타낸다면, 하이퍼볼릭 함수(쌍곡선 함수)는 쌍곡선(x² - y² = 1) 위의 점 (cosh t, sinh t)를 나타냅니다. sinh(x)는 (e^x - e^-x) / 2, tanh(x)는 sinh(x) / cosh(x)입니다. 양 끝을 잡고 늘어뜨린 줄의 모양(현수선)이 cosh의 그래프이며, tanh는 인공 신경망의 활성화 함수로도 사용됩니다.
import math
x = 1.5
print(math.cosh(x) ** 2 - math.sinh(x) ** 2)
print(math.cosh(x), (math.e ** x + math.e ** -x) / 2)
print(math.tanh(10), math.tanh(-10))
print(math.asinh(math.sinh(2.0)))- 결과
- 1.0000000000000009
2.352409615243247 2.352409615243247
0.9999999958776927 -0.9999999958776927
2.0
첫 줄은 쌍곡선의 식 cosh² - sinh² = 1을 확인한 것으로, 부동소수점 오차를 제외하면 1입니다. 두 번째 줄은 cosh()가 지수 함수로 정의된 식과 같은 값을 반환하는 것을 보여줍니다.
tanh(x)는 결과가 항상 -1과 1 사이이며, x가 커질수록 1에, 작아질수록 -1에 가까워집니다. 어떤 값이든 일정한 범위로 눌러 담을 수 있다는 성질 때문에 신경망에서 사용됩니다.
asinh()는 sinh()의 역함수이므로 sinh(2.0)의 결과를 넣으면 다시 2.0이 됩니다.
각도 변환
| 연산 | 의미 |
|---|---|
| degrees(x) | 라디안을 60분법(도)으로 변환 |
| radians(x) | 60분법(도)을 라디안(호도법)으로 변환 |
import math
print(math.degrees(math.pi))
print(math.radians(180))- 결과
- 180.0
3.141592653589793
degrees()와 radians()를 사용하면 π 라디안과 180°를 서로 변환할 수 있습니다.
두 함수는 180° = π라는 관계를 이용한 단순한 곱셈입니다. degrees(x)는 x × 180 / π, radians(x)는 x × π / 180을 계산합니다. 사람이 입력하거나 화면에 보여주는 각도는 60분법으로, math 함수에 전달하는 각도는 라디안으로 사용하고 그 사이에서 두 함수로 변환합니다.
import math
print(math.degrees(2 * math.pi), math.radians(90), math.pi / 2)
print(math.degrees(math.radians(45)))- 결과
- 360.0 1.5707963267948966 1.5707963267948966
45.0
2π 라디안은 360°이고, 90°는 π/2 라디안입니다. 각도를 라디안으로 바꾼 뒤 다시 60분법으로 바꾸면 원래 값으로 돌아옵니다.
논리 함수
제 4강에서 다룬 것처럼 실수 계산에는 오차가 있으므로 ==로 비교하기 어렵고, 계산 결과가 무한대나 nan이 되는 경우도 있습니다. 논리 함수는 이런 실수의 상태를 판단하여 True 또는 False를 반환합니다.
| 연산 | 의미 |
|---|---|
| isclose(x, y, rel_tol=z, abs_tol=w) | x와 y가 (z*1e+02)% 내외 또는 w 이하로 가까우면 True, 아니면 False |
| isinf(x) | x가 inf이면 True, 아니면 False |
| isfinite(x) | x가 inf, nan이면 False, 아니면 True |
| isnan(x) | x가 nan이면 True, 아니면 False |
- Tip : isclose(x, y, rel_tol=z) 에서 rel_tol=z를 미입력시 기본값은
1e-09로 계산합니다. 두 값의 차이가 5% 이내라면 z=0.05를 사용합니다.
import math
print(0.1 + 0.2 == 0.3)
print(math.isclose(0.1 + 0.2, 0.3))
print(math.isclose(100, 104, rel_tol=0.05))
print(math.isclose(1e-10, 0.0))
print(math.isclose(1e-10, 0.0, abs_tol=1e-9))
print(math.isinf(math.inf), math.isnan(math.nan), math.isfinite(1e308 * 10))- 결과
- False
True
True
False
True
True True False
rel_tol은 두 값 중 큰 값의 크기를 기준으로 한 상대 오차입니다. 그래서 0과 비교하는 경우에는 상대 오차가 의미가 없어 아주 작은 값도 False가 됩니다.
0 근처의 값을 비교할 때는 abs_tol로 허용할 절대 오차를 함께 지정합니다. (abs_tol의 기본값은 0.0)
1e308 * 10처럼 실수의 표현 범위를 넘으면 inf가 되므로 isfinite()는 False를 반환합니다.
isclose(a, b)는 내부적으로 abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)을 계산합니다. 즉, 두 값의 차이가 큰 값의 rel_tol 배 또는 abs_tol 중 큰 쪽 이하이면 같다고 판단합니다.
100과 104는 차이가 4이고, 큰 값 104의 5%는 5.2이므로 True입니다. 1e-10과 0.0은 큰 값이 1e-10이라 허용 오차가 1e-19밖에 되지 않아 False가 되지만, abs_tol=1e-9를 지정하면 차이 1e-10이 그보다 작아 True가 됩니다.
import math
a, b = 1000.0, 1000.001
print(math.isclose(a, b), math.isclose(a, b, rel_tol=1e-6))- 결과
- False True
상대 오차를 사용하는 이유는 값의 크기에 따라 허용할 오차가 달라야 하기 때문입니다. 1000.0과 1000.001은 차이가 0.001로, 기본 rel_tol=1e-09의 허용 오차(약 0.000001)보다 커서 False입니다. rel_tol=1e-6(백만분의 일)로 늘리면 허용 오차가 약 0.001이 되어 True가 됩니다. 비교할 값의 의미에 맞게 허용 오차를 정해야 합니다.
import math
print(math.inf > 1e308, -math.inf < -1e308)
print(math.inf + 1, math.inf - math.inf, math.inf * 0)
print(math.nan > 0, math.nan < 0, math.nan + 1)- 결과
- True True
inf nan nan
False False nan
inf는 어떤 실수보다 큰 값으로, inf + 1도 여전히 inf입니다. 하지만 inf - inf나 inf * 0처럼 결과를 하나로 정할 수 없는 연산은 nan이 됩니다.
nan은 어떤 값과 비교해도 False이며, nan이 포함된 계산의 결과도 nan이 됩니다. 그래서 계산 중간에 nan이 한 번 생기면 이후 결과가 모두 nan으로 번지므로, 결과를 사용하기 전에 isnan()이나 isfinite()로 확인하는 것이 좋습니다.
로그 함수
로그(Logarithm)는 거듭제곱의 역연산입니다. 2³ = 8일 때 log₂ 8 = 3이며, “밑을 몇 번 곱해야 이 값이 되는가“를 의미합니다. 값이 매우 큰 범위에 걸쳐 있는 데이터(소리의 크기, 지진의 규모 등)를 다루거나, 숫자의 자릿수를 구할 때 사용합니다.
| 연산 | 의미 |
|---|---|
| log(x, y) | y를 밑으로 하는 x 로그 |
| log10(x) | 10을 밑으로 하는 x로그 |
| log1p(x) | e를 밑으로 하는 x+1로그 |
| log2(x) | 2를 밑으로 하는 x로그 |
- Tip :
log(x, y)에서 y를 미입력 시 밑을e로 사용하여 자연로그로 이용합니다.
import math
print(math.log(math.e))
print(math.log(8, 2))
print(math.log2(8))
print(math.log10(1000))
print(math.log1p(1e-10))
print(math.log(1 + 1e-10))- 결과
- 1.0
3.0
3.0
3.0
9.999999999500001e-11
1.000000082690371e-10
log1p(x)는 log(1 + x)와 같은 값이지만, x가 매우 작을 때 1 + x를 먼저 계산하면서 생기는 오차가 없어 더 정확한 값을 반환합니다.
- Tip : 밑이 2나 10인 경우에는
log(x, 2)보다log2(x),log10(x)가 더 정확합니다.
log(math.e)는 e를 밑으로 하는 자연로그이므로 1.0입니다. log(8, 2), log2(8), log10(1000)은 모두 3.0입니다.
log1p()가 필요한 이유는 실수의 정밀도에 있습니다. 1 + 1e-10을 계산하면 결과는 1.0000000001이어야 하지만, 실수는 유효 숫자가 약 16자리뿐이므로 1 근처에서는 1e-10의 뒷자리가 반올림되어 사라집니다. 이렇게 손상된 값에 로그를 적용하면 정확한 값 9.9999999995e-11과 앞자리부터 달라진 1.00000008e-10이 나옵니다. log1p()는 1 + x를 직접 만들지 않고 계산하므로 이 문제가 없습니다.
import math
print(math.log(1000, 10))
print(math.log10(1000))
print(math.log(1000) / math.log(10))- 결과
- 2.9999999999999996
3.0
2.9999999999999996
위 Tip의 이유를 확인하는 예제입니다. log(x, y)는 내부적으로 log(x) / log(y)를 계산하므로, 두 번의 자연로그 계산과 나눗셈에서 생긴 오차가 결과에 남아 2.9999999999999996이 됩니다. 이 값을 int()로 정수로 바꾸면 2가 되어 자릿수 계산 등이 틀어질 수 있습니다. 밑이 10이나 2라면 전용 함수인 log10(), log2()를 사용합니다.
import math
try:
math.log(0)
except ValueError as err:
print(err)
n = 255
print(math.floor(math.log2(n)) + 1, n.bit_length())
print(math.floor(math.log10(12345)) + 1, len(str(12345)))- 결과
- expected a positive input
8 8
5 5
로그는 양수에서만 정의되므로 0이나 음수를 넣으면 ValueError가 발생합니다.
로그를 이용하면 자릿수를 구할 수 있습니다. log10(12345)는 약 4.09로, 내림한 뒤 1을 더하면 10진수 자릿수 5가 됩니다. 같은 방식으로 log2()를 사용하면 2진수의 자릿수, 즉 제 6강의 bit_length()와 같은 값을 얻습니다. 다만 실수 오차의 영향을 받을 수 있으므로, 정수의 자릿수는 bit_length()나 len(str(n))을 사용하는 것이 더 안전합니다.
연산 함수
| 연산 | 의미 |
|---|---|
| pow(x, y) | x의 y승 (실수 반환) |
| sqrt(x) | 루트 x |
| erf(x) | 오차함수 |
| erfc(x) | 여오차함수 |
| exp(x) | e의 x승 |
| expm1(x) | e의 x승 - 1 |
| frexp(x) | x를 (가수부, 지수부)로 반환 |
| ldexp(x, y) | x*(2^y) |
| gamma(x) | 감마함수 |
| lgamma(x) | 감마함수 절대값의 자연로그 |
| factorial(x) | 팩토리얼 |
| fsum([x, y, z, …]) | 오차를 줄인 실수의 합 |
| fmod(x, y) | x를 y로 나눈 나머지 |
| fabs(x) | 절대값 (실수 반환) |
| gcd(x, y, …) | 정수들의 최대공약수 |
| hypot(x, y, …) | 유클리드 노름을 반환 |
| modf(x) | x를 (소수부, 정수부)로 반환 |
| copysign(x, y) | y의 부호를 사용하는 x를 반환 |
- Tip : Python 3.8부터
hypot()은 여러 개의 값을, Python 3.9부터gcd()는 여러 개의 정수를 입력받을 수 있습니다.
import math
print(math.pow(3, 2), pow(3, 2), 3 ** 2)
print(math.expm1(1e-10), math.exp(1e-10) - 1)
print(math.frexp(8.0))
print(math.ldexp(0.5, 4))
print(math.factorial(5))
print(math.fmod(-7, 3), -7 % 3)
print(math.gcd(12, 18, 30))
print(math.hypot(3, 4), math.hypot(1, 2, 2))
print(math.modf(3.75))
print(math.copysign(3, -0.0))- 결과
- 9.0 9 9
1.00000000005e-10 1.000000082740371e-10
(0.5, 4)
8.0
120
-1.0 2
6
5.0 3.0
(0.75, 3.0)
-3.0
math.pow()는 항상 실수(float)를 반환하지만, 내장 함수 pow()와 **는 정수끼리 계산하면 정수를 반환합니다. 큰 정수를 정확하게 계산해야 한다면 **를 사용합니다.
expm1(x)는 exp(x) - 1과 같은 값이지만, x가 매우 작을 때 더 정확합니다.
fmod()는 C언어의 방식처럼 나누어지는 수(x)의 부호를 따르며, % 연산자는 나누는 수(y)의 부호를 따릅니다. 그래서 fmod(-7, 3)은 -1.0, -7 % 3은 2입니다.
copysign(3, -0.0)처럼 -0.0의 부호도 인식합니다.
나머지 결과도 한 줄씩 살펴보면 다음과 같습니다.
expm1(1e-10)은 앞의log1p()와 같은 원리입니다.exp(1e-10)은1.0000000001에 가까운 값이라1을 빼는 순간 앞자리가 모두 사라지고 반올림 오차만 남지만,expm1()은1을 빼는 과정을 거치지 않아 정확한1.00000000005e-10을 반환합니다.frexp(8.0)은8.0 = 0.5 × 2⁴이므로(0.5, 4)를,ldexp(0.5, 4)는 그 반대로0.5 × 2⁴ = 8.0을 반환합니다.factorial(5)는5 × 4 × 3 × 2 × 1 = 120입니다.gcd(12, 18, 30)은 세 수를 모두 나누는 가장 큰 수6입니다.hypot(3, 4)는√(3² + 4²) = 5.0,hypot(1, 2, 2)는 3차원의√(1 + 4 + 4) = 3.0입니다.modf(3.75)는 소수부0.75와 정수부3.0을 둘 다 실수로 반환합니다.
frexp()와 실수의 저장 방식
import math
print(math.frexp(10.0))
m, e = math.frexp(10.0)
print(m * 2 ** e)
print(math.ldexp(m, e))- 결과
- (0.625, 4)
10.0
10.0
제 4강에서 실수는 가수 × 2^지수 형태로 저장된다고 설명했습니다. frexp()는 실수를 바로 이 형태로 분해하여 0.5 이상 1 미만의 가수와 정수 지수를 반환합니다. 10.0 = 0.625 × 2⁴이며, ldexp()는 가수와 지수로 원래 실수를 다시 조립합니다. 실수의 내부 구조를 직접 다루는 저수준 계산에서 사용합니다.
감마 함수와 팩토리얼
import math
print(math.gamma(5), math.factorial(4))
print(math.gamma(0.5) ** 2, math.pi)
print(math.lgamma(100), math.log(math.factorial(99)))
try:
math.factorial(5.0)
except TypeError as err:
print(err)- 결과
- 24.0 24
3.1415926535897927 3.141592653589793
359.1342053695754 359.1342053695754
‘float’ object cannot be interpreted as an integer
감마 함수는 팩토리얼을 실수로 확장한 함수로, 양의 정수 n에 대해 gamma(n) = (n-1)!입니다. 그래서 gamma(5)는 4! = 24와 같으며, 실수 계산이므로 24.0으로 반환됩니다. gamma(0.5)처럼 정수가 아닌 값도 계산할 수 있으며, 그 제곱은 π가 됩니다.
lgamma(x)는 감마 함수의 결과에 로그를 취한 값을 반환합니다. 99!는 156자리의 매우 큰 수라 실수로 표현하면 범위를 넘을 수 있지만, 로그를 취하면 359.13...처럼 작은 값이 되므로 확률 계산처럼 큰 팩토리얼끼리 곱하고 나누는 계산에 사용합니다.
factorial()은 정수만 받습니다. 5.0처럼 값이 정수여도 자료형이 실수이면 TypeError가 발생하므로 int()로 변환하여 전달합니다.
오차 함수
erf(x)는 정규분포(종 모양의 확률 분포)와 관련된 함수로, 아래 함수 수식의 적분으로 정의됩니다. 통계에서 값이 평균으로부터 일정 범위 안에 들어갈 확률을 구할 때 사용합니다.
import math
print(math.erf(0), math.erf(1), math.erfc(1))
print(math.erf(1 / math.sqrt(2)))- 결과
- 0.0 0.8427007929497149 0.1572992070502851
0.6826894921370857
erfc(x)는 1 - erf(x)이므로 erf(1)과 erfc(1)을 더하면 1이 됩니다. erf(1 / √2)는 약 0.6827로, 정규분포에서 값이 평균 ± 1 표준편차 안에 들어갈 확률(약 68%)과 같습니다.
정확한 실수의 합
import math
total = 0.0
for value in [0.1] * 10:
total += value
print(total)
print(sum([0.1] * 10))
print(math.fsum([0.1] * 10))- 결과
- 0.9999999999999999
1.0
1.0
0.1을 반복하여 더하면 부동소수점 오차가 누적되어 0.9999999999999999가 됩니다.
fsum()은 오차를 추적하며 더하므로 정확한 합을 반환합니다.
- Tip : Python 3.12부터 내장 함수
sum()도 실수를 더할 때 오차를 보정하는 방식으로 변경되어 위 예제에서는1.0을 반환합니다. 다만 가장 정확한 합이 필요한 경우에는fsum()을 사용합니다.
덧셈을 할 때마다 결과가 실수의 유효 숫자에 맞게 반올림되므로, 작은 오차가 덧셈 횟수만큼 쌓입니다. fsum()은 각 덧셈에서 반올림으로 버려진 부분을 따로 모아 두었다가 마지막에 반영하므로, 모든 값을 정확하게 더한 결과를 실수로 반올림한 값을 반환합니다.
import math
values = [-1e16, 1.0, 1e30, 3.3, -1e30, 1e16]
print(sum(values))
print(math.fsum(values))- 결과
- 4.2998046875
4.3
sum()의 보정은 대부분의 경우 충분하지만 항상 정확한 것은 아닙니다. 위처럼 크기 차이가 매우 큰 값이 섞여 서로 상쇄되는 경우에는 sum()의 결과에 오차가 남지만, fsum()은 정확한 합 1.0 + 3.3 = 4.3을 반환합니다.
fmod()와 %의 차이
import math
print(math.fmod(7, 3), 7 % 3)
print(math.fmod(-7, 3), -7 % 3)
print(math.modf(-3.75))- 결과
- 1.0 1
-1.0 2
(-0.75, -3.0)
양수끼리는 fmod()와 %의 결과가 같지만, 음수가 섞이면 달라집니다. fmod()는 제 4강의 int()처럼 몫을 0 방향으로 버림하여 -7 = 3 × (-2) + (-1)로 계산하고, %는 몫을 음의 무한대 방향으로 내림하여 -7 = 3 × (-3) + 2로 계산합니다. C언어와 같은 결과가 필요하면 fmod(), 순환하는 범위의 값이 필요하면 %를 사용합니다.
modf()도 0 방향으로 나누므로 -3.75는 정수부 -3.0과 소수부 -0.75로 분리됩니다. 두 부분 모두 원래 값의 부호를 따릅니다.
정수 및 조합 함수
앞의 함수들이 실수 계산을 다룬다면, 다음 함수들은 정수를 정확하게 계산합니다. 결과를 실수로 바꾸지 않으므로 아무리 큰 정수도 오차 없이 계산할 수 있습니다.
| 연산 | 의미 |
|---|---|
| isqrt(n) | n의 정수 제곱근 (내림) (3.8+) |
| prod([x, y, z, …]) | 모든 원소의 곱 (3.8+) |
| dist(p, q) | 두 점 p, q 사이의 거리 (3.8+) |
| comb(n, k) | n개 중 k개를 뽑는 조합 수 (3.8+) |
| perm(n, k) | n개 중 k개를 뽑는 순열 수 (3.8+) |
| lcm(x, y, …) | 정수들의 최소공배수 (3.9+) |
import math
print(math.isqrt(17))
print(math.prod([1, 2, 3, 4]))
print(math.dist((0, 0), (3, 4)))
print(math.comb(5, 2))
print(math.perm(5, 2))
print(math.lcm(4, 6, 10))- 결과
- 4
24
5.0
10
20
60
isqrt(n)은 int(sqrt(n))과 달리 실수를 거치지 않아 매우 큰 정수에서도 정확한 정수 제곱근을 반환합니다.
dist(p, q)는 같은 차원의 두 좌표 사이의 유클리드 거리를 반환합니다.
comb(n, k)는 순서를 고려하지 않는 조합(nCk), perm(n, k)는 순서를 고려하는 순열(nPk)의 개수를 반환합니다.
결과를 한 줄씩 보면, √17 ≈ 4.12를 내림한 4, 1 × 2 × 3 × 4 = 24, 점 (0, 0)과 (3, 4) 사이의 거리 5.0, 5개 중 2개를 고르는 조합 10, 순서까지 고려한 순열 20, 4, 6, 10의 최소공배수 60입니다.
import math
n = (10 ** 17 + 1) ** 2
print(math.isqrt(n))
print(int(math.sqrt(n)))- 결과
- 100000000000000001
100000000000000000
n은 10 ** 17 + 1의 제곱이므로 정확한 제곱근은 100000000000000001입니다. isqrt()는 정수 연산만으로 이 값을 정확히 구하지만, sqrt()는 n을 먼저 실수로 바꾸면서 제 4강에서 다룬 것처럼 유효 숫자를 넘는 끝자리를 잃어 1만큼 틀린 결과를 반환합니다.
import math
print(math.comb(5, 2), math.factorial(5) // (math.factorial(2) * math.factorial(3)))
print(math.perm(5, 2), math.factorial(5) // math.factorial(3))
print(math.perm(5), math.comb(45, 6), math.comb(3, 5))- 결과
- 10 10
20 20
120 8145060 0
순열은 n개 중 k개를 순서대로 나열하는 경우의 수로 n! / (n-k)!이며, 조합은 순서를 무시하므로 같은 k개를 나열한 k!가지를 하나로 보아 n! / (k! × (n-k)!)입니다. 두 함수는 이 식과 같은 결과를 반환하지만, 큰 팩토리얼을 직접 계산하지 않으므로 더 빠릅니다.
perm(5)처럼 k를 생략하면 5!과 같은 전체 순열의 수를 반환합니다. comb(45, 6)은 45개의 번호 중 6개를 고르는 경우의 수이고, comb(3, 5)처럼 고를 개수가 전체보다 많으면 0을 반환합니다.
import math
print(math.gcd(12, 18), math.lcm(12, 18), 12 * 18 // math.gcd(12, 18))
print(math.prod([]), math.prod([1.5, 2]))
print(math.dist((1, 2, 3), (4, 6, 3)))- 결과
- 6 36 36
1 3.0
5.0
두 수의 최소공배수는 두 수의 곱 ÷ 최대공약수와 같습니다. 12 × 18 = 216을 6으로 나누면 36입니다.
prod()는 빈 목록에 대해 곱셈의 항등원인 1을 반환하며, 실수가 섞이면 결과도 실수입니다. dist()는 3차원 좌표에서도 동작하여 √(3² + 4² + 0²) = 5.0을 반환합니다.
추가된 실수 함수
| 연산 | 의미 |
|---|---|
| cbrt(x) | 세제곱근 (3.11+) |
| exp2(x) | 2의 x승 (3.11+) |
| sumprod(p, q) | 두 목록의 원소별 곱의 합 (3.12+) |
| fma(x, y, z) | x * y + z를 한 번의 반올림으로 계산 (3.13+) |
import math
print(math.cbrt(27))
print(math.exp2(10))
print(math.sumprod([1, 2, 3], [4, 5, 6]))
print(math.fma(2.0, 3.0, 4.0))- 결과
- 3.0
1024.0
32
10.0
sumprod(p, q)는 1*4 + 2*5 + 3*6처럼 두 목록의 내적을 계산합니다.
fma(x, y, z)는 x * y + z를 계산하되, 곱셈 결과를 중간에 반올림하지 않아 오차가 더 적은 결과를 반환합니다.
cbrt(27)은 3 × 3 × 3 = 27이므로 3.0, exp2(10)은 2¹⁰ = 1024.0입니다. sumprod()는 정수만 입력하면 정수 32를 반환합니다.
import math
print(math.cbrt(-27), (-27) ** (1/3))
print(0.1 * 10.0 - 1.0, math.fma(0.1, 10.0, -1.0))
try:
math.sumprod([1, 2], [1, 2, 3])
except ValueError as err:
print(err)- 결과
- -3.0 (1.5000000000000004+2.598076211353316j)
0.0 5.551115123125783e-17
Inputs are not the same length
음수의 세제곱근을 ** (1/3)로 구하면 1/3이 정확한 1/3이 아닌 실수이므로, Python은 복소수 범위에서 계산하여 복소수를 반환합니다. cbrt()는 세제곱근을 직접 계산하므로 -27의 실수 세제곱근 -3.0을 반환합니다.
0.1 * 10.0 - 1.0은 곱셈 결과가 먼저 1.0으로 반올림된 뒤 1.0을 빼므로 0.0이 됩니다. fma()는 곱셈 결과를 반올림하지 않고 뺄셈까지 마친 뒤 한 번만 반올림하므로, 실수 0.1에 원래 포함되어 있던 오차 5.55e-17이 그대로 드러납니다. 즉, fma()의 결과가 실제 저장된 값으로 계산한 정확한 결과입니다.
sumprod()는 두 목록의 길이가 다르면 ValueError가 발생합니다.
상수
| 연산 | 의미 |
|---|---|
| e | e |
| pi | π |
| tau | τ (2π) |
| inf | ∞ |
| nan | Not a Number |
import math
print(math.e)
print(math.pi)
print(math.tau)
print(math.inf)
print(math.nan == math.nan)- 결과
- 2.718281828459045
3.141592653589793
6.283185307179586
inf
False
nan은 자기 자신과도 같지 않으므로 ==로 비교할 수 없으며, 반드시 isnan()으로 확인합니다.
e는 자연로그의 밑(약 2.718), pi는 원주율, tau는 원 한 바퀴에 해당하는 2π 라디안입니다. 모두 실제 값에 가장 가까운 실수로 저장된 근사값입니다.
inf는 float("inf"), nan은 float("nan")과 같은 값이며, 계산 결과를 초기화하거나(예: 최솟값을 찾을 때 처음 값을 inf로 설정) 값이 없음을 표시할 때 사용합니다. nan이 자기 자신과도 같지 않은 것은 IEEE 754 표준의 규칙으로, “정의되지 않은 값”끼리는 같다고 판단할 근거가 없기 때문입니다.
함수 수식
erf(x) : 오차함수
$$ erf(x) = {2\over \sqrt\pi}\int_0^x e^{-t^2} dt $$
erfc(x) : 여오차함수
$$ erfc(x) = 1-erf(x) = {2\over \sqrt\pi}\int_x^\infty e^{-t^2} dt $$
frexp(x) : (가수부, 지수부) 반환
$$ frexp(x) = {가수부} \times 2^{지수부} $$
gamma(x) : 감마함수
$$ gamma(x) = \int_0^\infty {t^{x-1}} {e^{-t}} dt $$
hypot(x, y) : 유클리드 노름
$$ hypot(x, y) = {\sqrt{x^2 + y^2}} $$
정리
| 이름 | 설명 | 반환값/특징 |
|---|---|---|
| import math | 모듈 가져오기 | math.함수로 사용 |
| from math import 이름 | 필요한 이름만 가져오기 | *는 이름 충돌 위험 |
| ceil, floor, trunc | 올림, 내림, 0 방향 버림 | int 반환 |
| sin, cos, tan | 삼각 함수 | 입력은 라디안 |
| atan2(y, x) | 점의 방향 각도 | 사분면 구분, -π ~ π |
| degrees, radians | 각도 단위 변환 | 180° = π |
| isclose | 실수 근사 비교 | rel_tol, abs_tol |
| log, log2, log10 | 로그 | 양수만, 밑이 2·10이면 전용 함수 |
| log1p, expm1 | 0 근처의 정확한 계산 | 작은 값의 정밀도 유지 |
| fsum | 정확한 실수의 합 | 버려진 오차를 추적 |
| fmod | C 방식의 나머지 | 나누어지는 수의 부호 |
| isqrt, comb, perm | 정수 계산 | 실수를 거치지 않아 정확 |
| fma | x * y + z | 반올림 한 번 (3.13+) |
| e, pi, tau, inf, nan | 상수 | nan은 isnan()으로 확인 |
math 모듈의 함수 대부분은 실수(float)로 계산하므로 제 4강에서 다룬 부동소수점 오차가 그대로 나타납니다. 결과를 비교할 때는 isclose()를 사용하고, 정확한 정수 결과가 필요하면 isqrt(), comb()처럼 정수 전용 함수를 사용합니다. 삼각 함수에는 항상 라디안을 전달한다는 점을 기억합니다.
댓글 남기기