วิธีที่ถูกต้องในการเปรียบเทียบ Floats ใน Python

วิธีที่ถูกต้องในการเปรียบเทียบ Floats ใน Python

ตัวเลขทศนิยมเป็นวิธีที่รวดเร็วและมีประสิทธิภาพในการจัดเก็บและทำงานกับตัวเลข แต่มีข้อผิดพลาดหลายอย่างที่ทำให้โปรแกรมเมอร์มือใหม่หลายคนต้องหยุดชะงัก บางทีอาจเป็นโปรแกรมเมอร์ที่มีประสบการณ์ด้วยเช่นกัน! ตัวอย่างคลาสสิกที่แสดงให้เห็นถึงข้อผิดพลาดของการลอยตัวมีลักษณะดังนี้:

 >>> 0.1 + 0.2 == 0.3 False

เห็นสิ่งนี้เป็นครั้งแรกอาจทำให้สับสนได้ แต่อย่าโยนคอมพิวเตอร์ของคุณลงในถังขยะ พฤติกรรมนี้ถูกต้อง!

บทความนี้จะแสดงให้คุณเห็นว่าเหตุใดข้อผิดพลาดทศนิยมแบบเดียวกับข้างต้นจึงเป็นเรื่องปกติ เหตุใดจึงสมเหตุสมผล และสิ่งที่คุณสามารถทำได้เพื่อจัดการกับข้อผิดพลาดเหล่านี้ใน Python

คอมพิวเตอร์ของคุณเป็นคนโกหก (เรียงลำดับ)

คุณเห็นแล้วว่า 0.1 + 0.2 ไม่เท่ากับ 0.3 แต่ความบ้าคลั่งไม่ได้หยุดอยู่แค่นั้น ต่อไปนี้คือตัวอย่างที่ทำให้สับสนมากขึ้น:

 >>> 0.2 + 0.2 + 0.2 == 0.6 False >>> 1.3 + 2.0 == 3.3 False >>> 1.2 + 2.4 + 3.6 == 7.2 False

ปัญหานี้ไม่ได้จำกัดอยู่เพียงการเปรียบเทียบความเท่าเทียมกัน:

 >>> 0.1 + 0.2 <= 0.3 False >>> 10.4 + 20.8 > 31.2 True >>> 0.8 - 0.1 > 0.7 True

เกิดอะไรขึ้น? คอมพิวเตอร์ของคุณโกหกคุณหรือไม่? ดูเหมือนว่าจะเป็นเช่นนั้น แต่มีอะไรมากกว่านั้นเกิดขึ้นภายใต้พื้นผิว

เมื่อคุณพิมพ์ตัวเลข 0.1 ลงในล่าม Python จะถูกเก็บไว้ในหน่วยความจำเป็นตัวเลขทศนิยม มีการแปลงที่เกิดขึ้นเมื่อสิ่งนี้เกิดขึ้น 0.1 เป็นทศนิยมในฐาน 10 แต่ตัวเลขทศนิยมถูกเก็บไว้ในไบนารี กล่าวอีกนัยหนึ่ง 0.1 จะถูกแปลงจากฐาน 10 เป็นฐาน 2

เลขฐานสองที่ได้อาจไม่ได้แสดงถึงตัวเลขฐาน 10 เดิมอย่างถูกต้อง 0.1 เป็นตัวอย่างหนึ่ง การแทนค่าไบนารีคือ \(0.0\overline{0011}\) นั่นคือ 0.1 เป็นทศนิยมซ้ำอนันต์เมื่อเขียนในฐาน 2 สิ่งเดียวกันนี้เกิดขึ้นเมื่อคุณเขียนเศษส่วน ⅓ เป็นทศนิยมในฐาน 10 คุณลงเอยด้วยทศนิยมซ้ำอนันต์ \(0.\overline{33}\ ).

