15. ਫਲੋਟਿੰਗ-ਪੌਇੰਟ ਅੰਕਗਣਿਤ: ਸਮੱਸਿਆਵਾਂ ਅਤੇ ਸੀਮਾਵਾਂ

ਫਲੋਟਿੰਗ-ਪੁਆਇੰਟ ਨੰਬਰ ਕੰਪਿਊਟਰ ਹਾਰਡਵੇਅਰ ਵਿੱਚ ਬੇਸ 2 (ਬਾਈਨਰੀ) ਫ੍ਰੈਕਸ਼ਨਾਂ ਵਜੋਂ ਦਰਸਾਏ ਜਾਂਦੇ ਹਨ। ਉਦਾਹਰਨ ਵਜੋਂ, ਦਸ਼ਮਲ ਫ੍ਰੈਕਸ਼ਨ 0.625 ਦੀ ਕੀਮਤ 6/10 + 2/100 + 5/1000 ਹੈ, ਅਤੇ ਇਸੇ ਤਰ੍ਹਾਂ ਬਾਈਨਰੀ ਫ੍ਰੈਕਸ਼ਨ 0.101 ਦੀ ਕੀਮਤ 1/2 + 0/4 + 1/8 ਹੈ। ਇਹਨਾਂ ਦੋਹਾਂ ਭਗਾਂ ਦੇ ਮੁੱਲ ਇੱਕੋ ਜਿਹੇ ਹਨ, ਅਸਲ ਅੰਤਰ ਸਿਰਫ ਇਹ ਹੈ ਕਿ ਪਹਿਲਾ ਦਸਾਂਸ਼ੀ ਭਗ ਸੰਕੇਤਨ ਵਿੱਚ ਲਿਖਿਆ ਗਿਆ ਹੈ, ਅਤੇ ਦੂਜਾ ਦੋਹਾਂਸ਼ੀ ਵਿੱਚ।

ਦੁਰਭਾਗਵਸ਼, ਜ਼ਿਆਦਾਤਰ decimal ਭਿੰਨਾਂ ਨੂੰ binary ਭਿੰਨਾਂ ਵਜੋਂ ਬਿਲਕੁਲ ਸਹੀ ਤਰੀਕੇ ਨਾਲ ਦਰਸਾਇਆ ਨਹੀਂ ਜਾ ਸਕਦਾ। ਇਸਦਾ ਨਤੀਜਾ ਇਹ ਹੈ ਕਿ ਆਮ ਤੌਰ 'ਤੇ ਤੁਹਾਡੇ ਦੁਆਰਾ ਦਿੱਤੀਆਂ decimal floating-point ਸੰਖਿਆਵਾਂ ਨੂੰ ਮਸ਼ੀਨ ਵਿੱਚ ਸਟੋਰ ਕੀਤੀਆਂ binary floating-point ਸੰਖਿਆਵਾਂ ਸਿਰਫ਼ ਲਗਭਗ ਦਰਸਾਉਂਦੀਆਂ ਹਨ।

ਇਸ ਸਮੱਸਿਆ ਨੂੰ ਪਹਿਲਾਂ base 10 ਵਿੱਚ ਸਮਝਣਾ ਆਸਾਨ ਹੈ। 1/3 ਭਿੰਨ ਨੂੰ ਵੇਖੋ। ਤੁਸੀਂ ਇਸਨੂੰ base 10 ਭਿੰਨ ਵਜੋਂ ਲਗਭਗ ਇਸ ਤਰ੍ਹਾਂ ਲਿਖ ਸਕਦੇ ਹੋ:

0.3

ਜਾਂ ਹੋਰ ਵਧੀਆ,

0.33

ਜਾਂ ਹੋਰ ਵਧੀਆ,

0.333

ਅਤੇ ਇਸੇ ਤਰ੍ਹਾਂ ਅੱਗੇ। ਤੁਸੀਂ ਚਾਹੇ ਕਿੰਨੇ ਵੀ ਅੰਕ ਲਿਖੋ, ਨਤੀਜਾ ਕਦੇ ਵੀ ਬਿਲਕੁਲ 1/3 ਨਹੀਂ ਹੋਵੇਗਾ, ਪਰ 1/3 ਦੇ ਹੋਰ ਨੇੜੇ ਅਤੇ ਵਧੀਆ ਅਨੁਮਾਨ ਵਜੋਂ ਮਿਲੇਗਾ।

ਇਸੇ ਤਰ੍ਹਾਂ, ਤੁਸੀਂ base 2 ਦੇ ਚਾਹੇ ਕਿੰਨੇ ਵੀ ਅੰਕ ਵਰਤੋ, decimal ਮੁੱਲ 0.1 ਨੂੰ base 2 ਭਿੰਨ ਵਜੋਂ ਬਿਲਕੁਲ ਸਹੀ ਤਰੀਕੇ ਨਾਲ ਦਰਸਾਇਆ ਨਹੀਂ ਜਾ ਸਕਦਾ। Base 2 ਵਿੱਚ 1/10 ਇੱਕ ਅਨੰਤ ਦੁਹਰਾਈ ਜਾਣ ਵਾਲੀ ਭਿੰਨ ਹੈ:

