= 10 / 6 * 3
five = (7 - five) * 2
four
print(five, int(five))
print(four, int(four))
5.0 5
4.0 4
Yunho Kee
May 26, 2024
August 26, 2024
정수 아닌 실수 연산은 오차 관리가 더 힘들다. 십진수를 쓰면 문제가 없을까?
저장 용량에 따른 자릿수를 초과하면 이진법이나 십진법이나 오차가 있다.
정수 아닌 실수도 마찬가지인데 소수점 이하 자릿수가 무한일 수 있다: 무한소수.
from decimal import Decimal, ROUND_DOWN
five = Decimal('10') / Decimal('6') * Decimal('3')
four = (Decimal('7') - five) * Decimal('2')
print(five, int(five), five.quantize(Decimal('0'), ROUND_DOWN))
print(four, int(four), four.quantize(Decimal('0'), ROUND_DOWN))
5.000000000000000000000000001 5 5
3.999999999999999999999999998 3 3
소수점 이하 무한한 자릿수가 모두 필요할 일은 거의 없다. 가령 소수점 이하 n
번째 자리까지 필요하다면 그보다 작은 수를 가령 n + 1
번째 자리에 1
을 더하거나 빼 주면 된다. 더할 때는 절사, 절하, 내림, ROUND_DOWN
이나 ROUND_HALF_UP
이 필요할 때이다. 뺄 때는 절상, 올림, ROUND_UP
이나 ROUND_HALF_DOWN
이 요구될 때이다.
_EPSILON = 1e-2
five = 10 / 6 * 3
four = (7 - five) * 2 + _EPSILON
five_epsilon = five + _EPSILON
print(five_epsilon, int(five_epsilon))
print(four, int(four))
5.01 5
4.01 4
from decimal import Decimal, ROUND_DOWN
_EPSILON = Decimal('1e-2')
five = Decimal('10') / Decimal('6') * Decimal('3')
four = (Decimal('7') - five) * Decimal('2') + _EPSILON
five_epsilon = five + _EPSILON
print(five_epsilon, int(five_epsilon), five_epsilon.quantize(Decimal('0'), ROUND_DOWN))
print(four, int(four), four.quantize(Decimal('0'), ROUND_DOWN))
5.010000000000000000000000001 5 5
4.009999999999999999999999998 4 4
Java의 BigDecimal
은 오차를 지역적으로 관리한다. 오차 관리가 필요할 때 Runtime에 ArithmeticException
을 발생시킨다. 이처럼 반강제적인 예외 처리는 Python에서 발생할 오차를 일부 예방한다. 가령 연산 과정마다 주의하며 RoundingMode
그리고 scale
이나 precision
등을 일일이 명시해야 지역적 오차로 인한 비정상 종료가 방지된다.
하지만 일부 오차는 지역적 결과에 Greedy하게 의존하는 관리가 불가능하다. 가령 다음 예시의 printNaiveBigDecimals
에서 5와 4를 동시에 얻을 수 없고 4와 4 또는 5와 3밖에 얻지 못한다는 한계가 있다.
그래서 상기한 Epsilon 사용을 여전히 권장한다. 그러면 지역적 오차 관리 결과를 무시하고 원하는 다음 결과를 만들 수 있다. 즉 다음 예시의 printEpsilonBigDecimals
에서 divideRoundingMode
가 RoundingMode.DOWN
, RoundingMode.UP
등 어느 것이었는지와 무관하게 5와 4를 동시에 만들 수 있다.
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
class DecimalRoundDown {
public static void main(final String[] args) {
printNaivePrimitiveDoubles();
System.out.println();
printNaiveBigDecimals(RoundingMode.DOWN);
printNaiveBigDecimals(RoundingMode.UP);
System.out.println();
printEpsilonPrimitiveDoubles();
System.out.println();
printEpsilonBigDecimals(RoundingMode.DOWN);
printEpsilonBigDecimals(RoundingMode.UP);
}
private static void printNaivePrimitiveDoubles() {
final double five = 10. / 6 * 3;
final double four = (7 - five) * 2;
System.out.println(five + " " + (int) five);
System.out.println(four + " " + (int) four);
}
private static void printNaiveBigDecimals(final RoundingMode divideRoundingMode) {
BigDecimal five;
try {
five = new BigDecimal("10")
.divide(new BigDecimal("6"))
.multiply(new BigDecimal("3"));
} catch (Exception e) {
e.printStackTrace();
five = new BigDecimal("10")
.divide(new BigDecimal("6"), 3, divideRoundingMode)
.multiply(new BigDecimal("3"));
}
final BigDecimal four = new BigDecimal("7")
.subtract(five)
.multiply(new BigDecimal("2"));
final MathContext mc = new MathContext(2, RoundingMode.DOWN);
System.out.println(five + " " + five.intValue() + " " + five.round(mc));
System.out.println(four + " " + four.intValue() + " " + four.round(mc));
}
private static void printEpsilonPrimitiveDoubles() {
final double EPSILON = 1e-2;
final double five = 10. / 6 * 3;
final double four = (7 - five) * 2 + EPSILON;
final double fiveEpsilon = five + EPSILON;
System.out.println(fiveEpsilon + " " + (int) fiveEpsilon);
System.out.println(four + " " + (int) four);
}
private static void printEpsilonBigDecimals(final RoundingMode divideRoundingMode) {
final BigDecimal EPSILON = new BigDecimal("1e-2");
final BigDecimal five = new BigDecimal("10")
.divide(new BigDecimal("6"), 3, divideRoundingMode)
.multiply(new BigDecimal("3"));
final BigDecimal four = new BigDecimal("7")
.subtract(five)
.multiply(new BigDecimal("2"))
.add(EPSILON);
final BigDecimal fiveEpsilon = five.add(EPSILON);
final MathContext mc = new MathContext(2, RoundingMode.DOWN);
System.out.println(fiveEpsilon + " " + fiveEpsilon.intValue() + " " + fiveEpsilon.round(mc));
System.out.println(four + " " + four.intValue() + " " + four.round(mc));
}
}
5.0 5
4.0 4
java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
at java.base/java.math.BigDecimal.divide(BigDecimal.java:1780)
at DecimalRoundDown.printNaiveBigDecimals(DecimalRoundDown.java:39)
at DecimalRoundDown.main(DecimalRoundDown.java:12)
4.998 4 4.9
4.004 4 4.0
java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
at java.base/java.math.BigDecimal.divide(BigDecimal.java:1780)
at DecimalRoundDown.printNaiveBigDecimals(DecimalRoundDown.java:39)
at DecimalRoundDown.main(DecimalRoundDown.java:13)
5.001 5 5.0
3.998 3 3.9
5.01 5
4.01 4
5.008 5 5.0
4.014 4 4.0
5.011 5 5.0
4.008 4 4.0
@online{kee2024,
author = {Kee, Yunho},
title = {이진수 대신 십진수를 쓰면 오차가 없어질까?},
date = {2024-05-26},
url = {https://yhkee0404.github.io/posts/algorithms/decimal-round-down/},
langid = {ko}
}