หน่วยความจำคอมพิวเตอร์มีจำกัด ดังนั้นการแสดงเศษส่วนไบนารีซ้ำอนันต์ของ 0.1 จะถูกปัดเศษเป็นเศษส่วนจำกัด ค่าของตัวเลขนี้ขึ้นอยู่กับสถาปัตยกรรมของคอมพิวเตอร์ของคุณ (32 บิต กับ 64 บิต) วิธีหนึ่งในการดูค่าทศนิยมที่เก็บไว้เป็น 0.1 คือการใช้ .as_integer_ratio() สำหรับการทศนิยมเพื่อรับตัวเศษและตัวส่วนของการแสดงจุดทศนิยม:

 >>> numerator, denominator = (0.1).as_integer_ratio() >>> f"0.1 ≈ {numerator} / {denominator}" '0.1 ≈ 3602879701896397 / 36028797018963968'

ตอนนี้ใช้ format() เพื่อแสดงเศษส่วนที่ถูกต้องเป็นทศนิยม 55 ตำแหน่ง:

 >>> format(numerator / denominator, ".55f") '0.1000000000000000055511151231257827021181583404541015625'

ดังนั้น 0.1 จะถูกปัดเศษเป็นจำนวนที่มากกว่าค่าจริงเล็กน้อย

🐍
เรียนรู้เพิ่มเติมเกี่ยวกับวิธีการเกี่ยวกับตัวเลข เช่น .as_integer_ratio() ในบทความของฉัน 3 สิ่งที่คุณอาจไม่รู้เกี่ยวกับตัวเลขใน Python

ข้อผิดพลาดนี้เรียกว่า ข้อผิดพลาดในการแสดง จุดทศนิยม เกิดขึ้นบ่อยกว่าที่คุณคิด

ข้อผิดพลาดในการเป็นตัวแทนเป็นเรื่องธรรมดา จริงๆ

มีเหตุผลสามประการที่ทำให้ตัวเลขถูกปัดเศษเมื่อแสดงเป็นตัวเลขทศนิยม:

  1. ตัวเลขมีเลขนัยสำคัญมากกว่าจุดทศนิยมที่อนุญาต
  2. ตัวเลขไม่ลงตัว
  3. ตัวเลขเป็นจำนวนตรรกยะแต่มีการแทนค่าไบนารีที่ไม่สิ้นสุด

ตัวเลขทศนิยม 64 บิตดีสำหรับตัวเลขนัยสำคัญประมาณ 16 หรือ 17 หลัก ตัวเลขใดๆ ที่มีเลขนัยสำคัญมากกว่าจะถูกปัดเศษ จำนวนอตรรกยะ เช่น π และ e ไม่สามารถแทนด้วยเศษส่วนที่สิ้นสุดในฐานจำนวนเต็มใดๆ ดังนั้น ไม่ว่าจะเกิดอะไรขึ้น ตัวเลขอตรรกยะจะถูกปัดเศษเมื่อเก็บเป็นทศนิยม

สองสถานการณ์นี้สร้างชุดตัวเลขอนันต์ที่ไม่สามารถแสดงเป็นตัวเลขทศนิยมได้อย่างแน่นอน แต่ถ้าคุณไม่ใช่นักเคมีที่เกี่ยวกับตัวเลขเล็กๆ หรือนักฟิสิกส์ที่จัดการกับจำนวนมหาศาลทางดาราศาสตร์ คุณก็ไม่น่าจะประสบปัญหาเหล่านี้

แล้วจำนวนตรรกยะที่ไม่สิ้นสุด เช่น 0.1 ในฐาน 2 ล่ะ นี่คือจุดที่คุณจะพบปัญหาจุดลอยตัวส่วนใหญ่ และด้วยคณิตศาสตร์ที่กำหนดว่าเศษส่วนสิ้นสุดลงหรือไม่ คุณจะปัดเป่าข้อผิดพลาดในการแทนค่าได้บ่อยกว่าที่คุณคิด

ในฐาน 10 เศษส่วนสามารถแสดงเป็นเศษส่วนที่สิ้นสุดได้หากตัวส่วนเป็นผลคูณของ ตัวประกอบเฉพาะ ของ 10 ตัวประกอบเฉพาะของ 10 สองตัวคือ 2 และ 5 ดังนั้นเศษส่วนเช่น ½, ¼, ⅕, ⅛ และ ⅒ ยุติทั้งหมด แต่ ⅓, ⅐ และ ⅑ ไม่ยุติ อย่างไรก็ตาม ในฐาน 2 มีตัวประกอบเฉพาะเพียงตัวเดียว: 2. เศษส่วนที่มีตัวส่วนเป็นยกกำลัง 2 เท่านั้นจึงสิ้นสุด ด้วยเหตุนี้ เศษส่วนเช่น ⅓, ⅕, ⅙, ⅐, ⅑ และ ⅒ จึงไม่สิ้นสุดเมื่อแสดงเป็นเลขฐานสอง

