[파이썬 100강] 62강. fractions로 비율 계산을 정확하게 표현하기
부동소수점(float)은 빠르고 편하지만, 0.1 같은 값을 내부에서 정확히 표현하지 못해 누적 오차가 생길 수 있습니다. 비율, 배합, 점수 가중치처럼 “정확한 분수 관계”가 중요한 작업에서는 이 오차가 결과 신뢰도를 떨어뜨리곤 합니다. 이번 강의에서는 fractions.Fraction으로 값을 분수 그대로 보존하면서 계산하는 방법을 다룹니다. 서론은 여기까지 하고, 바로 실전에 가까운 예제로 들어가겠습니다.
핵심 개념
Fraction은 분자/분모 형태로 유리수를 정확하게 표현한다.Fraction(1, 3)처럼 명시적으로 만들거나 문자열/정수에서 안전하게 변환할 수 있다.- float를 그대로 넣으면 “float가 가진 근사값”을 분수화하므로 입력 경로를 설계해야 한다.
limit_denominator()를 쓰면 사람이 해석 가능한 근사 분수로 정리할 수 있다.- 비율 계산, 단위 환산, 점수 가중치, 레시피 배율처럼 “관계 보존”이 중요한 곳에서 특히 강력하다.
Fraction의 핵심은 “정확도”보다 더 정확히 말하면 “표현의 보존”입니다. 예를 들어 1/3은 float에서 무한소수라 잘려 저장되지만, Fraction(1, 3)은 1/3 자체를 구조로 저장합니다. 그래서 덧셈/곱셈을 반복해도 오차가 누적되는 대신 분자와 분모가 규칙적으로 커지거나 약분됩니다. 이 특성은 테스트 코드에서도 장점입니다. 0.30000000000000004 같은 값 비교를 피하고, 도메인 규칙(예: 2:3 배합)을 그대로 assertion으로 표현할 수 있기 때문입니다. 다만 모든 계산을 Fraction으로 밀어붙이면 성능 비용이 늘 수 있으니, “정확성이 필수인 경계 구간”에서 선택적으로 쓰는 습관이 실무적으로 좋습니다.
기본 사용
예제 1) 분수 생성과 기본 연산
>>> from fractions import Fraction
>>> a = Fraction(1, 3)
>>> b = Fraction(1, 6)
>>> a + b
Fraction(1, 2)
>>> a * 12
Fraction(4, 1)
>>> float(a)
0.3333333333333333
해설:
Fraction(1, 3)은 1/3을 정확히 저장합니다.a + b결과가 정확히 1/2로 떨어지는 점이 중요합니다.- 필요할 때만
float()로 변환해 출력/시각화 단계에 넘기면 됩니다.
예제 2) 문자열 입력을 안전하게 분수로 변환
>>> from fractions import Fraction
>>> Fraction("0.1") + Fraction("0.2")
Fraction(3, 10)
>>> float(Fraction("0.1") + Fraction("0.2"))
0.3
>>> Fraction("7/8") * 4
Fraction(7, 2)
해설:
- 사용자 입력이 문자열일 때
Fraction("0.1")처럼 처리하면 의도한 십진값을 보존할 수 있습니다. Fraction("7/8")형식도 지원해서 설정 파일(JSON/YAML) 기반 시스템에서 활용하기 쉽습니다.- “입력 경로 통일”이 핵심입니다. 문자열/정수 중심으로 받으면 정확도 문제를 크게 줄일 수 있습니다.
예제 3) 배합 비율 계산 미니 케이스
>>> from fractions import Fraction
>>> ratio_water = Fraction(2, 5)
>>> ratio_concentrate = Fraction(3, 5)
>>> total_ml = 750
>>> water_ml = ratio_water * total_ml
>>> conc_ml = ratio_concentrate * total_ml
>>> water_ml, conc_ml
(Fraction(300, 1), Fraction(450, 1))
>>> int(water_ml), int(conc_ml)
(300, 450)
해설:
- 비율의 합(2/5 + 3/5 = 1)을 분수로 표현하면 검증이 명확해집니다.
- 용량 계산에서 중간 오차가 없어 배치 단위가 커져도 일관된 결과를 유지합니다.
- 현업에서는 마지막 출력 지점에서만 정수 반올림 규칙을 적용하세요.
예제 4) 사람이 읽기 쉬운 근사 분수 만들기
>>> from fractions import Fraction
>>> x = Fraction("3.14159265")
>>> x
Fraction(62831853, 20000000)
>>> x.limit_denominator(1000)
Fraction(355, 113)
>>> float(x.limit_denominator(1000))
3.1415929203539825
해설:
- 원본 분수는 매우 큰 분모를 가질 수 있습니다.
limit_denominator(1000)은 해석 가능한 범위에서 좋은 근사를 찾을 때 유용합니다.- 리포트 표시나 규칙 추천(예: “대략 355/113”)에 잘 맞습니다.
자주 하는 실수
실수 1) float를 그대로 Fraction에 넣기
>>> from fractions import Fraction
>>> Fraction(0.1)
Fraction(3602879701896397, 36028797018963968)
원인:
0.1float 자체가 이미 근사값으로 저장되어 있습니다.Fraction(0.1)은 “근사된 binary float”를 충실히 분수화한 결과입니다.
해결:
>>> from fractions import Fraction
>>> Fraction("0.1")
Fraction(1, 10)
>>> Fraction(1, 10)
Fraction(1, 10)
- 입력은 가능하면 문자열 또는 정수 분자/분모로 받습니다.
- API 경계에서 변환 정책을 고정하세요(예: 소수 입력은 문자열만 허용).
실수 2) Fraction과 반올림/출력 규칙을 섞어버리기
증상:
- 계산 중간에
round(float(x), 2)를 반복 적용해서 값이 흔들립니다.
원인:
- 중간 단계에서 float로 내려가면 Fraction의 장점(정확한 관계 보존)이 사라집니다.
해결:
- 계산 파이프라인 내부는 Fraction 유지
- 마지막 출력 단계에서만 반올림/형식화
- 리포트용 수치와 내부 계산 수치를 분리
실수 3) 분모가 커지는 비용을 무시하기
증상:
- 대량 데이터 누적 계산에서 처리 속도가 예상보다 느려짐
원인:
- Fraction 연산은 분자/분모 정수 연산 + 약분이 반복되어 비용이 증가할 수 있음
해결:
>>> from fractions import Fraction
>>> vals = [Fraction(n, 1000) for n in range(1, 1000)]
>>> total = sum(vals)
>>> total.limit_denominator(10000)
Fraction(999, 2)
- 정확성이 필요한 구간만 Fraction 사용
- 집계 후
limit_denominator()나 정책적 다운캐스팅 적용 - 성능 민감 경로는 벤치마크로 확인
실무 패턴
-
입력 검증 규칙
- 외부 입력이 소수라면 문자열로 받도록 스키마를 정의합니다.
- “분모 0 금지”, “음수 허용 여부”, “최대 분모 제한”을 명시합니다.
-
로그/예외 처리 규칙
- 변환 실패(
ValueError,ZeroDivisionError)는 사용자 입력 오류와 시스템 오류를 분리해 기록합니다. - 로그에는 원본 입력 문자열을 남기고, 내부에서는 정제된 Fraction만 전달합니다.
- 변환 실패(
-
재사용 함수/구조화 팁
parse_ratio(value: str) -> Fraction같은 헬퍼를 만들어 모든 엔드포인트에서 공유합니다.- 도메인 객체(예: Recipe, Allocation)에 Fraction 필드를 두면 규칙 테스트가 쉬워집니다.
-
성능/메모리 체크 포인트
- 루프 내부에서 불필요한
Fraction(str(x))재생성을 줄이고 캐시 가능한 값은 재사용합니다. - 최종 저장소(DB/CSV)에는
"numerator/denominator"또는 decimal 변환 정책을 표준화합니다.
- 루프 내부에서 불필요한
-
협업 패턴
- 팀 규약에 “float -> Fraction 직접 변환 금지”를 넣어 리뷰 기준을 명확히 합니다.
- 테스트에서는
pytest.approx대신 Fraction 동등성 비교를 우선 사용해 의도를 드러냅니다.
오늘의 결론
한 줄 요약: 비율과 배합처럼 관계가 중요한 계산은 float가 아니라 Fraction으로 모델링하면, 결과보다 먼저 신뢰도를 확보할 수 있습니다.
기억할 것:
- 정확성이 중요한 구간에서는 입력부터 Fraction 친화적으로 설계한다.
- 계산 중간에는 Fraction을 유지하고 출력 직전에만 float/문자열로 변환한다.
- 성능이 걱정되면 범위를 좁혀 적용하고 벤치마크로 검증한다.
연습문제
- 문자열 리스트
['1/3', '1/6', '1/2']를 Fraction으로 변환해 총합이 정확히 1인지 검사하는 코드를 작성해 보세요. - 총 1200ml 용액에서 비율이 7:5:3인 세 성분의 용량을 Fraction으로 계산하고, 최종 출력은 정수 ml로 보여주세요.
Fraction("2.7182818")을 분모 제한 100, 1000으로 각각 근사한 뒤, 원래 값과의 오차(절댓값)를 비교해 보세요.
이전 강의 정답
- 소수 누적 오차 없이 총액 계산하기
>>> from decimal import Decimal
>>> prices = [Decimal("19.90"), Decimal("5.10"), Decimal("0.30")]
>>> sum(prices)
Decimal('25.30')
- 반올림 규칙(은행가 반올림 대신 HALF_UP) 적용하기
>>> from decimal import Decimal, ROUND_HALF_UP
>>> amount = Decimal("2.345")
>>> amount.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
Decimal('2.35')
- 세율 계산 후 통화 단위 맞추기
>>> from decimal import Decimal, ROUND_HALF_UP
>>> net = Decimal("10000")
>>> tax = (net * Decimal("0.1")).quantize(Decimal("1"), rounding=ROUND_HALF_UP)
>>> gross = net + tax
>>> tax, gross
(Decimal('1000'), Decimal('11000'))
실습 환경/재현 정보
- 실행 환경:
condaenvpython100(Python 3.11.14) - 가정한 OS: macOS/Linux 공통
- 주요 표준 라이브러리:
fractions - 검증 방식: REPL(pycon) 기준으로 예제 실행