اعداد اعشاری شناور در سختافزار رایانه به صورت کسرهای مبنای ۲ (باینری) ذخیره میشوند. به عنوان مثال، کسر اعشاری 0.625 مقدار ۶/۱۰ + ۲/۱۰۰ + ۵/۱۰۰۰ دارد، و به همین ترتیب کسر باینری 0.101 مقدار ۱/۲ + ۰/۴ + ۱/۸ دارد. این دو کسر مقادیر یکسانی دارند، تنها تفاوت واقعی این است که اولی در نماد کسری مبنای ۱۰ و دومی در مبنای ۲ نوشته شده است.
متأسفانه، بیشتر کسرهای اعشاری نمیتوانند دقیقاً به صورت کسرهای باینری نمایش داده شوند. نتیجه این است که به طور کلی، اعداد اعشاری شناوری که وارد میکنید فقط توسط اعداد اعشاری شناور باینری واقعاً ذخیره شده در ماشین تقریب زده میشوند.
مشکل در مبنای ۱۰ درک آن آسانتر است. کسر ۱/۳ را در نظر بگیرید. میتوانید آن را به عنوان کسری مبنای ۱۰ تقریب بزنید:
0.3
یا بهتر،
0.33
یا بهتر،
0.333
و الی آخر. مهم نیست چند رقم میخواهید بنویسید، نتیجه هرگز دقیقاً ۱/۳ نخواهد بود، بلکه تقریب فزاینده بهتری از ۱/۳ خواهد بود.
به همین ترتیب، مهم نیست چند رقم مبنای ۲ میخواهید استفاده کنید، مقدار اعشاری ۰.۱ نمیتواند دقیقاً به صورت کسری مبنای ۲ نمایش داده شود. در مبنای ۲، ۱/۱۰ کسری بینهایت تکرارشونده است:
0.0001100110011001100110011001100110011001100110011...
در هر تعداد محدودی از بیتها متوقف شوید و یک تقریب دریافت میکنید. در بیشتر ماشینهای امروزی، اعداد اعشاری شناور با استفاده از کسری باینری با صورتکسر استفاده کننده از ۵۳ بیت اول از مهمترین بیت و مخرج به صورت توانی از دو تقریب زده میشوند. در مورد ۱/۱۰، کسر باینری 3602879701896397 / 2 ** 55 است که نزدیک اما دقیقاً برابر مقدار واقعی ۱/۱۰ نیست.
بسیاری از کاربران به دلیل نحوه نمایش مقادیر از این تقریب آگاه نیستند. پایتون فقط یک تقریب اعشاری از مقدار اعشاری واقعی تقریب باینری ذخیره شده توسط ماشین چاپ میکند. در بیشتر ماشینها، اگر پایتون مقدار اعشاری واقعی تقریب باینری ذخیره شده برای ۰.۱ را چاپ کند، باید این را نمایش دهد:
>>> 0.1 0.1000000000000000055511151231257827021181583404541015625
این رقمهای بیشتری از آنچه بیشتر مردم مفید میدانند است، بنابراین پایتون تعداد رقمها را با نمایش مقدار گرد شده مدیریت میکند:
>>> 1 / 10 0.1
فقط به یاد داشته باشید، حتی اگر نتیجه چاپ شده مانند مقدار دقیق ۱/۱۰ به نظر برسد، مقدار ذخیره شده واقعی نزدیکترین کسر باینری قابل نمایش است.
جالب اینکه اعداد اعشاری متفاوت زیادی وجود دارند که تقریب باینری یکسانی دارند. به عنوان مثال، اعداد 0.1 و 0.10000000000000001 و 0.1000000000000000055511151231257827021181583404541015625 همگی توسط 3602879701896397 / 2 ** 55 تقریب زده میشوند. از آنجا که همه این مقادیر اعشاری تقریب یکسانی دارند، هر کدام از آنها میتواند نمایش داده شود در حالی که ناوردی eval(repr(x)) == x حفظ میشود.
از نظر تاریخی، اعلان پایتون و تابع داخلی repr() آن یکی با ۱۷ رقم معنادار، 0.10000000000000001 را انتخاب میکرد. از پایتون ۳.۱ به بعد، پایتون (در بیشتر سیستمها) اکنون قادر به انتخاب کوتاهترین آنها و نمایش ساده 0.1 است.
توجه کنید که این در ماهیت خود اعداد اعشاری شناور باینری است: این یک باگ در پایتون نیست، و در کد شما نیز باگ نیست. همان نوع چیز را در تمام زبانهایی که از حساب اعداد اعشاری شناور سختافزار شما پشتیبانی میکنند خواهید دید (هرچند برخی زبانها ممکن است تفاوت را به طور پیشفرض نمایش ندهند).
برای خروجی دلپذیرتر، ممکن است بخواهید از قالببندی رشته برای تولید تعداد محدودی رقم معنادار استفاده کنید:
>>> format(math.pi, '.12g') # ۱۲ رقم معنادار '3.14159265359' >>> format(math.pi, '.2f') # ۲ رقم بعد از نقطه '3.14' >>> repr(math.pi) '3.141592653589793'
مهم است درک کنید که این به یک معنای واقعی یک توهم است: شما فقط نمایش مقدار واقعی ماشین را گرد میکنید.
یک توهم ممکن است توهم دیگری ایجاد کند. به عنوان مثال، از آنجا که ۰.۱ دقیقاً ۱/۱۰ نیست، جمع کردن سه مقدار ۰.۱ ممکن است دقیقاً ۰.۳ نیز ندهد:
>>> 0.1 + 0.1 + 0.1 == 0.3 False
همچنین، از آنجا که ۰.۱ نمیتواند به مقدار واقعی ۱/۱۰ نزدیکتر شود و ۰.۳ نمیتواند به مقدار واقعی ۳/۱۰ نزدیکتر شود، گرد کردن از پیش با تابع round() کمک نمیکند:
>>> round(0.1, 1) + round(0.1, 1) + round(0.1, 1) == round(0.3, 1) False
اگرچه اعداد نمیتوانند به مقادیر واقعی مورد نظر نزدیکتر شوند، تابع math.isclose() میتواند برای مقایسه مقادیر غیردقیق مفید باشد:
>>> math.isclose(0.1 + 0.1 + 0.1, 0.3) True
یا به طور جایگزین، تابع round() میتواند برای مقایسه تقریبهای تقریبی استفاده شود:
>>> round(math.pi, ndigits=2) == round(22 / 7, ndigits=2) True
حساب اعداد اعشاری شناور باینری سورپرایزهای زیادی مانند این دارد. مشکل "۰.۱" به طور دقیق در بخش «خطای نمایش» در زیر توضیح داده شده است. نمونههایی از مشکلات اعداد اعشاری شناور را برای خلاصهای دلپذیر از نحوه عملکرد اعداد اعشاری شناور باینری و انواع مشکلات رایج ببینید. همچنین خطرات اعداد اعشاری شناور را برای حساب کاملتری از سورپرایزهای رایج ببینید.
همانطور که نزدیک انتهای آن گفته شده، "هیچ پاسخ آسانی وجود ندارد." با این حال، بیش از حد از اعداد اعشاری شناور نترسید! خطاهای عملیات اعداد اعشاری شناور پایتون از سختافزار اعداد اعشاری شناور به ارث رسیده و در بیشتر ماشینها در حدود بیش از ۱ قسمت در ۲**۵۳ در هر عملیات هستند. این بیش از کافی برای بیشتر وظایف است، اما باید به خاطر داشته باشید که حساب اعشاری نیست و هر عملیات اعداد اعشاری شناور میتواند یک خطای گرد کردن جدید داشته باشد.
اگرچه موارد پاتولوژیک وجود دارند، برای بیشتر استفادههای تصادفی از حساب اعداد اعشاری شناور، اگر به سادگی نمایش نتایج نهایی خود را به تعداد رقمهای اعشاری مورد انتظار خود گرد کنید، نتیجهای را که انتظار دارید در انتها خواهید دید. str() معمولاً کافی است و برای کنترل دقیقتر مشخصکنندههای قالب متد str.format() در نحو قالببندی رشته را ببینید.
برای موارد استفادهای که نمایش اعشاری دقیق نیاز دارند، استفاده از ماژول decimal را امتحان کنید که حساب اعشاری مناسب برنامههای حسابداری و برنامههای با دقت بالا پیادهسازی میکند.
یک فرم دیگر حساب دقیق توسط ماژول fractions پشتیبانی میشود که حساب مبتنی بر اعداد گویا (بنابراین اعدادی مانند ۱/۳ میتوانند دقیقاً نمایش داده شوند) پیادهسازی میکند.
اگر استفادهکننده سنگینی از عملیات اعداد اعشاری شناور هستید، باید نگاهی به بسته NumPy و بسیاری از بستههای دیگر برای عملیات ریاضی و آماری عرضه شده توسط پروژه SciPy بیندازید.
پایتون ابزارهایی فراهم میکند که میتوانند در آن موارد نادر کمک کنند که واقعاً میخواهید مقدار دقیق یک عدد اعشاری شناور را بدانید. متد float.as_integer_ratio() مقدار یک عدد اعشاری شناور را به صورت کسری بیان میکند:
>>> x = 3.14159 >>> x.as_integer_ratio() (3537115888337719, 1125899906842624)
از آنجا که نسبت دقیق است، میتواند برای بازسازی بدون اتلاف مقدار اصلی استفاده شود:
>>> x == 3537115888337719 / 1125899906842624 True
متد float.hex() یک عدد اعشاری شناور را به صورت هگزادسیمال (مبنای ۱۶) بیان میکند و مجدداً مقدار دقیق ذخیره شده توسط رایانه شما را ارائه میدهد:
>>> x.hex() '0x1.921f9f01b866ep+1'
این نمایش هگزادسیمال دقیق میتواند برای بازسازی دقیق مقدار عدد اعشاری شناور استفاده شود:
>>> x == float.fromhex('0x1.921f9f01b866ep+1')
True
از آنجا که نمایش دقیق است، برای انتقال مطمئن مقادیر بین نسخههای مختلف پایتون (استقلال پلتفرم) و تبادل داده با زبانهای دیگری که از همان قالب پشتیبانی میکنند (مانند Java و C99) مفید است.
یک ابزار مفید دیگر تابع sum() است که به کاهش از دست دادن دقت در حین جمعبندی کمک میکند. این از دقت گسترشیافته برای مراحل گرد کردن میانی استفاده میکند وقتی مقادیر به مجموع در حال اجرا اضافه میشوند. این میتواند تفاوتی در دقت کلی ایجاد کند تا خطاها تا حدی که بر مجموع نهایی تأثیر بگذارند انباشته نشوند:
>>> 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 == 1.0 False >>> sum([0.1] * 10) == 1.0 True
math.fsum() فراتر میرود و تمام «رقمهای از دست رفته» را ردیابی میکند وقتی مقادیر به مجموع در حال اجرا اضافه میشوند تا نتیجه فقط یک گرد کردن داشته باشد. این از sum() کندتر است اما در موارد غیررایجی که ورودیهای با بزرگی زیاد عمدتاً یکدیگر را خنثی میکنند و مجموع نهایی نزدیک صفر میشود دقیقتر است:
>>> arr = [-0.10430216751806065, -266310978.67179024, 143401161448607.16, ... -143401161400469.7, 266262841.31058735, -0.003244936839808227] >>> float(sum(map(Fraction, arr))) # جمعبندی دقیق با گرد کردن تکی 8.042173697819788e-13 >>> math.fsum(arr) # گرد کردن تکی 8.042173697819788e-13 >>> sum(arr) # گرد کردنهای متعدد در دقت گسترشیافته 8.042178034628478e-13 >>> total = 0.0 >>> for x in arr: ... total += x # گرد کردنهای متعدد در دقت استاندارد ... >>> total # جمع مستقیم هیچ رقم صحیحی ندارد! -0.0051575902860057365
این بخش مثال "۰.۱" را به طور دقیق توضیح میدهد و نشان میدهد چگونه میتوانید تحلیل دقیق مواردی مانند این را خودتان انجام دهید. آشنایی پایه با نمایش اعداد اعشاری شناور باینری فرض شده است.
خطای نمایش به این واقعیت اشاره دارد که برخی (در واقع بیشتر) کسرهای اعشاری نمیتوانند دقیقاً به صورت کسرهای باینری (مبنای ۲) نمایش داده شوند. این دلیل اصلی است که پایتون (یا Perl، C، C++، Java، Fortran و بسیاری دیگر) اغلب مقدار اعشاری دقیقی که انتظار دارید را نمایش نمیدهد.
چرا؟ ۱/۱۰ دقیقاً به صورت کسری باینری قابل نمایش نیست. از آنجا که از سال ۲۰۰۰ تقریباً تمام ماشینها از حساب اعداد اعشاری شناور باینری IEEE 754 استفاده میکنند و تقریباً تمام پلتفرمها اعداد اعشاری شناور پایتون را به مقادیر IEEE 754 binary64 "دو برابر دقت" نگاشت میکنند. مقادیر IEEE 754 binary64 حاوی ۵۳ بیت دقت هستند، بنابراین در ورودی رایانه تلاش میکند ۰.۱ را به نزدیکترین کسری که میتواند از قالب J/2**N تبدیل کند که در آن J عدد صحیحی حاوی دقیقاً ۵۳ بیت است. بازنویسی:
1 / 10 ~= J / (2**N)
به صورت
J ~= 2**N / 10
و به یاد آوردن اینکه J دقیقاً ۵۳ بیت دارد (>= 2**52 اما < 2**53)، بهترین مقدار برای N برابر ۵۶ است:
>>> 2**52 <= 2**56 // 10 < 2**53 True
یعنی ۵۶ تنها مقداری است برای N که J را با دقیقاً ۵۳ بیت باقی میگذارد. بهترین مقدار ممکن برای J سپس آن خارجقسمت گرد شده است:
>>> q, r = divmod(2**56, 10) >>> r 6
از آنجا که باقیمانده بیش از نصف ۱۰ است، بهترین تقریب با گرد کردن به بالا به دست میآید:
>>> q+1 7205759403792794
بنابراین بهترین تقریب ممکن برای ۱/۱۰ در IEEE 754 دقت دوگانه این است:
7205759403792794 / 2 ** 56
تقسیم صورتکسر و مخرج بر دو کسر را کاهش میدهد به:
3602879701896397 / 2 ** 55
توجه کنید که از آنجا که گرد کردن به بالا انجام دادیم، این در واقع کمی بزرگتر از ۱/۱۰ است؛ اگر به بالا گرد نکرده بودیم، خارجقسمت کمی کوچکتر از ۱/۱۰ میبود. اما در هیچ حالتی نمیتواند دقیقاً ۱/۱۰ باشد!
پس رایانه هرگز ۱/۱۰ را «نمیبیند»: آنچه میبیند کسر دقیق بالا است، بهترین تقریب دوگانه IEEE 754 که میتواند بگیرد:
>>> 0.1 * 2 ** 55 3602879701896397.0
اگر آن کسر را در ۱۰**۵۵ ضرب کنیم، میتوانیم مقدار را تا ۵۵ رقم اعشاری ببینیم:
>>> 3602879701896397 * 10 ** 55 // 2 ** 55 1000000000000000055511151231257827021181583404541015625
یعنی مقدار دقیق ذخیره شده در رایانه برابر مقدار اعشاری ۰.۱۰۰۰۰۰۰۰۰۰۰۰۰۰۰۵۵۵۱۱۱۵۱۲۳۱۲۵۷۸۲۷۰۲۱۱۸۱۵۸۳۴۰۴۵۴۱۰۱۵۶۲۵ است. به جای نمایش مقدار اعشاری کامل، بسیاری از زبانها (شامل نسخههای قدیمیتر پایتون) نتیجه را به ۱۷ رقم معنادار گرد میکنند:
>>> format(0.1, '.17f') '0.10000000000000001'
ماژولهای fractions و decimal این محاسبات را آسان میکنند:
>>> from decimal import Decimal
>>> from fractions import Fraction
>>> Fraction.from_float(0.1)
Fraction(3602879701896397, 36028797018963968)
>>> (0.1).as_integer_ratio()
(3602879701896397, 36028797018963968)
>>> Decimal.from_float(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')
>>> format(Decimal.from_float(0.1), '.17')
'0.10000000000000001'این محتوا کاملا رایگان توسط تیم کدلپر ترجمه شده و در اختیار شما کاربران عزیز قرار گرفته است، هر گونه کپی برداری برای مقاصد غیر رایگان و بدون ذکر منبع، مورد پیگیری قانونی قرار میگیرد.
ترجمه شده از منبع: https://docs.python.org/3/tutorial