ตอนนี้คุณสามารถเข้าใจตัวอย่างดั้งเดิมในบทความนี้ 0.1 , 0.2 และ 0.3 ทั้งหมดจะถูกปัดเศษเมื่อแปลงเป็นตัวเลขทศนิยม:

 >>> # -----------vvvv Display with 17 significant digits >>> format(0.1, ".17g") '0.10000000000000001' >>> format(0.2, ".17g") '0.20000000000000001' >>> format(0.3, ".17g") '0.29999999999999999'

เมื่อเพิ่ม 0.1 และ 0.2 ผลลัพธ์จะเป็นตัวเลขที่มากกว่า 0.3 เล็กน้อย:

 >>> 0.1 + 0.2 0.30000000000000004

เนื่องจาก 0.1 + 0.2 มีขนาดใหญ่กว่า 0.3 เล็กน้อย และ 0.3 จะแสดงด้วยตัวเลขที่เล็กกว่าตัวมันเองเล็กน้อย นิพจน์ 0.1 + 0.2 == 0.3 จะถูกประเมินเป็น False

ข้อผิดพลาดในการแสดงจุดทศนิยมเป็นสิ่งที่โปรแกรมเมอร์ทุกคนในทุกภาษาจำเป็นต้องรับรู้และรู้วิธีจัดการ ไม่เฉพาะเจาะจงกับ Python คุณสามารถดูผลลัพธ์ของการพิมพ์ 0.1 + 0.2 ในภาษาต่างๆ ได้ที่เว็บไซต์ที่มีชื่อเหมาะเจาะของ Erik Wiffin 0.300000000000004.com

วิธีเปรียบเทียบ Floats ใน Python

ดังนั้นคุณจะจัดการกับข้อผิดพลาดในการแสดงจุดทศนิยมได้อย่างไรเมื่อเปรียบเทียบทศนิยมใน Python เคล็ดลับคือการหลีกเลี่ยงการตรวจสอบความเท่าเทียมกัน ห้ามใช้ == , >= หรือ <= กับ floats ใช้ math.isclose() แทน:

 >>> import math >>> math.isclose(0.1 + 0.2, 0.3) True

math.isclose() ตรวจสอบว่าอาร์กิวเมนต์แรกใกล้เคียงกับอาร์กิวเมนต์ที่สองหรือไม่ แต่นั่นหมายความว่าอย่างไร? เคล็ดลับคือการตรวจสอบระยะห่างระหว่างอาร์กิวเมนต์แรกกับอาร์กิวเมนต์ที่สอง ซึ่งเทียบเท่ากับค่าสัมบูรณ์ของผลต่างของทั้งสองค่า:

 >>> a = 0.1 + 0.2 >>> b = 0.3 >>> abs(a - b) 5.551115123125783e-17

ถ้า abs(a - b) น้อยกว่าเปอร์เซ็นต์ของ a หรือ b ที่ใหญ่กว่า ถือว่า a ใกล้เคียงกับ b มากพอที่จะ “เท่ากับ” กับ b เปอร์เซ็นต์นี้เรียกว่าความ อดทนสัมพัทธ์ คุณสามารถระบุได้ด้วยอาร์กิวเมนต์คีย์เวิร์ด rel_tol ของ math.isclose() ซึ่งมีค่าเริ่มต้นเป็น 1e-9 กล่าวอีกนัยหนึ่งถ้า abs(a - b) น้อยกว่า 0.00000001 * max(abs(a), abs(b)) a และ b จะถือว่า “ใกล้” กัน สิ่งนี้รับประกันว่า a และ b มีค่าเท่ากับทศนิยมเก้าตำแหน่ง