0.0001100110011001100110011001100110011001100110011...

ਜੇ ਤੁਸੀਂ ਕਿਸੇ ਸੀਮਿਤ ਗਿਣਤੀ ਦੇ bits 'ਤੇ ਰੁਕਦੇ ਹੋ, ਤਾਂ ਤੁਹਾਨੂੰ ਇੱਕ ਅਨੁਮਾਨਿਤ ਮੁੱਲ ਮਿਲਦਾ ਹੈ। ਅੱਜਕੱਲ੍ਹ ਜ਼ਿਆਦਾਤਰ ਮਸ਼ੀਨਾਂ ਵਿੱਚ floats ਨੂੰ ਇੱਕ binary ਭਿੰਨ ਰਾਹੀਂ ਦਰਸਾਇਆ ਜਾਂਦਾ ਹੈ ਜਿਸਦਾ numerator ਸਭ ਤੋਂ ਮਹੱਤਵਪੂਰਨ bit ਤੋਂ ਸ਼ੁਰੂ ਹੋਣ ਵਾਲੇ ਪਹਿਲੇ 53 bits ਵਰਤਦਾ ਹੈ ਅਤੇ denominator ਦੋ ਦੀ ਘਾਤ ਹੁੰਦੀ ਹੈ। 1/10 ਦੇ ਮਾਮਲੇ ਵਿੱਚ binary ਭਿੰਨ 3602879701896397 / 2 ** 55 ਹੁੰਦੀ ਹੈ, ਜੋ 1/10 ਦੇ ਅਸਲੀ ਮੁੱਲ ਦੇ ਬਹੁਤ ਨੇੜੇ ਹੈ ਪਰ ਉਸਦੇ ਬਰਾਬਰ ਨਹੀਂ।

ਕਈ ਉਪਭੋਗਤਾ ਇਸ ਅਨੁਮਾਨਿਤ ਮੁੱਲ ਬਾਰੇ ਜਾਣੂ ਨਹੀਂ ਹੁੰਦੇ ਕਿਉਂਕਿ ਮੁੱਲ ਦਿਖਾਉਣ ਦਾ ਤਰੀਕਾ ਅਜਿਹਾ ਹੁੰਦਾ ਹੈ। Python ਸਿਰਫ਼ ਮਸ਼ੀਨ ਵਿੱਚ ਸਟੋਰ ਕੀਤੇ binary approximation ਦੇ ਅਸਲ decimal ਮੁੱਲ ਦਾ ਇੱਕ decimal ਅਨੁਮਾਨ ਦਿਖਾਉਂਦਾ ਹੈ। ਜੇ Python 0.1 ਲਈ ਸਟੋਰ ਕੀਤੇ binary approximation ਦਾ ਅਸਲ decimal ਮੁੱਲ ਦਿਖਾਏ, ਤਾਂ ਇਸਨੂੰ ਇਹ ਦਿਖਾਉਣਾ ਪਵੇਗਾ:

>>> 0.1
0.1000000000000000055511151231257827021181583404541015625

ਇਹ ਜ਼ਿਆਦਾਤਰ ਲੋਕਾਂ ਲਈ ਲੋੜ ਨਾਲੋਂ ਵੱਧ ਅੰਕ ਹਨ, ਇਸ ਲਈ Python ਗੋਲ ਕੀਤਾ ਹੋਇਆ ਮੁੱਲ ਦਿਖਾ ਕੇ ਅੰਕਾਂ ਦੀ ਗਿਣਤੀ ਸੰਭਾਲਣਯੋਗ ਰੱਖਦਾ ਹੈ:

>>> 1 / 10
0.1

ਸਿਰਫ਼ ਇਹ ਯਾਦ ਰੱਖੋ ਕਿ ਭਾਵੇਂ ਦਿਖਾਇਆ ਗਿਆ ਨਤੀਜਾ 1/10 ਦੇ ਬਿਲਕੁਲ ਸਹੀ ਮੁੱਲ ਵਰਗਾ ਲੱਗਦਾ ਹੈ, ਪਰ ਅਸਲ ਵਿੱਚ ਸਟੋਰ ਕੀਤਾ ਗਿਆ ਮੁੱਲ ਸਭ ਤੋਂ ਨੇੜਲਾ ਦਰਸਾਇਆ ਜਾ ਸਕਣ ਵਾਲਾ binary ਭਿੰਨ ਹੁੰਦਾ ਹੈ।

