ทำไมค่าทศนิยมของ 4 * 0.1 ดูดีใน Python 3 แต่ 3 * 0.1 ไม่ได้?


158

ฉันรู้ว่าทศนิยมส่วนใหญ่ไม่ได้เป็นตัวแทนลอยจุดที่แน่นอน ( ลอยคณิตศาสตร์จุดเสีย? )

แต่ฉันไม่เห็นสาเหตุที่4*0.1พิมพ์ออกมาเป็นอย่างดี0.4แต่3*0.1ไม่ใช่เมื่อค่าทั้งสองมีการแสดงทศนิยมที่น่าเกลียด:

>>> 3*0.1
0.30000000000000004
>>> 4*0.1
0.4
>>> from decimal import Decimal
>>> Decimal(3*0.1)
Decimal('0.3000000000000000444089209850062616169452667236328125')
>>> Decimal(4*0.1)
Decimal('0.40000000000000002220446049250313080847263336181640625')

7
เพราะตัวเลขบางตัวสามารถแสดงได้อย่างแม่นยำและบางอันก็ไม่สามารถทำได้
Morgan Thrapp

58
@MorganThrapp: ไม่มันไม่ใช่ OP กำลังถามเกี่ยวกับตัวเลือกการจัดรูปแบบที่ค่อนข้างมีลักษณะโดยพลการ 0.3 หรือ 0.4 สามารถแทนได้อย่างแน่นอนในเลขทศนิยม
Bathsheba

42
@BartoszKP: ต้องอ่านเอกสารหลายครั้งก็ไม่ได้อธิบายว่าทำไมงูหลามจะแสดง0.3000000000000000444089209850062616169452667236328125เป็น0.30000000000000004และ0.40000000000000002220446049250313080847263336181640625เป็น.4ถึงแม้ว่าพวกเขาดูเหมือนจะมีความถูกต้องเหมือนกันและจึงไม่ได้ตอบคำถาม
Mooing Duck

6
ดูstackoverflow.com/questions/28935257/อีกด้วย- ฉันค่อนข้างหงุดหงิดที่ถูกปิดเป็นรายการที่ซ้ำกัน แต่อันนี้ไม่มี
Random832

12
เปิด, โปรดอย่าใกล้นี้เป็นซ้ำของ "ลอยคณิตศาสตร์จุดเสีย"
Antti Haapala

คำตอบ:


301

คำตอบง่ายๆคือเนื่องจาก3*0.1 != 0.3ข้อผิดพลาด quantization (roundoff) (ในขณะที่4*0.1 == 0.4เนื่องจากการคูณด้วยกำลังสองมักจะเป็นการดำเนินการ "แน่นอน")

คุณสามารถใช้.hexวิธีการใน Python เพื่อดูการแทนค่าภายในของตัวเลข (โดยทั่วไปคือค่าเลขทศนิยมแบบไบนารีที่แน่นอนแทนที่จะเป็นการประมาณเบส -10) สิ่งนี้สามารถช่วยอธิบายสิ่งที่เกิดขึ้นภายใต้ประทุน

>>> (0.1).hex()
'0x1.999999999999ap-4'
>>> (0.3).hex()
'0x1.3333333333333p-2'
>>> (0.1*3).hex()
'0x1.3333333333334p-2'
>>> (0.4).hex()
'0x1.999999999999ap-2'
>>> (0.1*4).hex()
'0x1.999999999999ap-2'

0.1 คือ 0x1.999999999999a คูณ 2 ^ -4 "a" ที่ท้ายหมายถึงตัวเลข 10 - ในคำอื่น ๆ , 0.1 ในทศนิยมเลขฐานสองมีขนาดใหญ่กว่าค่า "แน่นอน" ที่ 0.1 เล็กน้อย (เพราะ 0x0.99 สุดท้ายจะถูกปัดเศษขึ้นเป็น 0x0.a) เมื่อคุณคูณนี้โดย 4 อำนาจของสองตัวแทนกะขึ้น (จาก 2 ^ -4 ถึง 2 ^ -2) 4*0.1 == 0.4แต่จำนวนไม่เปลี่ยนแปลงเป็นอย่างอื่นดังนั้น

อย่างไรก็ตามเมื่อคุณคูณด้วย 3 ความแตกต่างเล็ก ๆ ระหว่าง 0x0.99 และ 0x0.a0 (0x0.07) จะขยายเป็นข้อผิดพลาด 0x0.15 ซึ่งแสดงเป็นข้อผิดพลาดหนึ่งหลักในตำแหน่งสุดท้าย สิ่งนี้ทำให้ 0.1 * 3 มีขนาดใหญ่กว่าค่าโค้งมนเล็กน้อย 0.3