คุณสามารถเปลี่ยนค่าเผื่อสัมพัทธ์ได้หากต้องการ:

 >>> math.isclose(0.1 + 0.2, 0.3, rel_tol=1e-20) False

แน่นอน ความอดทนสัมพัทธ์นั้นขึ้นอยู่กับข้อจำกัดที่กำหนดโดยปัญหาที่คุณกำลังแก้ไข อย่างไรก็ตาม สำหรับการใช้งานประจำวันส่วนใหญ่ ค่าเผื่อสัมพัทธ์ที่เป็นค่าเริ่มต้นก็เพียงพอแล้ว

มีปัญหาหาก a หรือ b เป็นศูนย์และ rel_tol มีค่าน้อยกว่าหนึ่ง ในกรณีนั้น ไม่ว่าค่าที่ไม่ใช่ศูนย์จะเป็นศูนย์มากแค่ไหน ความคลาดเคลื่อนสัมพัทธ์รับประกันว่าการตรวจสอบความใกล้ชิดจะล้มเหลวเสมอ ในกรณีนี้ การใช้ความคลาดเคลื่อนสัมบูรณ์ทำงานเป็นทางเลือก:

 >>> # Relative check fails! >>> # ---------------vvvv Relative tolerance >>> # ----------------------vvvvv max(0, 1e-10) >>> abs(0 - 1e-10) < 1e-9 * 1e-10 False >>> # Absolute check works! >>> # ---------------vvvv Absolute tolerance >>> abs(0 - 1e-10) < 1e-9 True

math.isclose() จะทำการตรวจสอบให้คุณโดยอัตโนมัติ อาร์กิวเมนต์คำหลัก abs_tol กำหนดความอดทนสัมบูรณ์ อย่างไรก็ตาม abs_tol ค่าเริ่มต้นเป็น 0.0 ดังนั้น คุณจะต้องตั้งค่านี้ด้วยตนเอง หากคุณต้องการตรวจสอบว่าค่าใกล้ศูนย์แค่ไหน

สรุปแล้ว math.isclose() ส่งคืนผลลัพธ์ของการเปรียบเทียบต่อไปนี้ ซึ่งรวมการทดสอบแบบสัมพัทธ์และแบบสัมบูรณ์เป็นนิพจน์เดียว:

 abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)

math.isclose() ถูกนำมาใช้ใน PEP 485 และพร้อมใช้งานตั้งแต่ Python 3.5

คุณควรใช้ math.isclose() เมื่อใด

โดยทั่วไป คุณควรใช้ math.isclose() เมื่อใดก็ตามที่คุณต้องการเปรียบเทียบค่าทศนิยม แทนที่ == ด้วย math.isclose() :

 >>> # Don't do this: >>> 0.1 + 0.2 == 0.3 False >>> # Do this instead: >>> math.isclose(0.1 + 0.2, 0.3) True

คุณต้องระวังด้วย >= และ <= การเปรียบเทียบ จัดการความเท่าเทียมกันแยกกันโดยใช้ math.isclose() จากนั้นตรวจสอบการเปรียบเทียบที่เข้มงวด:

 >>> a, b, c = 0.1, 0.2, 0.3 >>> # Don't do this: >>> a + b <= c False >>> # Do this instead: >>> math.isclose(a + b, c) or (a + b < c) True

มีทางเลือกอื่นสำหรับ math.isclose() หากคุณใช้ NumPy คุณสามารถใช้ประโยชน์จาก numpy.allclose() และ numpy.isclose() :

 >>> import numpy as np >>> # Use numpy.allclose() to check if two arrays are equal >>> # to each other within a tolerance. >>> np.allclose([1e10, 1e-7], [1.00001e10, 1e-8]) False >>> np.allclose([1e10, 1e-8], [1.00001e10, 1e-9]) True >>> # Use numpy.isclose() to check if the elements of two arrays >>> # are equal to each other within a tolerance >>> np.isclose([1e10, 1e-7], [1.00001e10, 1e-8]) array([ True, False]) >>> np.isclose([1e10, 1e-8], [1.00001e10, 1e-9]) array([ True, True])