ਦਿਲਚਸਪ ਗੱਲ ਇਹ ਹੈ ਕਿ ਕਈ ਵੱਖ-ਵੱਖ decimal ਸੰਖਿਆਵਾਂ ਇੱਕੋ ਹੀ ਨੇੜਲੇ binary approximation ਨੂੰ ਸਾਂਝਾ ਕਰਦੀਆਂ ਹਨ। ਉਦਾਹਰਨ ਲਈ, 0.1, 0.10000000000000001 ਅਤੇ 0.1000000000000000055511151231257827021181583404541015625 ਸਭ ਦਾ approximation 3602879701896397 / 2 ** 55 ਹੈ। ਕਿਉਂਕਿ ਇਹ ਸਾਰੇ decimal ਮੁੱਲ ਇੱਕੋ approximation ਸਾਂਝਾ ਕਰਦੇ ਹਨ, ਇਸ ਲਈ ਇਨ੍ਹਾਂ ਵਿੱਚੋਂ ਕੋਈ ਵੀ ਦਿਖਾਇਆ ਜਾ ਸਕਦਾ ਹੈ ਅਤੇ ਫਿਰ ਵੀ eval(repr(x)) == x ਸਹੀ ਰਹੇਗਾ।

ਇਤਿਹਾਸਕ ਤੌਰ 'ਤੇ, Python prompt ਅਤੇ built-in repr() ਫੰਕਸ਼ਨ 17 significant digits ਵਾਲਾ ਮੁੱਲ 0.10000000000000001 ਚੁਣਦੇ ਸਨ। Python 3.1 ਤੋਂ ਬਾਅਦ, Python (ਜ਼ਿਆਦਾਤਰ ਸਿਸਟਮਾਂ 'ਤੇ) ਇਨ੍ਹਾਂ ਵਿੱਚੋਂ ਸਭ ਤੋਂ ਛੋਟਾ ਰੂਪ ਚੁਣ ਸਕਦਾ ਹੈ ਅਤੇ ਸਿਰਫ਼ 0.1 ਦਿਖਾਉਂਦਾ ਹੈ।