การลอยของ Python 3 reprได้รับการออกแบบให้สามารถปัดเศษได้ซึ่งก็คือค่าที่แสดงควรแปลงให้เป็นค่าดั้งเดิมได้อย่างแน่นอน ดังนั้นจึงไม่สามารถแสดง0.3และ0.1*3เหมือนกันอย่างแน่นอนหรือตัวเลขที่แตกต่างกันสองหมายเลขจะสิ้นสุดลงเหมือนเดิมหลังจากปัดเศษรอบ ดังนั้นreprเครื่องยนต์ของ Python 3 เลือกที่จะแสดงอันที่มีข้อผิดพลาดที่เห็นได้ชัดเล็กน้อย


25
นี่เป็นคำตอบที่ครอบคลุมอย่างน่าอัศจรรย์ขอบคุณ (โดยเฉพาะอย่างยิ่งขอบคุณสำหรับการแสดง.hex()ฉันไม่ทราบว่ามันมีอยู่)
NPE

21
@supercat: Python พยายามค้นหาสตริงที่สั้นที่สุดที่จะปัดเศษให้เป็นค่าที่ต้องการไม่ว่าจะเกิดอะไรขึ้นก็ตาม เห็นได้ชัดว่าค่าที่ประเมินจะต้องอยู่ภายใน 0.5ulp (หรือมันจะปัดเศษเป็นอย่างอื่น) แต่อาจต้องการตัวเลขเพิ่มเติมในกรณีที่ไม่ชัดเจน รหัสนี้เป็นgnarly มากแต่ถ้าคุณต้องการแอบดู: hg.python.org/cpython/file/03f2c8fc24ea/Python/dtoa.c#l2345
nneonneo

2
@supercat: สตริงที่สั้นที่สุดที่อยู่ภายใน 0.5 ulp เสมอ ( ภายในอย่างเคร่งครัดหากเรากำลังดูโฟลตที่มี LSB แปลก ๆ นั่นคือสตริงที่สั้นที่สุดที่ทำให้มันทำงานได้กับความสัมพันธ์แบบกลม - ต่อ - คู่) ข้อยกเว้นใด ๆ สำหรับข้อผิดพลาดนี้เป็นข้อผิดพลาดและควรรายงาน
Mark Dickinson

7
@ MarkRansom แน่นอนพวกเขาใช้อย่างอื่นมากกว่าeเพราะนั่นคือเลขฐานสิบหกอยู่แล้ว บางทีpสำหรับพลังงานแทนการยกกำลัง
Bergi

11
@Bergi: การใช้งานpในบริบทนี้กลับไป (อย่างน้อย) ถึง C99 และยังปรากฏใน IEEE 754 และในภาษาอื่น ๆ อีกมากมาย (รวมถึง Java) เมื่อใดfloat.hexและfloat.fromhexมีการนำไปใช้ (โดยฉัน :-) Python เป็นเพียงการคัดลอกสิ่งที่เกิดขึ้นจากการฝึกฝนในตอนนั้น ฉันไม่รู้ว่าเจตนาคือ 'p' สำหรับ "Power" แต่ดูเหมือนจะเป็นวิธีที่ดีในการคิดเกี่ยวกับมัน
Mark Dickinson

75

repr(และstrใน Python 3) จะใส่ตัวเลขหลายหลักตามที่ต้องการเพื่อทำให้ค่าไม่คลุมเครือ ในกรณีนี้ผลลัพธ์ของการคูณ3*0.1ไม่ใช่ค่าที่ใกล้เคียงที่สุดกับ 0.3 (0x1.3333333333333-2 ใน hex) จริง ๆ แล้วมันเป็นหนึ่ง LSB ที่สูงกว่า (0x1.3333333333334p-2) ดังนั้นจึงต้องการตัวเลขที่มากขึ้นเพื่อแยกความแตกต่างจาก 0.3

ในทางกลับกันการคูณ4*0.1 จะได้ค่าที่ใกล้เคียงที่สุดกับ 0.4 (0x1.999999999999-2 ในรูปแบบเลขฐานสิบหก) ดังนั้นจึงไม่ต้องการตัวเลขเพิ่มเติม

คุณสามารถตรวจสอบได้ค่อนข้างง่าย:

>>> 3*0.1 == 0.3
False
>>> 4*0.1 == 0.4
True