โปรดทราบว่าค่าความคลาดเคลื่อนที่สัมพันธ์เริ่มต้นและความคลาดเคลื่อนสัมบูรณ์ไม่เหมือนกับ math.isclose() ค่าเผื่อสัมพัทธ์เริ่มต้นสำหรับทั้ง numpy.allclose() และ numpy.isclose() คือ 1e-05 และค่าเผื่อเริ่มต้นที่แน่นอนสำหรับทั้งคู่คือ 1e-08

math.isclose() มีประโยชน์อย่างยิ่งสำหรับการทดสอบหน่วย แม้ว่าจะมีทางเลือกอื่นอยู่บ้าง โมดูล unittest ในตัวของ Python มีเมธอดunittest.TestCase.assertAlmostEqual() อย่างไรก็ตาม วิธีการนั้นใช้การทดสอบความแตกต่างแบบสัมบูรณ์เท่านั้น นอกจากนี้ยังเป็นการยืนยันด้วย ซึ่งหมายความว่าความล้มเหลวทำให้เกิด AssertionError ซึ่งทำให้ไม่เหมาะสำหรับการเปรียบเทียบในตรรกะทางธุรกิจของคุณ

ทางเลือกที่ดีสำหรับ math.isclose() สำหรับการทดสอบหน่วยคือ pytest.approx() จาก แพ็คเกจ pytest เช่นเดียวกับ math.isclose() , pytest.approx() รับสองอาร์กิวเมนต์และคืนค่าว่าเท่ากันหรือไม่ภายในเกณฑ์ความคลาดเคลื่อนบางประการ:

 >>> import pytest >>> pytest.approx(0.1 + 0.2, 0.3) True

เช่นเดียวกับ math.isclose() pytest.approx() มีอาร์กิวเมนต์คีย์เวิร์ด rel_tol และ abs_tol สำหรับการตั้งค่าความคลาดเคลื่อนสัมพัทธ์และสัมบูรณ์ อย่างไรก็ตาม ค่าเริ่มต้นจะแตกต่างกัน rel_tol มีค่าเริ่มต้น 1e-6 และ abs_tol มีค่าเริ่มต้น 1e-12

หากอาร์กิวเมนต์สองตัวแรกส่งผ่านไปยัง pytest.approx() มีลักษณะเหมือนอาร์เรย์ หมายความว่าเป็น Python ที่ iterable เช่น list หรือ tuple หรือแม้แต่อาร์เรย์ NumPy pytest.approx() จะทำงานเหมือน numpy.allclose() และส่งคืนว่าอาร์เรย์ทั้งสองมีค่าเท่ากันภายในค่าความคลาดเคลื่อนหรือไม่:

 >>> import numpy as np >>> np.array([0.1, 0.2]) + np.array([0.2, 0.4]) == pytest.approx(np.array([0.3, 0.6])) True

pytest.approx() จะทำงานร่วมกับค่าพจนานุกรม:

 >>> {'a': 0.1 + 0.2, 'b': 0.2 + 0.4} == pytest.approx({'a': 0.3, 'b': 0.6}) True

ตัวเลขทศนิยมเหมาะสำหรับการทำงานกับตัวเลขเมื่อใดก็ตามที่ไม่ต้องการความแม่นยำอย่างแท้จริง มีความรวดเร็วและหน่วยความจำมีประสิทธิภาพ แต่ถ้าคุณต้องการความแม่นยำ มีทางเลือกอื่นที่คุณควรพิจารณา

ทางเลือกทศนิยมที่แม่นยำ

มีตัวเลขในตัวสองประเภทใน Python ที่ให้ความแม่นยำเต็มที่สำหรับสถานการณ์ที่ floats ไม่เพียงพอ: Decimal และ Fraction

ประเภท Decimal

