📸 تحلیل سوال
📀 احتمالا با این مشکل معروف مواجه شدید یا جایی دیدید:
>>> 0.1 + 0.2 == 0.3
False
>>> 0.1 + 0.2
0.30000000000000004
❔ اما چرا این اتفاق میفته؟ بذارید قبل از ورود به اصل مطلب، کمی حساب کتاب عادی انجام بدیم! فرض کنیم مقدار «یکهفتادم» رو از شما [در مبنای ۱۰ خودمون!] خواستن، میدونید که یک عدد اعشاری بدست میاد که انتهایی نداره!
0.0142857142857142857...
برای کامپیوتر هم، به همین شکل! منتهی مبنایی که برای کارهای خودش بکار میبره، مبنای ۲ هست. و وقتی اعدادی مثل ۰.۱ («یکدهم» در مبنای ده) رو بهش میدید، به یک عدد اعشاری در مبنای ۲ تبدیل میکنه، و عدد حاصل، اینجا هم انتهایی نداره!
0.00011001100110011001...
برای همین، پایتون - مثل خیلی از زبانها - تا یک جایی تعداد ارقام رو حساب میکنه. مثلا برای 0.1 و 0.2:
0.1 -> 0.0001100110011001100110011001100110011001100110011001101
0.2 -> 0.001100110011001100110011001100110011001100110011001101
که وقتی دوباره میخواد توی مبنای ده نشون بده، همچین چیزی میشه و [احتمالا!] همچین چیزی ببینید:
0.1 -> 0.1000000000000000055511151231257827021181583404541015625
0.2 -> 0.200000000000000011102230246251565404236316680908203125
🔭 برای گرفتن این مقدار، میتونید از سایت زیر استفاده کنید:
https://www.exploringbinary.com/floating-point-converter/👈 به همین ترتیب، کار با اعداد اعشاری، ممکن هست که کمی بالا و پایین داشته باشه! که البته تا وقتی که با مقادیر دقیق سر و کار ندارید، مشکل خاصی بوجود نمیاره.
‼️ دقت کنید که پایتون (درواقع: repr)، اعداد رو با تقریب بهتون نشون میده! مثلا وقتی ۰.۱ مینویسید، بعد از تبدیل، مقدار حاصل رو با تقریب ریزی نشون میده. مثلا:
>>> 0.1
0.1
>>> "{:.55f}".format(x)
'0.1000000000000000055511151231257827021181583404541015625'
پس دقت کنید که اکثر مواقع، عدد اعشاری با اندکی بالا و پایین شدن مقدار ذخیره شده؛ اما ممکن هست که بصورت تقریبی نمایش داده بشه، و سعی میکنه تا جای ممکن تقریب بزنه.
❓ اما، اگه خواستیم حساب دقیقی داشته باشیم، تکلیف چیه؟ لایبرری decimal این مشکل رو حل میکنه:
>>> from decimal import Decimal
>>> Decimal('0.1') + Decimal('0.2')
Decimal('0.3')
‼️ حواستون باشه که عدد اعشاری رو در قالب float به Decimal ندید! چون خود اون عدد اعشاری هم، اول توسط پایتون پردازش میشه و باز هم کمی تقریب این وسط بوجود میاد:
>>> Decimal(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')
>>> Decimal(0.2)
Decimal('0.200000000000000011102230246251565404236316680908203125')
برای همین، باید در قالب رشته بدید تا مقدار رو دقیق نگهداره و محاسبه کنه.
⚠️ در گزینهای که به عنوان جواب نهایی تلقی شد، یک ابهام وجود داره! در واقع، هدف از گذاشتن float، یکدست شدن typeها و float شدن بود، اما همچنان یک نکته وجود داره. وقتی که مینویسیم:
float(decimal.Decimal('0.3')) == 0.3
که decimal.Decimal('0.3') دقیقا برابر با حاصل decimal.Decimal('0.1') + decimal.Decimal('0.2') هست، ما True رو دریافت میکنیم! و واقعا هم همینطور هست! چون 0.3 در پایتون، در هر دو هست. اما این، همون 0.3 (کسر ۳ بر ۱۰) که ما در دنیای واقعی میشناسیم هست...؟ خیر! این رو ببینید:
>>> f"{0.3:.55f}"
'0.2999999999999999888977697537484345957636833190917968750'
>>> f"{float(decimal.Decimal('0.3')):.55f}"
'0.2999999999999999888977697537484345957636833190917968750'
برابرند، اما نه با اون چیزی که میگفتیم! :)
🙏🏻 از آقای
@AmirSoroushh بابت تذکر و توضیحات و راهنماییها تشکر میکنم.
📕 منابع (برای مطالعه بیشتر و عمیقتر توصیه میشه) :
🔗 python3 doc > 3.9.6 > tutorial > 15. Floating Point Arithmetic: Issues and Limitations
🔗 cpython/Lib/_pydecimal.py source code
🔗 language agnostic - Is floating point math broken? - Stack Overflow
🆔 @raspberry_python