ਧਿਆਨ ਦਿਓ ਕਿ ਇਹ binary floating point ਦੀ ਮੂਲ ਪ੍ਰਕਿਰਤੀ ਹੈ। ਇਹ ਨਾ ਤਾਂ Python ਵਿੱਚ bug ਹੈ ਅਤੇ ਨਾ ਹੀ ਤੁਹਾਡੇ code ਵਿੱਚ। ਤੁਸੀਂ ਇਹੋ ਜਿਹੀ ਗੱਲ ਹਰ ਉਸ ਭਾਸ਼ਾ ਵਿੱਚ ਵੇਖੋਗੇ ਜੋ ਤੁਹਾਡੇ hardware ਦੀ floating-point arithmetic ਨੂੰ ਸਮਰਥਨ ਦਿੰਦੀ ਹੈ (ਹਾਲਾਂਕਿ ਕੁਝ ਭਾਸ਼ਾਵਾਂ ਡਿਫਾਲਟ ਤੌਰ 'ਤੇ ਇਹ ਫਰਕ ਨਹੀਂ ਦਿਖਾਉਂਦੀਆਂ)।

ਹੋਰ ਸੁਗਮ output ਲਈ ਤੁਸੀਂ string formatting ਦੀ ਵਰਤੋਂ ਕਰਕੇ significant digits ਦੀ ਸੀਮਿਤ ਗਿਣਤੀ ਦਿਖਾ ਸਕਦੇ ਹੋ:

>>> format(math.pi, '.12g')  # give 12 significant digits
'3.14159265359'

>>> format(math.pi, '.2f')   # give 2 digits after the point
'3.14'

>>> repr(math.pi)
'3.141592653589793'

ਇਹ ਸਮਝਣਾ ਮਹੱਤਵਪੂਰਨ ਹੈ ਕਿ ਇੱਕ ਅਰਥ ਵਿੱਚ ਇਹ ਸਿਰਫ਼ ਇੱਕ ਭਰਮ ਹੈ: ਤੁਸੀਂ ਅਸਲ ਮਸ਼ੀਨੀ ਮੁੱਲ ਦੇ display ਨੂੰ ਹੀ round ਕਰ ਰਹੇ ਹੋ।

ਇੱਕ ਭਰਮ ਦੂਜੇ ਭਰਮ ਨੂੰ ਜਨਮ ਦੇ ਸਕਦਾ ਹੈ। ਉਦਾਹਰਨ ਲਈ, ਕਿਉਂਕਿ 0.1 ਬਿਲਕੁਲ 1/10 ਨਹੀਂ ਹੈ, ਇਸ ਲਈ 0.1 ਦੇ ਤਿੰਨ ਮੁੱਲ ਜੋੜਨ ਨਾਲ ਵੀ ਬਿਲਕੁਲ 0.3 ਨਹੀਂ ਮਿਲ ਸਕਦਾ:

>>> 0.1 + 0.1 + 0.1 == 0.3
False

ਇਸ ਤੋਂ ਇਲਾਵਾ, ਕਿਉਂਕਿ 0.1 ਨੂੰ 1/10 ਦੇ ਅਸਲ ਮੁੱਲ ਦੇ ਹੋਰ ਨੇੜੇ ਨਹੀਂ ਲਿਆਂਦਾ ਜਾ ਸਕਦਾ ਅਤੇ 0.3 ਨੂੰ ਵੀ 3/10 ਦੇ ਅਸਲ ਮੁੱਲ ਦੇ ਹੋਰ ਨੇੜੇ ਨਹੀਂ ਲਿਆਂਦਾ ਜਾ ਸਕਦਾ, ਇਸ ਲਈ round() ਫੰਕਸ਼ਨ ਨਾਲ ਪਹਿਲਾਂ ਤੋਂ 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

ਬਾਈਨਰੀ ਫਲੋਟਿੰਗ-ਪੁਆਇੰਟ ਗਣਿਤ ਵਿੱਚ ਇਸ ਤਰ੍ਹਾਂ ਦੀਆਂ ਬਹੁਤ ਸਾਰੀਆਂ ਹੈਰਾਨੀਆਂ ਹੁੰਦੀਆਂ ਹਨ। "0.1" ਨਾਲ ਸੰਬੰਧਿਤ ਸਮੱਸਿਆ ਹੇਠਾਂ "ਪ੍ਰਤੀਨਿਧਿਤਾ ਭੁੱਲ" ਸੈਕਸ਼ਨ ਵਿੱਚ ਬਿਲਕੁਲ ਵਿਸਥਾਰ ਨਾਲ ਸਮਝਾਈ ਗਈ ਹੈ। ਬਾਈਨਰੀ ਫਲੋਟਿੰਗ-ਪੁਆਇੰਟ ਕਿਵੇਂ ਕੰਮ ਕਰਦਾ ਹੈ ਅਤੇ ਅਮਲੀ ਤੌਰ 'ਤੇ ਆਮ ਤੌਰ 'ਤੇ ਆਉਣ ਵਾਲੀਆਂ ਸਮੱਸਿਆਵਾਂ ਦਾ ਇੱਕ ਸੁਹਣਾ ਸੰਖੇਪ ਜਾਣਨ ਲਈ Examples of Floating Point Problems ` ਵੇਖੋ। ਹੋਰ ਆਮ ਹੈਰਾਨੀਆਂ ਬਾਰੇ ਵਧੇਰੇ ਪੂਰੀ ਜਾਣਕਾਰੀ ਲਈ The Perils of Floating Point ਵੀ ਵੇਖੋ।

ਜਿਵੇਂ ਉੱਥੇ ਅੰਤ ਵਿੱਚ ਕਿਹਾ ਗਿਆ ਹੈ, "ਇਸਦਾ ਕੋਈ ਆਸਾਨ ਜਵਾਬ ਨਹੀਂ ਹੈ।" ਫਿਰ ਵੀ floating point ਤੋਂ ਬੇਵਜ੍ਹਾ ਡਰਣ ਦੀ ਲੋੜ ਨਹੀਂ ਹੈ। Python ਵਿੱਚ float operations ਦੀਆਂ ਗਲਤੀਆਂ floating-point hardware ਤੋਂ ਆਉਂਦੀਆਂ ਹਨ ਅਤੇ ਜ਼ਿਆਦਾਤਰ ਮਸ਼ੀਨਾਂ 'ਤੇ ਹਰ operation ਲਈ ਇਹ 2**53 ਵਿੱਚੋਂ ਲਗਭਗ 1 ਹਿੱਸੇ ਤੋਂ ਵੱਧ ਨਹੀਂ ਹੁੰਦੀਆਂ। ਇਹ ਜ਼ਿਆਦਾਤਰ ਕੰਮਾਂ ਲਈ ਕਾਫ਼ੀ ਹੈ, ਪਰ ਇਹ ਯਾਦ ਰੱਖਣਾ ਜ਼ਰੂਰੀ ਹੈ ਕਿ ਇਹ decimal arithmetic ਨਹੀਂ ਹੈ ਅਤੇ ਹਰ float operation ਇੱਕ ਨਵੀਂ rounding error ਪੈਦਾ ਕਰ ਸਕਦੀ ਹੈ।

ਭਾਵੇਂ ਕੁਝ ਵਿਸ਼ੇਸ਼ ਅਤੇ ਜਟਿਲ ਮਾਮਲੇ ਮੌਜੂਦ ਹਨ, ਪਰ floating-point arithmetic ਦੀ ਆਮ ਵਰਤੋਂ ਵਿੱਚ ਤੁਸੀਂ ਆਮ ਤੌਰ 'ਤੇ ਉਹੀ ਨਤੀਜਾ ਵੇਖੋਗੇ ਜਿਸਦੀ ਉਮੀਦ ਕਰਦੇ ਹੋ, ਜੇ ਤੁਸੀਂ ਆਪਣੇ ਅੰਤਿਮ ਨਤੀਜੇ ਨੂੰ ਲੋੜੀਂਦੇ decimal ਅੰਕਾਂ ਤੱਕ round ਕਰ ਦਿਓ। str() ਆਮ ਤੌਰ 'ਤੇ ਕਾਫ਼ੀ ਹੁੰਦਾ ਹੈ ਅਤੇ ਹੋਰ ਨਿਯੰਤਰਣ ਲਈ Format string syntax ਵਿੱਚ str.format() ਦੇ format specifiers ਵੇਖੋ।

ਉਹਨਾਂ ਮਾਮਲਿਆਂ ਲਈ ਜਿੱਥੇ ਬਿਲਕੁਲ ਸਹੀ decimal representation ਦੀ ਲੋੜ ਹੁੰਦੀ ਹੈ, decimal ਮੋਡੀਊਲ ਦੀ ਵਰਤੋਂ ਕਰੋ, ਜੋ accounting ਅਤੇ high-precision ਐਪਲੀਕੇਸ਼ਨਾਂ ਲਈ ਉਚਿਤ decimal arithmetic ਪ੍ਰਦਾਨ ਕਰਦਾ ਹੈ।

ਸਹੀ arithmetic ਦਾ ਇੱਕ ਹੋਰ ਰੂਪ fractions ਮੋਡੀਊਲ ਦੁਆਰਾ ਪ੍ਰਦਾਨ ਕੀਤਾ ਜਾਂਦਾ ਹੈ, ਜੋ rational numbers 'ਤੇ ਆਧਾਰਿਤ arithmetic ਲਾਗੂ ਕਰਦਾ ਹੈ (ਇਸ ਲਈ 1/3 ਵਰਗੀਆਂ ਸੰਖਿਆਵਾਂ ਨੂੰ ਬਿਲਕੁਲ ਸਹੀ ਤਰੀਕੇ ਨਾਲ ਦਰਸਾਇਆ ਜਾ ਸਕਦਾ ਹੈ)।

ਜੇ ਤੁਸੀਂ floating-point operations ਦੀ ਬਹੁਤ ਜ਼ਿਆਦਾ ਵਰਤੋਂ ਕਰਦੇ ਹੋ, ਤਾਂ ਤੁਹਾਨੂੰ NumPy package ਅਤੇ SciPy project ਦੁਆਰਾ ਪ੍ਰਦਾਨ ਕੀਤੇ ਹੋਰ ਗਣਿਤਕ ਅਤੇ ਅੰਕੜਾ-ਵਿਗਿਆਨਕ packages ਨੂੰ ਵੀ ਵੇਖਣਾ ਚਾਹੀਦਾ ਹੈ। ਵੇਖੋ: <https://scipy.org>.

Python ਉਹਨਾਂ ਵਿਰਲੇ ਮੌਕਿਆਂ ਲਈ ਕੁਝ tools ਪ੍ਰਦਾਨ ਕਰਦਾ ਹੈ ਜਦੋਂ ਤੁਸੀਂ ਸੱਚਮੁੱਚ ਕਿਸੇ float ਦਾ ਬਿਲਕੁਲ ਸਹੀ ਮੁੱਲ ਜਾਣਨਾ ਚਾਹੁੰਦੇ ਹੋ। float.as_integer_ratio() ਮੈਥਡ ਇੱਕ float ਦੇ ਮੁੱਲ ਨੂੰ ਭਿੰਨ ਦੇ ਰੂਪ ਵਿੱਚ ਦਰਸਾਉਂਦੀ ਹੈ:

>>> x = 3.14159
>>> x.as_integer_ratio()
(3537115888337719, 1125899906842624)

ਕਿਉਂਕਿ ਇਹ ਅਨੁਪਾਤ ਬਿਲਕੁਲ ਸਹੀ ਹੈ, ਇਸਦੀ ਵਰਤੋਂ ਮੂਲ ਮੁੱਲ ਨੂੰ ਬਿਨਾਂ ਕਿਸੇ ਜਾਣਕਾਰੀ ਦੇ ਨੁਕਸਾਨ ਦੇ ਮੁੜ ਪ੍ਰਾਪਤ ਕਰਨ ਲਈ ਕੀਤੀ ਜਾ ਸਕਦੀ ਹੈ:

>>> x == 3537115888337719 / 1125899906842624
True

float.hex() ਮੈਥਡ ਇੱਕ float ਨੂੰ hexadecimal (base 16) ਰੂਪ ਵਿੱਚ ਦਰਸਾਉਂਦੀ ਹੈ ਅਤੇ ਮੁੜ ਉਹੀ ਸਹੀ ਮੁੱਲ ਦਿੰਦੀ ਹੈ ਜੋ ਤੁਹਾਡੇ ਕੰਪਿਊਟਰ ਵਿੱਚ ਸਟੋਰ ਹੈ:

>>> x.hex()
'0x1.921f9f01b866ep+1'

ਇਸ ਸਹੀ hexadecimal representation ਦੀ ਵਰਤੋਂ ਕਰਕੇ float ਦਾ ਮੁੱਲ ਬਿਲਕੁਲ ਸਹੀ ਤਰੀਕੇ ਨਾਲ ਮੁੜ ਬਣਾਇਆ ਜਾ ਸਕਦਾ ਹੈ:

>>> x == float.fromhex('0x1.921f9f01b866ep+1')
True

ਕਿਉਂਕਿ ਇਹ representation ਬਿਲਕੁਲ ਸਹੀ ਹੈ, ਇਸ ਲਈ ਇਹ Python ਦੇ ਵੱਖ-ਵੱਖ ਸੰਸਕਰਣਾਂ (platform independence) ਵਿਚਕਾਰ ਮੁੱਲਾਂ ਨੂੰ ਭਰੋਸੇਯੋਗ ਢੰਗ ਨਾਲ ਤਬਦੀਲ ਕਰਨ ਅਤੇ ਉਹਨਾਂ ਹੋਰ ਭਾਸ਼ਾਵਾਂ ਨਾਲ ਡਾਟਾ ਅਦਾਨ-ਪ੍ਰਦਾਨ ਕਰਨ ਲਈ ਲਾਭਦਾਇਕ ਹੈ ਜੋ ਇਹੋ ਜਿਹਾ format ਸਮਰਥਨ ਕਰਦੀਆਂ ਹਨ (ਜਿਵੇਂ Java ਅਤੇ C99)।

ਇੱਕ ਹੋਰ ਲਾਭਦਾਇਕ tool sum() ਫੰਕਸ਼ਨ ਹੈ, ਜੋ ਜੋੜ ਦੇ ਦੌਰਾਨ precision ਦੇ ਨੁਕਸਾਨ ਨੂੰ ਘਟਾਉਣ ਵਿੱਚ ਮਦਦ ਕਰਦਾ ਹੈ। ਜਦੋਂ ਮੁੱਲਾਂ ਨੂੰ ਚੱਲਦੇ ਕੁੱਲ ਵਿੱਚ ਜੋੜਿਆ ਜਾਂਦਾ ਹੈ, ਤਾਂ ਇਹ ਵਿਚਕਾਰਲੇ rounding ਕਦਮਾਂ ਲਈ extended precision ਵਰਤਦਾ ਹੈ। ਇਸ ਨਾਲ ਕੁੱਲ ਸ਼ੁੱਧਤਾ ਵਿੱਚ ਸੁਧਾਰ ਆ ਸਕਦਾ ਹੈ ਅਤੇ ਗਲਤੀਆਂ ਇੰਨੀ ਨਹੀਂ ਵੱਧਦੀਆਂ ਕਿ ਉਹ ਅੰਤਿਮ ਨਤੀਜੇ ਨੂੰ ਪ੍ਰਭਾਵਿਤ ਕਰਨ।

>>> 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() ਇਸ ਤੋਂ ਵੀ ਅੱਗੇ ਜਾਂਦਾ ਹੈ ਅਤੇ ਚੱਲਦੇ ਕੁੱਲ ਵਿੱਚ ਮੁੱਲ ਜੋੜਦੇ ਸਮੇਂ ਸਾਰੇ "lost digits" ਨੂੰ ਟ੍ਰੈਕ ਕਰਦਾ ਹੈ ਤਾਂ ਜੋ ਅੰਤਿਮ ਨਤੀਜੇ ਵਿੱਚ ਸਿਰਫ਼ ਇੱਕ ਹੀ rounding ਹੋਵੇ। ਇਹ sum() ਨਾਲੋਂ ਹੌਲਾ ਹੈ, ਪਰ ਉਹਨਾਂ ਵਿਰਲੇ ਮਾਮਲਿਆਂ ਵਿੱਚ ਵਧੇਰੇ ਸਹੀ ਹੁੰਦਾ ਹੈ ਜਿੱਥੇ ਵੱਡੇ ਮੁੱਲ ਇੱਕ-ਦੂਜੇ ਨੂੰ ਲਗਭਗ ਰੱਦ ਕਰ ਦਿੰਦੇ ਹਨ ਅਤੇ ਅੰਤਿਮ ਜੋੜ ਸਿਫ਼ਰ ਦੇ ਨੇੜੇ ਰਹਿ ਜਾਂਦਾ ਹੈ।

>>> arr = [-0.10430216751806065, -266310978.67179024, 143401161448607.16,
...        -143401161400469.7, 266262841.31058735, -0.003244936839808227]
>>> float(sum(map(Fraction, arr)))   # Exact summation with single rounding
8.042173697819788e-13
>>> math.fsum(arr)                   # Single rounding
8.042173697819788e-13
>>> sum(arr)                         # Multiple roundings in extended precision
8.042178034628478e-13
>>> total = 0.0
>>> for x in arr:
...     total += x                   # Multiple roundings in standard precision
...
>>> total                            # ਸਿੱਧੇ ਜੋੜ ਵਿੱਚ ਕੋਈ ਵੀ ਸਹੀ ਅੰਕ ਨਹੀਂ!
-0.0051575902860057365

15.1. ਪ੍ਰਤਿਨਿਧਿਤਾ ਗਲਤੀ

ਇਹ ਭਾਗ "0.1" ਵਾਲੀ ਉਦਾਹਰਨ ਨੂੰ ਵਿਸਥਾਰ ਨਾਲ ਸਮਝਾਉਂਦਾ ਹੈ ਅਤੇ ਦਿਖਾਉਂਦਾ ਹੈ ਕਿ ਤੁਸੀਂ ਇਸ ਤਰ੍ਹਾਂ ਦੇ ਮਾਮਲਿਆਂ ਦਾ ਸਹੀ ਵਿਸ਼ਲੇਸ਼ਣ ਖੁਦ ਕਿਵੇਂ ਕਰ ਸਕਦੇ ਹੋ। ਇਹ ਮੰਨਿਆ ਗਿਆ ਹੈ ਕਿ ਤੁਹਾਨੂੰ binary floating-point representation ਦੀ ਮੁੱਢਲੀ ਜਾਣਕਾਰੀ ਹੈ।

Representation error ਇਸ ਗੱਲ ਵੱਲ ਇਸ਼ਾਰਾ ਕਰਦਾ ਹੈ ਕਿ ਕੁਝ (ਅਸਲ ਵਿੱਚ ਜ਼ਿਆਦਾਤਰ) decimal ਭਿੰਨਾਂ ਨੂੰ binary (base 2) ਭਿੰਨਾਂ ਵਜੋਂ ਬਿਲਕੁਲ ਸਹੀ ਤਰੀਕੇ ਨਾਲ ਦਰਸਾਇਆ ਨਹੀਂ ਜਾ ਸਕਦਾ। ਇਹ ਮੁੱਖ ਕਾਰਨ ਹੈ ਕਿ Python (ਜਾਂ Perl, C, C++, Java, Fortran ਅਤੇ ਹੋਰ ਕਈ ਭਾਸ਼ਾਵਾਂ) ਅਕਸਰ ਉਹ ਸਹੀ decimal ਸੰਖਿਆ ਨਹੀਂ ਦਿਖਾਉਂਦੀਆਂ ਜਿਸਦੀ ਤੁਸੀਂ ਉਮੀਦ ਕਰਦੇ ਹੋ।

ਅਜਿਹਾ ਕਿਉਂ ਹੈ? 1/10 ਨੂੰ binary ਭਿੰਨ ਵਜੋਂ ਬਿਲਕੁਲ ਸਹੀ ਤਰੀਕੇ ਨਾਲ ਦਰਸਾਇਆ ਨਹੀਂ ਜਾ ਸਕਦਾ। 2000 ਤੋਂ ਬਾਅਦ ਲਗਭਗ ਸਾਰੀਆਂ ਮਸ਼ੀਨਾਂ IEEE 754 binary floating-point arithmetic ਵਰਤਦੀਆਂ ਹਨ ਅਤੇ ਲਗਭਗ ਸਾਰੇ platforms Python floats ਨੂੰ IEEE 754 binary64 "double precision" ਮੁੱਲਾਂ ਨਾਲ ਮੈਪ ਕਰਦੇ ਹਨ। IEEE 754 binary64 ਮੁੱਲਾਂ ਵਿੱਚ 53 bits ਦੀ precision ਹੁੰਦੀ ਹੈ, ਇਸ ਲਈ ਕੰਪਿਊਟਰ 0.1 ਨੂੰ J/2**N ਦੇ ਰੂਪ ਵਾਲੀ ਸਭ ਤੋਂ ਨੇੜਲੀ ਭਿੰਨ ਵਿੱਚ ਬਦਲਣ ਦੀ ਕੋਸ਼ਿਸ਼ ਕਰਦਾ ਹੈ, ਜਿੱਥੇ J ਇੱਕ ਪੂਰਨ ਅੰਕ ਹੈ ਜਿਸ ਵਿੱਚ ਬਿਲਕੁਲ 53 bits ਹੁੰਦੀਆਂ ਹਨ। ਇਸਨੂੰ ਮੁੜ ਲਿਖਦੇ ਹਾਂ

1 / 10 ~= J / (2**N)

ਇਸ ਤਰ੍ਹਾਂ

J ~= 2**N / 10

ਅਤੇ ਇਹ ਯਾਦ ਰੱਖਦੇ ਹੋਏ ਕਿ J ਵਿੱਚ ਬਿਲਕੁਲ 53 bits ਹਨ (ਅਰਥਾਤ >= 2**52 ਪਰ < 2**53), N ਲਈ ਸਭ ਤੋਂ ਵਧੀਆ ਮੁੱਲ 56 ਹੈ:

>>> 2**52 <=  2**56 // 10  < 2**53
True

ਅਰਥਾਤ, 56 ਹੀ N ਦਾ ਉਹ ਇਕੱਲਾ ਮੁੱਲ ਹੈ ਜੋ J ਨੂੰ ਬਿਲਕੁਲ 53 bits ਨਾਲ ਛੱਡਦਾ ਹੈ। ਇਸ ਤੋਂ ਬਾਅਦ J ਲਈ ਸਭ ਤੋਂ ਵਧੀਆ ਸੰਭਵ ਮੁੱਲ ਉਹ ਭਾਗਫਲ ਹੁੰਦਾ ਹੈ ਜਿਸਨੂੰ round ਕੀਤਾ ਗਿਆ ਹੋਵੇ:

>>> q, r = divmod(2**56, 10)
>>> r
6

ਕਿਉਂਕਿ ਬਾਕੀ ਰਹਿੰਦਾ ਭਾਗ 10 ਦੇ ਅੱਧ ਤੋਂ ਵੱਧ ਹੈ, ਇਸ ਲਈ ਸਭ ਤੋਂ ਵਧੀਆ approximation ਉੱਪਰ ਵੱਲ round ਕਰਕੇ ਪ੍ਰਾਪਤ ਹੁੰਦਾ ਹੈ:

>>> q+1
7205759403792794

ਇਸ ਲਈ IEEE 754 double precision ਵਿੱਚ 1/10 ਦਾ ਸਭ ਤੋਂ ਵਧੀਆ ਸੰਭਵ approximation ਇਹ ਹੈ:

7205759403792794 / 2 ** 56

Numerator ਅਤੇ denominator ਦੋਹਾਂ ਨੂੰ ਦੋ ਨਾਲ ਭਾਗ ਦੇਣ 'ਤੇ ਇਹ ਭਿੰਨ ਘਟ ਕੇ ਇਸ ਰੂਪ ਵਿੱਚ ਆ ਜਾਂਦੀ ਹੈ:

3602879701896397 / 2 ** 55

ਧਿਆਨ ਦਿਓ ਕਿ ਕਿਉਂਕਿ ਅਸੀਂ round up ਕੀਤਾ ਹੈ, ਇਸ ਲਈ ਇਹ ਅਸਲ ਵਿੱਚ 1/10 ਨਾਲੋਂ ਥੋੜ੍ਹਾ ਵੱਡਾ ਹੈ। ਜੇ ਅਸੀਂ round up ਨਾ ਕੀਤਾ ਹੁੰਦਾ, ਤਾਂ ਭਾਗਫਲ 1/10 ਨਾਲੋਂ ਥੋੜ੍ਹਾ ਛੋਟਾ ਹੁੰਦਾ। ਪਰ ਕਿਸੇ ਵੀ ਹਾਲਤ ਵਿੱਚ ਇਹ ਬਿਲਕੁਲ 1/10 ਨਹੀਂ ਬਣ ਸਕਦਾ!

ਇਸ ਲਈ ਕੰਪਿਊਟਰ ਕਦੇ ਵੀ ਅਸਲ 1/10 ਨੂੰ "ਨਹੀਂ ਵੇਖਦਾ"। ਇਹ ਉੱਪਰ ਦਿੱਤੀ ਸਹੀ ਭਿੰਨ ਨੂੰ ਹੀ ਵੇਖਦਾ ਹੈ, ਜੋ IEEE 754 double precision ਵਿੱਚ ਮਿਲ ਸਕਣ ਵਾਲਾ ਸਭ ਤੋਂ ਵਧੀਆ approximation ਹੈ:

>>> 0.1 * 2 ** 55
3602879701896397.0

ਜੇ ਅਸੀਂ ਉਸ ਭਿੰਨ ਨੂੰ 10**55 ਨਾਲ ਗੁਣਾ ਕਰੀਏ, ਤਾਂ ਅਸੀਂ ਉਸਦਾ ਮੁੱਲ 55 decimal ਅੰਕਾਂ ਤੱਕ ਵੇਖ ਸਕਦੇ ਹਾਂ:

>>> 3602879701896397 * 10 ** 55 // 2 ** 55
1000000000000000055511151231257827021181583404541015625

ਇਸਦਾ ਅਰਥ ਹੈ ਕਿ ਕੰਪਿਊਟਰ ਵਿੱਚ ਸਟੋਰ ਕੀਤੀ ਗਈ ਸਹੀ ਸੰਖਿਆ decimal ਮੁੱਲ 0.1000000000000000055511151231257827021181583404541015625 ਦੇ ਬਰਾਬਰ ਹੈ। ਪੂਰਾ decimal ਮੁੱਲ ਦਿਖਾਉਣ ਦੀ ਬਜਾਏ, ਕਈ ਭਾਸ਼ਾਵਾਂ (Python ਦੇ ਪੁਰਾਣੇ ਸੰਸਕਰਣਾਂ ਸਮੇਤ) ਨਤੀਜੇ ਨੂੰ 17 significant digits ਤੱਕ round ਕਰ ਦਿੰਦੀਆਂ ਹਨ:

>>> 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'