ประเภท Decimal สามารถจัดเก็บค่าทศนิยมได้อย่างแม่นยำเท่าที่คุณต้องการ โดยค่าเริ่มต้น Decimal จะรักษาตัวเลขสำคัญ 28 ตัว แต่คุณสามารถเปลี่ยนสิ่งนี้เป็นสิ่งที่คุณต้องการเพื่อให้เหมาะกับปัญหาเฉพาะที่คุณกำลังแก้ไข:

 >>> # Import the Decimal type from the decimal module >>> from decimal import Decimal >>> # Values are represented exactly so no rounding error occurs >>> Decimal("0.1") + Decimal("0.2") == Decimal("0.3") True >>> # By default 28 significant figures are preserved >>> Decimal(1) / Decimal(7) Decimal('0.1428571428571428571428571429') >>> # You can change the significant figures if needed >>> from decimal import getcontext >>> getcontext().prec = 6 # Use 6 significant figures >>> Decimal(1) / Decimal(7) Decimal('0.142857')

คุณสามารถอ่านเพิ่มเติมเกี่ยวกับประเภท Decimal ได้ใน เอกสาร Python

ประเภท Fraction

อีกทางเลือกหนึ่งสำหรับตัวเลขทศนิยมคือ ประเภท Fraction Fraction สามารถเก็บจำนวนตรรกยะได้อย่างแม่นยำและเอาชนะปัญหาข้อผิดพลาดในการแสดงที่พบกับตัวเลขทศนิยม:

 >>> # import the Fraction type from the fractions module >>> from fractions import Fraction >>> # Instantiate a Fraction with a numerator and denominator >>> Fraction(1, 10) Fraction(1, 10) >>> # Values are represented exactly so no rounding error occurs >>> Fraction(1, 10) + Fraction(2, 10) == Fraction(3, 10) True

ทั้ง Fraction และ Decimal ให้ประโยชน์มากมายเหนือค่าทศนิยมมาตรฐาน อย่างไรก็ตาม ประโยชน์เหล่านี้มาพร้อมกับราคา: ความเร็วที่ลดลงและการใช้หน่วยความจำที่สูงขึ้น หากคุณไม่ต้องการความแม่นยำอย่างแท้จริง คุณก็ควรใช้ลูกลอย แต่สำหรับสิ่งต่าง ๆ เช่น แอปพลิเคชันทางการเงินและที่สำคัญต่อภารกิจ การประนีประนอมที่เกิดจาก Fraction และ Decimal อาจคุ้มค่า

บทสรุป

ค่าทศนิยมเป็นทั้งพรและคำสาป พวกเขาเสนอการดำเนินการทางคณิตศาสตร์ที่รวดเร็วและการใช้หน่วยความจำอย่างมีประสิทธิภาพโดยเสียค่าใช้จ่ายในการแสดงที่ไม่ถูกต้อง ในบทความนี้ คุณได้เรียนรู้:

  • ทำไมตัวเลขทศนิยมไม่แม่นยำ
  • เหตุใดข้อผิดพลาดในการแสดงจุดทศนิยมจึงเป็นเรื่องปกติ
  • วิธีเปรียบเทียบค่าทศนิยมใน Python . อย่างถูกต้อง
  • วิธีแสดงตัวเลขอย่างแม่นยำโดยใช้ประเภท Fraction และ Decimal ของ Python

หากคุณได้เรียนรู้สิ่งใหม่ๆ อาจมีมากกว่านั้นที่คุณไม่รู้เกี่ยวกับตัวเลขใน Python ตัวอย่างเช่น คุณทราบหรือไม่ว่าประเภท int ไม่ใช่ประเภทจำนวนเต็มเพียงชนิดเดียวใน Python ค้นหาว่าจำนวนเต็มประเภทอื่นคืออะไรและข้อเท็จจริงอื่นๆ ที่ไม่ค่อยมีใครรู้จักเกี่ยวกับตัวเลขในบทความของฉัน 3 สิ่งที่คุณอาจไม่ทราบเกี่ยวกับตัวเลขใน Python

วิธีที่ถูกต้องในการเปรียบเทียบ Floats ใน Python

แหล่งข้อมูลเพิ่มเติม


ต้องการยกระดับทักษะ Python ของคุณไปอีกระดับหรือไม่? ฉันเสนอการฝึกสอนแบบตัวต่อตัวแบบตัวต่อตัวสำหรับการเขียนโปรแกรม Python และการเขียนเชิงเทคนิค คลิกที่นี่ เพื่อเรียนรู้เพิ่มเติม

ใส่ความเห็น