Python decimal.localcontext: ล็อกกฎปัดเศษเฉพาะงาน ไม่ทำ Context รั่วทั้งโปรแกรม
งานเงินและบัญชีไม่ได้ต้องการแค่ “ทศนิยมแม่น” แต่ต้องมีกฎ precision และการปัดเศษตรงกัน ถ้าแก้ getcontext() ระดับกว้างเพื่อทำ calculation หนึ่งชุด โค้ดที่รันถัดไปใน context เดียวกันอาจรับกฎใหม่โดยไม่รู้ตัว decimal.localcontext สร้างสำเนา context ชั่วคราวและคืนค่าหลังออกจากบล็อก คล้ายตั้งโต๊ะชั่งเฉพาะใบเสร็จนี้ครับ
import unittest
from decimal import Decimal, ROUND_HALF_EVEN, ROUND_HALF_UP, getcontext, localcontext
def invoice_total(unit_price: str, quantity: int) -> Decimal:
with localcontext() as ctx:
ctx.prec = 12
ctx.rounding = ROUND_HALF_UP
raw = Decimal(unit_price) * Decimal(quantity)
return raw.quantize(Decimal("0.01"))
class DecimalContextTests(unittest.TestCase):
def test_rounds_money_inside_local_rule(self):
self.assertEqual(invoice_total("1.005", 1), Decimal("1.01"))
def test_does_not_leak_rounding_rule(self):
original = getcontext().rounding
getcontext().rounding = ROUND_HALF_EVEN
try:
invoice_total("2.675", 1)
self.assertEqual(getcontext().rounding, ROUND_HALF_EVEN)
finally:
getcontext().rounding = original
if __name__ == "__main__":
unittest.main()
ขอบเขตที่ต้องตกลงกับธุรกิจ
ROUND_HALF_UP ในตัวอย่างไม่ใช่กฎสากล บางระบบใช้ half-even บางประเทศกำหนดกฎตามชนิดภาษี และบางงานต้องปัดต่อบรรทัดก่อนรวมแทนการรวมแล้วปัดครั้งเดียว โค้ดที่ดีจึงตั้งชื่อ rule และมี fixture จากข้อกำหนดจริง ไม่ใช่เลือกเพราะ “ดูเหมือนโรงเรียนสอน”
สร้าง Decimal จาก string ไม่ใช่ float เพื่อไม่พาเศษฐานสองเข้ามาก่อนเริ่มคำนวณ กำหนด precision ให้พอสำหรับ intermediate result แล้วใช้ quantize ที่ boundary ซึ่งหน่วยเงินชัด ถ้ามี division ให้ทดสอบจำนวนหลักมากและค่าใกล้ครึ่งหน่วยย่อย
localcontext ช่วยกัน side effect ของ configuration แต่ไม่แทน validation ควรปฏิเสธ NaN/Infinity หากโดเมนไม่อนุญาต และแยก currency ที่มีจำนวน decimal place ต่างกัน อย่า hard-code 0.01 กับทุกสกุล
Recap
- ใช้ local context เมื่อกฎคำนวณเป็นขอบเขตเฉพาะ
- สร้าง Decimal จาก string และ quantize ที่ boundary
- ทดสอบกฎปัดเศษตามข้อกำหนดธุรกิจจริง
- ตรวจว่ากฎไม่รั่วไปยัง calculation ถัดไปครับ