ฉันใช้สัญลักษณ์หกเหลี่ยมด้านบนเพราะมันดีและกะทัดรัดและแสดงความแตกต่างเล็กน้อยระหว่างค่าสองค่า (3*0.1).hex()คุณสามารถทำได้ด้วยตัวคุณเองโดยใช้เช่น หากคุณต้องการเห็นพวกเขาในทุกตำแหน่งทศนิยมคุณจะไปที่:

>>> Decimal(3*0.1)
Decimal('0.3000000000000000444089209850062616169452667236328125')
>>> Decimal(0.3)
Decimal('0.299999999999999988897769753748434595763683319091796875')
>>> Decimal(4*0.1)
Decimal('0.40000000000000002220446049250313080847263336181640625')
>>> Decimal(0.4)
Decimal('0.40000000000000002220446049250313080847263336181640625')

2
(+1) คำตอบที่ดีขอบคุณ คุณคิดว่ามันอาจจะมีมูลค่าที่แสดงว่า "ไม่คุ้มค่าที่ใกล้เคียงที่สุด" จุดโดยรวมทั้งผลของ3*0.1 == 0.3และ4*0.1 == 0.4?
NPE

@NPE ฉันควรจะทำอย่างนั้นออกไปนอกประตูขอบคุณสำหรับคำแนะนำ
Mark Ransom

ฉันสงสัยว่ามันจะคุ้มค่าหรือไม่ที่จะทราบค่าทศนิยมที่แม่นยำของ "doubles" ที่ใกล้ที่สุดถึง 0.1, 0.3 และ 0.4 เนื่องจากผู้คนจำนวนมากไม่สามารถอ่านเลขฐานสิบหกที่ลอยได้
supercat

@ supercat คุณทำคะแนนได้ดี การใส่คู่ที่มีขนาดใหญ่พิเศษเหล่านี้ลงในข้อความจะทำให้เสียสมาธิ แต่ฉันคิดถึงวิธีที่จะเพิ่มพวกเขา
Mark Ransom

25

นี่คือข้อสรุปที่ง่ายจากคำตอบอื่น ๆ

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

เริ่มต้นด้วยรุ่น 3.2, Python strและreprใช้รูปแบบการปัดเศษแบบซับซ้อนซึ่งต้องการทศนิยมที่ดูดีถ้าเป็นไปได้ แต่ใช้ตัวเลขที่จำเป็นเพื่อรับประกันการทำแผนที่ bijective (แบบหนึ่งต่อหนึ่ง) ระหว่างการลอยตัวและการแสดงสตริง

โครงการนี้รับประกันความคุ้มค่าจากการที่repr(float(s))รูปลักษณ์ที่ดีสำหรับทศนิยมง่ายแม้ว่าพวกเขาไม่สามารถแสดงได้อย่างแม่นยำเป็นลอย (เช่น. s = "0.1")เมื่อ

ในขณะเดียวกันก็รับประกันได้ว่าfloat(repr(x)) == xจะมีการลอยทุกครั้งx


2
คำตอบของคุณนั้นถูกต้องสำหรับเวอร์ชัน Python> = 3.2 ที่strและreprเหมือนกันสำหรับการลอย สำหรับ Python 2.7 reprมีคุณสมบัติที่คุณระบุ แต่strง่ายกว่ามาก - เพียงคำนวณตัวเลข 12 หลักและสร้างสตริงเอาต์พุตตามค่าเหล่านั้น สำหรับ Python <= 2.6 ทั้งคู่reprและstrขึ้นอยู่กับจำนวนหลักที่สำคัญ (17 สำหรับrepr, 12 สำหรับstr) (และไม่มีใครสนใจ Python 3.0 หรือ Python 3.1 :-)
Mark Dickinson

ขอบคุณ @ Markdickinson! ฉันรวมความคิดเห็นของคุณไว้ในคำตอบแล้ว
Aivar

2
โปรดทราบว่าการปัดเศษของเชลล์มาจากreprพฤติกรรม Python 2.7 จะเหมือนกัน ...
Antti Haapala

5

ไม่เฉพาะเจาะจงกับการนำไปใช้ของ Python แต่ควรนำไปใช้กับฟังก์ชันสตริงทศนิยมหรือทศนิยม

จำนวนจุดลอยตัวนั้นเป็นเลขฐานสอง แต่เป็นสัญกรณ์ทางวิทยาศาสตร์ที่มีขีด จำกัด คงที่ของตัวเลขนัยสำคัญ

การผกผันของตัวเลขใด ๆ ที่มีตัวประกอบจำนวนเฉพาะที่ไม่ได้แชร์กับฐานจะส่งผลให้เกิดการแสดงจุดจุดซ้ำ ตัวอย่างเช่น 1/7 มีตัวประกอบสำคัญคือ 7 ซึ่งไม่ได้ใช้ร่วมกับ 10 ดังนั้นจึงมีการแทนทศนิยมที่เกิดซ้ำและเช่นเดียวกับ 1/10 ที่มีปัจจัยหลัก 2 และ 5 ซึ่งไม่ได้แชร์กับ 2 ; นี่หมายความว่า 0.1 ไม่สามารถถูกแทนด้วยจำนวนบิตที่แน่นอนหลังจากจุดจุด

เนื่องจาก 0.1 ไม่มีการนำเสนอที่แน่นอนฟังก์ชันที่แปลงการประมาณเป็นสตริงทศนิยมจะพยายามประมาณค่าบางค่าเพื่อไม่ให้ได้ผลลัพธ์ที่ไม่เข้าใจง่ายเช่น 0.1000000000004121

เนื่องจากจุดลอยตัวอยู่ในเครื่องหมายทางวิทยาศาสตร์การคูณใด ๆ ด้วยพลังของฐานจะมีผลเฉพาะส่วนเลขชี้กำลัง ตัวอย่างเช่น 1.231e + 2 * 100 = 1.231e + 4 สำหรับรูปแบบเลขฐานสิบและเช่นเดียวกันคือ 1.00101010e11 * 100 = 1.00101010e101 ในรูปแบบเลขฐานสอง ถ้าฉันคูณด้วยฐานที่ไม่ได้ใช้พลังงานตัวเลขที่สำคัญจะได้รับผลกระทบด้วย ตัวอย่างเช่น 1.2e1 * 3 = 3.6e1

ขึ้นอยู่กับอัลกอริทึมที่ใช้อาจพยายามเดาทศนิยมทั่วไปตามตัวเลขที่สำคัญเท่านั้น ทั้ง 0.1 และ 0.4 มีตัวเลขที่สำคัญเหมือนกันในไบนารี่เพราะทฤษฏีของมันนั้นถูกตัดทอนที่ (8/5) (2 ^ -4) และ (8/5) (2 ^ -6) ตามลำดับ หากอัลกอริทึมระบุรูปแบบ 8/5 sigfig เป็นทศนิยม 1.6 มันจะทำงานใน 0.1, 0.2, 0.4, 0.8, ฯลฯ มันอาจมีรูปแบบ sigfig เวทย์มนตร์สำหรับชุดค่าผสมอื่น ๆ เช่นทศนิยม 3 หารด้วยทศนิยม 10 และรูปแบบเวทย์มนตร์อื่น ๆ มีแนวโน้มที่จะเกิดขึ้นโดยการหารด้วย 10

ในกรณีของ 3 * 0.1 ตัวเลขที่มีนัยสำคัญไม่กี่คนที่ผ่านมาน่าจะแตกต่างจากการหารลอย 3 ด้วยการลอย 10 ทำให้อัลกอริทึมล้มเหลวในการจำหมายเลขเวทย์มนตร์สำหรับค่าคงที่ 0.3 ขึ้นอยู่กับความอดทนสำหรับการสูญเสียความแม่นยำ

แก้ไข: https://docs.python.org/3.1/tutorial/floatingpoint.html

ที่น่าสนใจมีตัวเลขทศนิยมที่แตกต่างกันมากมายที่ใช้ร่วมกันเศษส่วนไบนารีใกล้เคียงที่สุดโดยประมาณ ตัวอย่างเช่นตัวเลข 0.1 และ 0.10000000000000001 และ 0.10000000000000000000055511151231257827021181583404541015625 ทั้งหมดประมาณ 3602879701896397/2 ** 55 เนื่องจากค่าทศนิยมทั้งหมดเหล่านี้ใช้การประมาณเดียวกัน ) == x

ไม่มีความอดทนต่อการสูญเสียความแม่นยำหาก float x (0.3) ไม่เท่ากับลอย y (0.1 * 3) ดังนั้น repr (x) จะไม่เท่ากับเท่ากับ repr (y)


4
นี่ไม่ได้เพิ่มคำตอบที่มีอยู่จริงๆ
Antti Haapala

1
"ขึ้นอยู่กับอัลกอริธึมที่ใช้มันอาจพยายามเดาทศนิยมทั่วไปตามตัวเลขที่สำคัญเท่านั้น" <- ดูเหมือนว่าเป็นการเก็งกำไรที่บริสุทธิ์ คำตอบอื่น ๆ ได้อธิบายสิ่งที่หลามจริงไม่
Mark Dickinson
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.