সরাসরি পর্বে যান
পর্ব ০৭ · ৫৫-এর মধ্যে · মডিউল ২

সংখ্যা ও হিসাব — int, float-এর ফাঁদ আর Decimal

Numbers & Arithmetic — int, Float Pitfalls and Decimal

Taught in Bangla, this lesson covers numbers in Python: how bits store int and float, arbitrary-size integers, true versus floor division, why 0.1 + 0.2 is not 0.3, comparing and rounding floats, handling money with Decimal and quantize, and useful tools from the math module.

এই পর্বে যা শিখবেন

  • কম্পিউটার সংখ্যা কীভাবে binary-তে রাখে, আর int ও float-এর রাখার ধরন কেন সম্পূর্ণ আলাদা
  • সাতটি গাণিতিক অপারেটর, / বনাম //, ঋণাত্মক সংখ্যায় %, divmod() আর অপারেটরের অগ্রাধিকার
  • 0.1 + 0.2 কেন 0.3 নয়, float কীভাবে তুলনা করতে হয়, আর round() কেন “অদ্ভুত” ফল দেয়
  • VAT, বিল ভাগ, মোবাইল ব্যাংকিং চার্জ — টাকার হিসাব Decimal বা পয়সা-int দিয়ে নির্ভুলভাবে করা
  • math, fractions, statistics আর complex সংখ্যার দরকারি অংশ, আর AI-এর লেখা হিসাবের কোড যাচাই

১ · কম্পিউটারে সংখ্যা কীভাবে থাকে — বিট, int আর float

আগের পর্বে দেখেছেন, Python-এ প্রতিটি মান একটি object, আর ভ্যারিয়েবল সেই object-এর গায়ে লাগানো একটি লেবেল। এবার সবচেয়ে বেশি ব্যবহৃত object নিয়ে কাজ — সংখ্যা। প্রায় প্রতিটি প্রোগ্রামেই হিসাব থাকে: দোকানের বিল, পরীক্ষার গড় নম্বর, ব্যাংকের সুদ, সেন্সরের তাপমাত্রা। সংখ্যা দেখতে সহজ, কিন্তু এখানেই লুকিয়ে আছে প্রোগ্রামিংয়ের কিছু সবচেয়ে ব্যয়বহুল ভুল।

শুরু করা যাক একটি মৌলিক সত্য দিয়ে। কম্পিউটারের মেমরি শুধু দুটি অবস্থা চেনে — চালু আর বন্ধ, অর্থাৎ ১ আর ০. প্রতিটি ঘরকে বলে বিট. তাই সব সংখ্যাকে শেষ পর্যন্ত binary (দুই-ভিত্তিক) রূপে লিখতে হয়। দশমিক 13 binary-তে 1101 — অর্থাৎ ৮ + ৪ + ০ + ১. পূর্ণসংখ্যার জন্য এটি একদম নিখুঁত: যত বড় পূর্ণসংখ্যাই হোক, যথেষ্ট বিট পেলে ঠিকঠাক লেখা যায়।

সমস্যা শুরু হয় ভগ্নাংশে। দশমিক পদ্ধতিতেও আমরা ১/৩-কে ঠিকঠাক লিখতে পারি না — 0.3333… চলতেই থাকে, কোথাও থামাতে হয়। Binary-তে ঠিক একই সমস্যা হয়, তবে অন্য সংখ্যায়। দুঃখের বিষয়, সেই সংখ্যাগুলোর মধ্যে আছে আমাদের খুব চেনা 0.1, 0.2, 0.3 — টাকা-পয়সার হিসাবের প্রতিদিনের সংখ্যা।

🔢 int — পূর্ণসংখ্যা

নিখুঁত। দরকার হলে Python নিজেই আরও বিট নিয়ে নেয়, তাই আকারের কোনো নির্দিষ্ট সীমা নেই — সীমা শুধু আপনার কম্পিউটারের মেমরি।

42, -7, 2 ** 100

🌊 float — দশমিক সংখ্যা

সবসময় ঠিক ৬৪ বিট। খুব বড় আর খুব ছোট সংখ্যা ধরতে পারে, কিন্তু বেশিরভাগ দশমিক মান সবচেয়ে কাছের binary মান দিয়ে আনুমানিকভাবে রাখে।

3.14, 0.1, 1e-9

🧾 Decimal — দশমিক, নির্ভুল

Standard library-র একটি type, যা সংখ্যাকে দশ-ভিত্তিক অঙ্কে রাখে। 0.1 ঠিক 0.1 থাকে। একটু ধীর, কিন্তু টাকার জন্য সঠিক পছন্দ।

Decimal("0.10")
মূল ধারণা

int হলো গোনা — কয়টি ডিম, কয়জন যাত্রী, কত পয়সা। float হলো মাপা — দৈর্ঘ্য, ওজন, তাপমাত্রা, যেখানে সামান্য ভুল এমনিতেই মাপার যন্ত্রে থাকে। টাকা “মাপার” জিনিস মনে হলেও আসলে “গোনার” জিনিস — পয়সা গোনা হয়। এই একটি বাক্য মনে রাখলে এই পর্বের বেশিরভাগ ভুল এড়াতে পারবেন।

মুদি দোকানের দাঁড়িপাল্লা আর ক্যাশবাক্সের কথা ভাবুন। দাঁড়িপাল্লায় ১ কেজি চাল মাপলে আসলে ৯৯৮ গ্রাম না ১০০২ গ্রাম, কেউ জানে না — আর তাতে কারো আপত্তিও নেই। এটা float-এর জগৎ। কিন্তু ক্যাশবাক্সে ৫০০ টাকার নোট গুনলে একটি নোটও এদিক-ওদিক হলে চলবে না। এটা int (বা Decimal)-এর জগৎ। ভুল যন্ত্রে ভুল জিনিস মাপলেই ঝামেলা — যেমন দাঁড়িপাল্লায় টাকা “মাপা”।

২ · int — যত বড় চান, ততই বড়

C বা Java-তে একটি int সাধারণত ৩২ বিটের — সবচেয়ে বড় মান প্রায় ২১৪ কোটি। এর বেশি হলে সংখ্যাটি “উল্টে” ঋণাত্মক হয়ে যেতে পারে; একে বলে overflow. বাংলাদেশের জনসংখ্যা ধরা যায়, কিন্তু পুরো পৃথিবীর মানুষের মোট বয়স (দিনে) ধরা যায় না।

Python-এর int এই সমস্যা থেকে মুক্ত। একে বলে arbitrary precision integer: সংখ্যা বড় হলে Python নিজেই আরও মেমরি নিয়ে নেয়। 2 ** 100 লিখলে ৩১ অঙ্কের পুরো সংখ্যা পাবেন, একটি অঙ্কও না হারিয়ে। math.factorial(50) দেয় ৬৫ অঙ্কের একটি নির্ভুল সংখ্যা। RSA-র মতো এনক্রিপশনে কয়েকশো অঙ্কের সংখ্যা নিয়ে কাজ হয় — Python-এ সেটি কোনো বাড়তি library ছাড়াই সম্ভব।

সীমাহীন মানে বিনামূল্যে নয়। বড় int বেশি মেমরি নেয় আর তার ওপর হিসাব ধীর হয়। ছোট সংখ্যার হিসাবও C-র চেয়ে ধীর, কারণ প্রতিটি int একটি পূর্ণ object (পর্ব ০৫). আর একটি নিরাপত্তা-সীমাও আছে: খুব বড় int-কে দশমিক লেখায় (str() বা print) বদলাতে গেলে, ডিফল্টে ৪৩০০ অঙ্কের বেশি হলে ValueError আসে। কারণ ও সমাধান ভাবনার প্রশ্ন ৩-এ।

লেখার কৌশল — underscore আর ভিন্ন ভিত্তি

কোডে সরাসরি লেখা মানকে বলে literal. বড় সংখ্যা পড়তে সুবিধার জন্য Python 3.6 থেকে literal-এর ভেতরে underscore দেওয়া যায়: 1_000_000 আর 1000000 হুবহু একই সংখ্যা। বাংলাদেশি কায়দায় 10_00_000 (দশ লাখ) লিখলেও চলে — Python underscore-এর অবস্থান নিয়ে মাথা ঘামায় না, শুধু পড়ার সুবিধা দেয়।

দশ ছাড়া অন্য ভিত্তিতেও literal লেখা যায়, আর উল্টো দিকে রূপান্তরের জন্য আছে ফাংশন:

ভিত্তিLiteralমানint থেকে লেখায়কোথায় কাজে লাগে
২ (binary)0b101010bin(10) → '0b1010'বিট-ফ্ল্যাগ, হার্ডওয়্যার
৮ (octal)0o1715oct(15) → '0o17'Linux ফাইল permission (0o755)
১৬ (hexadecimal)0xFF255hex(255) → '0xff'রঙ (#FF8800), মেমরি ঠিকানা

মনে রাখুন, ভিত্তি শুধু লেখার ধরন. 0xFF, 0o377, 0b11111111 আর 255 — চারটিই একই int object তৈরি করে। আর bin(), hex() সংখ্যা ফেরত দেয় না, ফেরত দেয় string. উল্টো দিকে string থেকে int চাইলে int("ff", 16) লিখুন।

৩ · হিসাবের অপারেটর — / আর //-এর আসল পার্থক্য

Python-এ সাতটি গাণিতিক operator আছে। প্রথম চারটি স্কুলের অঙ্কের মতো, শেষ তিনটিতে মনোযোগ দিন:

Operatorকাজউদাহরণফল
+ - *যোগ, বিয়োগ, গুণ7 * 214
/true division — আসল ভাগফল7 / 2, 10 / 23.5, 5.0
//floor division — নিচের দিকে গোল করা ভাগফল7 // 23
%modulo — ভাগশেষ7 % 21
**ঘাত (power)2 ** 101024

প্রথম নিয়ম: / সবসময় float দেয়, এমনকি নিঃশেষে বিভাজ্য হলেও। 10 / 2 হলো 5.0, 5 নয়। দুটি int-এ +, -, *, //, % করলে int-ই থাকে; কিন্তু একপাশে float থাকলে ফলও float হয়ে যায় (7 // 2.0 হলো 3.0).

দ্বিতীয় নিয়ম আরও গুরুত্বপূর্ণ: // মানে “ছোট করো” নয়, মানে “নিচের দিকে গোল করো” — সংখ্যারেখায় বাম দিকে, ঋণাত্মক অসীমের দিকে। ধনাত্মক সংখ্যায় দুটো একই দেখায়, তাই পার্থক্য চোখে পড়ে না। ঋণাত্মক সংখ্যায় এলেই চমক। নিচের সংখ্যারেখায় ধাপে ধাপে দেখুন:

7 // 2ধনাত্মক — কোনো চমক নেই
  2     3    3.5    4     5
──┼─────┼─────●─────┼─────┼──
        ▲
   7 / 2 = 3.5  →  নিচের দিকে  →  3
   7 % 2 = 7 - 2×3 = 1
-7 / 2আসল ভাগফল আগে
 -5    -4   -3.5   -3    -2
──┼─────┼─────●─────┼─────┼──
              ▲
   -7 / 2 = -3.5   (দুটি পূর্ণসংখ্যার মাঝে)
-7 // 2নিচের দিকে = বাম দিকে
 -5    -4   -3.5   -3    -2
──┼─────┼─────●─────┼─────┼──
        ◀─────
   Python:  -7 // 2 = -4   (floor, বাম দিকে)
   C:       -7 / 2  = -3   (শূন্যের দিকে কাটা)
-7 % 2যে নিয়ম সবসময় সত্য
   a == b * (a // b) + (a % b)

   -7 == 2 * (-4) + (a % b)
   -7 == -8 + 1

   তাই  -7 % 2 = 1     divmod(-7, 2) = (-4, 1)
   C-তে -7 % 2 = -1
  1. ধনাত্মক সংখ্যায় দুটি একই। 7 ÷ 2 = 3.5. নিচের দিকে গোল করলে 3, দশমিক কেটে ফেললেও 3. ভাগশেষ 7 − 2×3 = 1. এখানে সবাই একমত।
  2. এবার -7 ÷ 2. আসল ভাগফল -3.5 — সংখ্যারেখায় -4 আর -3-এর ঠিক মাঝখানে। এখন প্রশ্ন: পূর্ণসংখ্যা চাইলে কোন দিকে যাব?
  3. Python বাম দিকে যায়: -4. “নিচের দিকে” মানে ছোটর দিকে, আর -4 হলো -3-এর চেয়ে ছোট। C, C++, Java এখানে দশমিক অংশ কেটে ফেলে, অর্থাৎ শূন্যের দিকে যায় — তাদের ফল -3. একই অঙ্ক, ভিন্ন ভাষায় ভিন্ন উত্তর।
  4. ভাগশেষ সেই সিদ্ধান্ত মেনে চলে। Python নিশ্চিত করে a == b * (a // b) + a % b সবসময় সত্য। -4 বেছে নেওয়ায় ভাগশেষ হলো 1 — আর এর চিহ্ন সবসময় ভাজকের (এখানে 2) মতো। এর বড় সুবিধা: n % 7 সবসময় 0 থেকে 6-এর মধ্যে, n ঋণাত্মক হলেও। সপ্তাহের দিন বা ঘড়ির হিসাবে ঠিক এটাই দরকার।

দুটি একসাথে চাইলে divmod(a, b) ব্যবহার করুন — এটি (a // b, a % b) জোড়া একবারে দেয়। ধরুন ১০০ টাকার বিল ৩ বন্ধু ভাগ করবেন। টাকাকে পয়সায় নিলে ১০০০০ পয়সা; divmod(10000, 3) দেয় (3333, 1) — প্রত্যেকে ৩৩ টাকা ৩৩ পয়সা, আর ১ পয়সা বাকি, যা কোনো একজনকে বাড়তি দিতে হবে। float দিয়ে 100 / 3 করলে পাবেন 33.333333333333336 — যার শেষ অঙ্কটিও আবার ভুল!

অগ্রাধিকার — কোনটি আগে

একাধিক অপারেটর একসাথে থাকলে কোনটি আগে হবে, তা ঠিক করে operator precedence. স্কুলের BODMAS-এর মতোই, ওপর থেকে নিচে:

  1. ( ) — বন্ধনী, সবার আগে
  2. ** — ঘাত
  3. একক -x, +x — চিহ্ন
  4. * / // % — গুণ-ভাগ, বাম থেকে ডানে
  5. + - — যোগ-বিয়োগ, বাম থেকে ডানে

দুটি জায়গায় অনেকে ভুল করেন। প্রথমত, ** চিহ্নের চেয়ে শক্তিশালী, তাই -2 ** 2 মানে -(2 ** 2), ফল -4. বর্গ চাইলে লিখুন (-2) ** 2. দ্বিতীয়ত, ** হলো right-associative — ডান দিক থেকে হিসাব হয়। 2 ** 3 ** 2 মানে 2 ** (3 ** 2) = 2 ** 9 = 512, (2 ** 3) ** 2 = 64 নয়। গণিতের ঘাত লেখার নিয়মও এমনই। তুলনা (<, ==) আর যুক্তি (and, or) অপারেটর আরও নিচের ধাপে — সেগুলো পরের পর্বে।

নিশ্চিত না হলে বন্ধনী দিন। (a + b) / 2 পড়তে স্পষ্ট, আর কোডের গতিতে বন্ধনীর কোনো খরচ নেই। যে expression পড়ে বুঝতে অগ্রাধিকারের টেবিল খুলতে হয়, তা পরের পাঠকের জন্য একটি ফাঁদ।

এবার সব একসাথে চালিয়ে দেখুন — বড় int, ভিন্ন ভিত্তি, দুই রকম ভাগ, অগ্রাধিকার আর পয়সায় বিল ভাগ:

import math

print(2 ** 100)
print(math.factorial(25))
print(1_000_000 * 3, 0xFF, 0b1010, bin(10), hex(255))
print(7 / 2, 7 // 2, 7 % 2, 10 / 2)
print(-7 // 2, -7 % 2, divmod(-7, 2))
print(-2 ** 2, (-2) ** 2, 2 ** 3 ** 2)

# ১০০ টাকার বিল ৩ বন্ধুর মধ্যে — পয়সায় হিসাব
each, extra = divmod(100_00, 3)
print(f"প্রত্যেকে {each} পয়সা, বাকি {extra} পয়সা")
print(f"একজন দেবেন {each + extra} পয়সা")
print(100 / 3)

প্রথম দুই লাইনে ৩১ আর ২৬ অঙ্কের নির্ভুল সংখ্যা — কোনো overflow নেই। তৃতীয় লাইনে 3000000 255 10 0b1010 0xff: underscore গায়েব, hex আর binary literal সাধারণ int হয়ে গেছে, আর bin()/hex() string ফেরত দিয়েছে। চতুর্থ লাইনে 3.5 3 1 5.0 — খেয়াল করুন 10 / 2-ও 5.0. পঞ্চম লাইন -4 1 (-4, 1) ঠিক উপরের সংখ্যারেখার মতো। ষষ্ঠ লাইন -4 4 512: চিহ্নের আগে ঘাত, আর ঘাত ডান দিক থেকে। শেষে বিল ভাগ: প্রত্যেকে 3333 পয়সা, 1 পয়সা বাকি, তাই একজন দেবেন 3334 পয়সা — হিসাব মিলে গেল ঠিক ১০০০০-এ। আর শেষ লাইনের 33.333333333333336 দেখাচ্ছে float কোথায় হোঁচট খায়।

C/C++ জানলে

C-তে 7 / 2 হলো 3 (দুটি int হলে পূর্ণসংখ্যার ভাগ), আর C99 থেকে ঋণাত্মক ভাগ শূন্যের দিকে কাটা হয়: -7 / 2 == -3, -7 % 2 == -1. Python-এ / সবসময় আসল ভাগ, আর //, % floor মানে — তাই C থেকে কোড অনুবাদ করার সময় ঋণাত্মক সংখ্যায় ফল বদলে যায়। আবার C-র int overflow হয় (signed int-এ তা undefined behavior), Python-এর int হয় না; কিন্তু Python-এর float আর C-র double হুবহু একই IEEE 754 জিনিস, তাই float-এর সব ফাঁদ দুই ভাষায় একই। Bit আর ভগ্নাংশ কীভাবে মেমরিতে থাকে, বিস্তারিত দেখুন C কোর্সের ডেটা রিপ্রেজেন্টেশন পর্বে।

৪ · float — দশমিকের ভেতরে লুকানো ফাঁদ

Python-এর float হলো IEEE 754 মানের একটি “double precision” floating-point সংখ্যা — C-র double-এর হুবহু একই জিনিস। এর ৬৪ বিট তিন ভাগে ভাগ করা: ১ বিট চিহ্ন (ধনাত্মক না ঋণাত্মক), ১১ বিট ঘাত (exponent), আর ৫২ বিট অঙ্ক (fraction; একটি লুকানো বিট মিলিয়ে কার্যত ৫৩ বিটের নির্ভুলতা)। বিজ্ঞানের বইয়ের scientific notation-এর মতো: 6.02 × 10²³ — শুধু ভিত্তি ১০-এর বদলে ২.

“Floating” নামটি এসেছে এখান থেকে: দশমিক বিন্দুটি “ভাসে”। একই ৬৪ বিটে আপনি পরমাণুর আকার (1e-10) আর গ্যালাক্সির দূরত্ব (1e20) দুটোই রাখতে পারেন। কিন্তু দাম হলো — যেকোনো float-এ মোটামুটি ১৫ থেকে ১৭টি অর্থপূর্ণ দশমিক অঙ্ক থাকে, তার বেশি নয়। আর যে মান binary ভগ্নাংশে ঠিকঠাক লেখা যায় না, তা রাখা হয় সবচেয়ে কাছের মান দিয়ে।

0.1 কেন binary-তে ঠিকঠাক লেখা যায় না? দশমিক ভগ্নাংশকে binary-তে নেওয়ার একটি সহজ কৌশল আছে: বারবার ২ দিয়ে গুণ করুন; পূর্ণ অংশ (০ বা ১) হলো পরের binary অঙ্ক, আর দশমিক অংশ নিয়ে আবার গুণ। 0.1-এর ওপর চালিয়ে দেখুন:

10.1 × 2 = 0.2  → অঙ্ক 020.2 × 2 = 0.4  → অঙ্ক 030.4 × 2 = 0.8  → অঙ্ক 040.8 × 2 = 1.6  → অঙ্ক 1, থাকে 0.650.6 × 2 = 1.2  → অঙ্ক 1, থাকে 0.260.2 আবার! চক্র শুরু …
এখন পর্যন্ত binary
binary
বাকি অংশ
  1. 0.1 × 2 = 0.2. পূর্ণ অংশ 0, তাই binary-র প্রথম অঙ্ক 0. বাকি থাকল 0.2.
  2. 0.2 × 2 = 0.4. আবার পূর্ণ অংশ 0. Binary এখন 0.00.
  3. 0.4 × 2 = 0.8. এখনো 0. তিনটি শূন্য — মানে 0.1 হলো ১/৮-এর চেয়েও ছোট, যা ঠিকই আছে।
  4. 0.8 × 2 = 1.6. এবার পূর্ণ অংশ 1! Binary 0.0001. দশমিক অংশ 0.6 নিয়ে এগোই।
  5. 0.6 × 2 = 1.2. আবার 1. বাকি থাকল 0.2 — খেয়াল করুন, এটি আমরা ধাপ ১-এ আগেই দেখেছি।
  6. চক্র ধরা পড়ল। 0.2 থেকে আবার 0.4, 0.8, 0.6, 0.2… — অর্থাৎ 0011 অংশটি চিরকাল ঘুরবে, ঠিক যেমন দশমিকে ১/৩ = 0.333… কিন্তু float-এ জায়গা মাত্র ৫৩ বিট, তাই কোথাও কাটতেই হয়। ফলে Python যে “0.1” রাখে, তা আসলে 0.1000000000000000055511…, অর্থাৎ 0.1-এর চেয়ে সামান্য বড়। দশমিক ভগ্নাংশ binary-তে শেষ হয় শুধু যদি হর (denominator) ২-এর ঘাত হয় — 0.5, 0.25, 0.375 নিখুঁত; 0.1, 0.2, 0.3 নয়।

তাহলে print(0.1) কেন 0.1 দেখায়, লম্বা সংখ্যাটি নয়? কারণ Python 3.1 থেকে float দেখানোর সময় সবচেয়ে ছোট এমন দশমিক লেখা বেছে নেয়, যা আবার পড়লে হুবহু একই float পাওয়া যায়। 0.1 লিখলেই সেই float ফিরে আসে, তাই Python ভদ্রতা করে 0.1 দেখায়। কিন্তু 0.1 + 0.2-এর ফল এমন একটি float, যা 0.3 লিখলে পাওয়া যায় না — দুটির ছোট ছোট ভুল যোগ হয়ে একটু সরে গেছে। তাই Python সৎভাবে দেখায় 0.30000000000000004. এটি Python-এর বাগ নয়; IEEE 754 float ব্যবহার করে এমন প্রায় সব ভাষায় (C, Java, JavaScript) একই ফল আসে। এই ধরনের ক্ষুদ্র ভুলকে বলে rounding error.

float = সবচেয়ে কাছের binary ভগ্নাংশ
ছোট ভুল + ছোট ভুল = দৃশ্যমান ভুল

float-এর সীমানা — inf, nan আর ২৫৩

  • সবচেয়ে বড় float প্রায় 1.8e308. এর বেশি হলে গুণফল হয়ে যায় infinity: 1e308 * 10 দেয় inf. নিজে লিখতে চাইলে float("inf"); সবচেয়ে কম মান খোঁজার লুপে শুরুর মান হিসেবে এটি কাজে লাগে।
  • nan (“Not a Number”) আসে অর্থহীন হিসাব থেকে, যেমন inf - inf. এর অদ্ভুত বৈশিষ্ট্য: nan == nan হলো False — nan নিজের সমানও নয়! তাই nan আছে কি না দেখতে math.isnan(x) লিখুন। ডেটা সায়েন্সে হারানো মান বোঝাতে nan খুব বেশি দেখবেন (পর্ব ৪৬).
  • বড় পূর্ণসংখ্যাও float-এ নিখুঁত নয়। ৫৩ বিটের নির্ভুলতা মানে ২৫৩ = 9007199254740992 পর্যন্ত সব পূর্ণসংখ্যা নিখুঁত; তার পরে ফাঁক শুরু। float(2**53 + 1) দেয় 9007199254740992.0 — শেষের 1 হারিয়ে গেছে। তাই জাতীয় পরিচয়পত্র নম্বর বা ব্যাংক হিসাব নম্বরকে কখনো float বানাবেন না; এগুলো আসলে string (পর্ব ০৯).
  • শূন্য দিয়ে ভাগ: Python-এ 1 / 0 আর 1.0 / 0.0 দুটোই ZeroDivisionError exception দেয়, C-র মতো চুপচাপ inf দেয় না। Exception সামলানো শিখবেন পর্ব ২৪-এ।

৫ · float তুলনা, round() আর সুন্দর করে দেখানো

float-এ == কেন বিপজ্জনক

যেহেতু float-এ ছোট ছোট ভুল জমে, দুটি float হুবহু সমান কি না জিজ্ঞেস করা প্রায়ই ভুল প্রশ্ন। 0.1 + 0.2 == 0.3 হলো False. একটি লুপ যদি while x != 1.0 শর্তে চলে আর প্রতিবার x += 0.1 করে, সেটি কখনো থামবে না — কারণ x কখনো ঠিক 1.0 হয় না, 0.9999999999999999 থেকে লাফিয়ে 1.0999999999999999 হয়ে যায়।

সঠিক প্রশ্ন হলো: “দুটি কি যথেষ্ট কাছাকাছি?” এর জন্য Python 3.5 থেকে আছে math.isclose(a, b). ডিফল্টে এটি দেখে পার্থক্য দুটি মানের প্রায় একশো কোটি ভাগের এক ভাগের (rel_tol=1e-09) মধ্যে কি না। একটি সতর্কতা: শূন্যের সাথে তুলনায় আপেক্ষিক পার্থক্য কাজ করে না, তাই তখন abs_tol দিতে হয় — যেমন math.isclose(x, 0.0, abs_tol=1e-9).

round() — স্কুলের নিয়ম নয়

স্কুলে শিখেছেন, ঠিক মাঝখানে (.5) হলে ওপরের দিকে গোল করতে হয়: 2.5 → 3. Python-এর round() তা করে না। Python 3 মানে banker's rounding (round half to even): ঠিক মাঝখানে পড়লে সবচেয়ে কাছের জোড় সংখ্যায় যায়। তার ওপর আছে float-এর representation-এর সমস্যা। চারটি ঘটনা দেখুন:

1round(1.5)2round(2.5)3round(0.125, 2)4round(2.675, 2)
দুই নিয়মের তুলনা
স্কুলের নিয়ম
Python
কারণ
  1. round(1.5) → 2. 1.5 ঠিক 1 আর 2-এর মাঝখানে। জোড় সংখ্যাটি 2, তাই 2 — স্কুলের উত্তরের সাথেও মিলে যায়। এখানে কেউ কিছু টের পায় না।
  2. round(2.5) → 2. ঠিক মাঝখানে, আর জোড় সংখ্যা 2, তাই 2 — স্কুলের 3 নয়। একইভাবে round(0.5) হলো 0, round(3.5) হলো 4. অর্ধেক ঘটনা ওপরে, অর্ধেক নিচে যায় — তাই অনেক সংখ্যা যোগ করলে গোল করার ভুল একদিকে জমে না। কেন এটি ভালো, তা ভাবনার প্রশ্ন ৪-এ।
  3. round(0.125, 2) → 0.12. 0.125 হলো ১/৮ — binary-তে নিখুঁত, তাই এটি সত্যিই ঠিক মাঝখানে। দুই ঘর পরে জোড় অঙ্ক 2, তাই 0.12. Banker's rounding আবার কাজ করল।
  4. round(2.675, 2) → 2.67. এখানে banker's rounding দায়ী নয়! 2.675 binary-তে নিখুঁত নয়; float-এ আসলে রাখা আছে 2.67499999999999982236431605997495353221893310546875 — মাঝখানের সামান্য নিচে. তাই নিচের দিকে গোল হয়েছে। এই ঘটনাটি Python-এর অফিসিয়াল ডকুমেন্টেও উদাহরণ হিসেবে আছে। শিক্ষা: float-এ “ঠিক মাঝখান” প্রায়ই আসলে মাঝখান নয়।

সুতরাং টাকা বা নম্বরের মতো জায়গায়, যেখানে নির্দিষ্ট গোল করার নিয়ম আইন বা নীতিতে লেখা থাকে, round()-এর ওপর ভরসা করবেন না। পরের অংশের Decimal দিয়ে নিয়মটি নিজে বলে দিন।

দেখানোর জন্য গোল করা — f-string

অনেক সময় আপনি মান বদলাতে চান না, শুধু সুন্দর করে দেখাতে চান। তখন f-string-এর format ব্যবহার করুন: f"{100/3:.2f}" দেয় '33.33', আর f"{1234567.891:,.2f}" দেয় '1,234,567.89'. এতে আসল float বদলায় না, শুধু লেখাটা তৈরি হয়। মনে রাখুন, .2f-ও একই binary মান দেখে গোল করে, তাই f"{2.675:.2f}"-ও '2.67'. Format-এর পুরো ভাষা শিখবেন পর্ব ১০-এ।

নিয়ম: হিসাবের মাঝখানে গোল করবেন না, শুধু শেষে দেখানোর সময় করুন — যদি না নিয়মেই বলা থাকে প্রতিটি ধাপে গোল করতে হবে (যেমন প্রতি মাসের সুদ পয়সায় গোল করে খাতায় তোলা)। দ্বিতীয় ক্ষেত্রে float নয়, Decimal দিয়ে প্রতিটি ধাপে স্পষ্টভাবে গোল করুন।

৬ · টাকার হিসাব — Decimal, quantize আর পয়সা-int

একটি দোকানের সফটওয়্যারে ১০টি পণ্য, প্রতিটির দাম ১০ পয়সা (0.10 টাকা)। মোট হওয়ার কথা ঠিক ১ টাকা। float দিয়ে একটি একটি করে যোগ করলে কী হয়, আর Decimal দিয়ে করলে কী হয় — পাশাপাশি দেখুন:

1total_f = 0.02total_d = Decimal("0")3for i in range(10):4    total_f += 0.15    total_d += Decimal("0.10")6print(total_f == 1.0, total_d == 1)
দুটি মোট (সবুজ লাইন এইমাত্র চলেছে)
পণ্য যোগ হয়েছে
total_f
total_d
  1. দুটি খাতা খোলা হলো। একটি float-এ, একটি Decimal-এ। দুটিই শূন্য থেকে শুরু।
  2. তিনটি পণ্যের পর। Decimal বলছে 0.30 — নিখুঁত। float বলছে 0.30000000000000004 — ভুল এর মধ্যেই ঢুকে গেছে, কারণ প্রতিটি “0.1” আসলে একটু বড়, আর যোগের প্রতিটি ধাপে আবার গোল হয়।
  3. আটটি পণ্যের পর। float এবার উল্টো দিকে সরেছে: 0.7999999999999999. ছোট ভুলগুলো কখনো ওপরে, কখনো নিচে যায় — কোথায় দাঁড়াবে আগে থেকে বলা কঠিন।
  4. দশটি পণ্যের পর। float: 0.9999999999999999, Decimal: 1.00. গ্রাহকের রসিদে ১ টাকা ছাপা হলেও ভেতরের মান ১ নয়। লক্ষ লক্ষ লেনদেনে এই ভুল জমে হিসাব মেলে না।
  5. তুলনা। total_f == 1.0 হলো False, total_d == 1 হলো True. যে কোড “মোট ১ টাকা হলে ছাড় দাও” পরীক্ষা করে, float-এ সেটি চুপচাপ ভুল সিদ্ধান্ত নেবে। (একটি টীকা: Python 3.12 থেকে বিল্ট-ইন sum() float যোগে একটি সতর্ক পদ্ধতি ব্যবহার করে, তাই sum([0.1] * 10) দেয় 1.0 — কিন্তু সাধারণ += লুপ, অন্য ভাষা বা পুরোনো Python-এ সেই সুরক্ষা নেই।)

Decimal — তিনটি নিয়ম

decimal হলো standard library-র একটি module (Python 2.4 থেকে আছে, আর 3.3 থেকে এর দ্রুত C সংস্করণ)। এটি সংখ্যাকে দশ-ভিত্তিক অঙ্কে রাখে, ঠিক যেভাবে আমরা খাতায় লিখি। তিনটি নিয়ম মানলেই যথেষ্ট:

  1. সবসময় string থেকে তৈরি করুন। Decimal("0.1") ঠিক 0.1. কিন্তু Decimal(0.1) লিখলে আগে float 0.1 তৈরি হয় — ভুলসহ — তারপর Decimal সেই ভুলটিকে বিশ্বস্তভাবে কপি করে: 0.1000000000000000055511151231257827021181583404541015625. float-এর দূষণ ঢুকে গেল। int থেকে তৈরি নিরাপদ: Decimal(1250).
  2. Decimal-এর সাথে float মেশাবেন না। Decimal("1.5") + 0.1 লিখলে TypeError আসে — এটি ভালো, Python আপনাকে ভুল থেকে বাঁচাচ্ছে। Decimal-এর সাথে int মেশানো চলে।
  3. গোল করুন quantize দিয়ে, নিয়ম বলে দিয়ে। x.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) মানে “পয়সা পর্যন্ত রাখো, মাঝখানে পড়লে ওপরে যাও”। নিয়ম না বললে Decimal-ও ডিফল্টে ROUND_HALF_EVEN, অর্থাৎ banker's rounding মানে।

বাজারের হিসাবের খাতা আর ক্যালকুলেটরের পার্থক্যের মতো। পুরোনো দোকানদার খাতায় লেখেন “১২৫০ টাকা ৫০ পয়সা” — প্রতিটি অঙ্ক যেমন আছে তেমন। float হলো এমন এক ক্যালকুলেটর, যা ভেতরে ভেতরে সব সংখ্যা অন্য এক ভাষায় অনুবাদ করে রাখে, আর ফেরত দেওয়ার সময় আবার অনুবাদ করে — প্রতিটি অনুবাদে সামান্য অর্থ হারায়। Decimal হলো সেই খাতা।

এবার বাস্তব কাজ। ১২৫০ টাকার রেস্টুরেন্ট বিলে ১৫% VAT; আর একটি মোবাইল ব্যাংকিং অ্যাপে ১৮৫০ টাকা ক্যাশ-আউটে ১.৮৫% চার্জ (হারটি উদাহরণমাত্র)। চালিয়ে দেখুন:

import math
from decimal import Decimal, ROUND_HALF_UP

print(0.1 + 0.2, 0.1 + 0.2 == 0.3)
print(math.isclose(0.1 + 0.2, 0.3))
print(f"{0.1:.20f}")
print(round(2.5), round(3.5), round(2.675, 2))
print(Decimal(0.1))
print(Decimal("0.1") + Decimal("0.2") == Decimal("0.3"))

POISHA = Decimal("0.01")
bill = Decimal("1250.00")
vat = (bill * Decimal("0.15")).quantize(POISHA, rounding=ROUND_HALF_UP)
print(f"VAT {vat} টাকা, মোট {bill + vat} টাকা")

fee = Decimal("1850") * Decimal("0.0185")   # ১.৮৫% চার্জ
print(fee, fee.quantize(POISHA, rounding=ROUND_HALF_UP), fee.quantize(POISHA))

প্রথম চার লাইন float-এর ফাঁদ এক জায়গায়: 0.30000000000000004 False, তারপর math.isclose-এ True, তারপর .20f দিয়ে 0.1-এর আসল রূপ 0.10000000000000000555, আর round()-এর ফল 2 4 2.67 — banker's rounding আর representation দুটোই দেখা গেল। পঞ্চম লাইনে Decimal(0.1) float-এর পুরো ভুলটি ফাঁস করে দিয়েছে, আর ষষ্ঠ লাইনে string থেকে তৈরি Decimal-এ 0.1 + 0.2 == 0.3 হলো True. VAT-এর লাইন দেখায় VAT 187.50 টাকা, মোট 1437.50 টাকা — Decimal দুই ঘরের পয়সা ধরে রেখেছে। শেষ লাইনটি সবচেয়ে শিক্ষণীয়: আসল চার্জ 34.2250, ঠিক মাঝখানে। ROUND_HALF_UP দিয়ে 34.23, কিন্তু নিয়ম না বললে Decimal-এর ডিফল্ট banker's rounding দেয় 34.22. লাখো লেনদেনে এই এক পয়সার পার্থক্য বড় অঙ্ক — তাই কোন নিয়মে গোল হবে, তা ব্যবসার নীতি থেকে জেনে কোডে স্পষ্ট লিখুন।

বিকল্প: টাকাকে পয়সায় int হিসেবে রাখা

অনেক পেমেন্ট সিস্টেম Decimal-ও ব্যবহার করে না — তারা সব টাকা সবচেয়ে ছোট এককে (পয়সা) int হিসেবে রাখে। ১২৫০ টাকা ৫০ পয়সা মানে 125050. int নিখুঁত আর দ্রুত, যোগ-বিয়োগে কোনো ভুল নেই, ডেটাবেসে রাখাও সহজ। শুধু দেখানোর সময় ভাগ করে টাকা আর পয়সা আলাদা করুন: taka, poisha = divmod(125050, 100). ভাগ করার সময় (যেমন বিল ভাগ) ভাগশেষ কে পাবে, তা নিজে ঠিক করতে হয় — যেমন আগের কোডে ১ পয়সা একজনকে দেওয়া হয়েছিল। শতাংশ হিসাবের মতো জটিল কাজে Decimal সহজ; সাধারণ যোগ-বিয়োগের খাতায় পয়সা-int সহজ।

কাজউপযুক্ত typeকেন
গোনা: মানুষ, পণ্য, রোল নম্বরintনিখুঁত, সীমাহীন
মাপা: দৈর্ঘ্য, ওজন, তাপমাত্রা, বিজ্ঞান, মেশিন লার্নিংfloatদ্রুত, হার্ডওয়্যারে চলে; সামান্য ভুল গ্রহণযোগ্য
টাকা, VAT, সুদ, চার্জDecimal বা পয়সায় intদশমিক নিখুঁত, গোল করার নিয়ম নিজে বলা যায়
ঠিক ভগ্নাংশ: ১/৩, অনুপাতFractionলব-হর আলাদা রাখে, কোনো ভুল নেই

৭ · আরও কিছু সংখ্যা আর math-এর হাতিয়ার

Fraction আর complex — এক নজরে

fractions module-এর Fraction লব আর হর দুটোই int হিসেবে রাখে, তাই কোনো ভুল হয় না: Fraction(1, 3) + Fraction(1, 6) দেয় Fraction(1, 2). গণিতের সমস্যা যাচাই বা অনুপাতের হিসাবে চমৎকার, তবে অনেক ধাপ চললে লব-হর বিশাল হয়ে ধীর হয়ে যায়। আর Python-এ complex number বিল্ট-ইন: 3 + 4j লিখুন (গণিতের i-এর বদলে প্রকৌশলের j), abs(3 + 4j) দেয় 5.0 — ইলেকট্রিক্যাল প্রকৌশল আর সিগন্যাল প্রসেসিংয়ে কাজে লাগে।

math module — দরকারি পাঁচটি

  • math.sqrt(2) → 1.4142135623730951 (বর্গমূল; 2 ** 0.5-ও চলে)
  • math.floor(-2.5) → -3, math.ceil(2.1) → 3 (নিচে আর ওপরে গোল, int ফেরত দেয়)। সাবধান: int(-2.9) দেয় -2 — int() শূন্যের দিকে কাটে, floor করে না।
  • math.pi → 3.141592653589793
  • math.gcd(84, 36) → 12 (গসাগু; ভগ্নাংশ ছোট করা বা ভাগ সমস্যায়)
  • math.isclose(), math.isnan() — আগেই দেখেছেন

একটি গোল পুকুরের ব্যাসার্ধ ৭ মিটার হলে ক্ষেত্রফল math.pi * 7 ** 2 = 153.93804002589985 বর্গমিটার; রিপোর্টে f"{area:.2f}" লিখলে দেখাবে 153.94. এখানে float একদম ঠিক পছন্দ — পুকুরের পাড় নিজেই এক সেন্টিমিটার এদিক-ওদিক, ১৫ অঙ্কের নির্ভুলতা যথেষ্টর চেয়েও বেশি।

আবার ৫০,০০০ টাকা বছরে ৬% চক্রবৃদ্ধি সুদে ৫ বছর রাখলে সূত্র P * (1 + r) ** n. float দিয়ে পাবেন 66911.27888000001 — শেষে আবার সেই লেজ। Decimal দিয়ে Decimal("50000") * (1 + Decimal("0.06")) ** 5 করে পয়সায় quantize করলে নির্ভুল 66911.28. DPS-এর মতো মাসিক কিস্তির হিসাবে প্রতি মাসে গোল করতে হয় কি না, তা নিয়ে একটি অনুশীলন নিচে আছে।

গড় বের করতে statistics module আছে: statistics.mean([1, 2, 3, 4]) দেয় 2.5; সেখানে median, stdev-ও পাবেন। বড় ডেটার হিসাবে অবশ্য NumPy অনেক দ্রুত — পর্ব ৪৬-এ।

পরীক্ষা ও ইন্টারভিউতে
  • 0.1 + 0.2 == 0.3 কেন False? — 0.1 আর 0.2 binary-তে নিখুঁতভাবে লেখা যায় না; float সবচেয়ে কাছের মান রাখে, আর দুটির ক্ষুদ্র ভুল যোগ হয়ে ফল 0.30000000000000004.
  • -7 // 2 আর -7 % 2 কত? — -4 আর 1; // ঋণাত্মক অসীমের দিকে গোল করে, আর %-এর চিহ্ন ভাজকের মতো।
  • round(2.5) কত, কেন? — 2; Python 3 banker's rounding (মাঝখানে পড়লে জোড়ের দিকে) মানে।
  • Python-এ int overflow হয় কি? — না, int arbitrary precision; শুধু মেমরি শেষ হলে সীমা। তবে float-এর সীমা আছে (প্রায় 1.8e308).
  • টাকার হিসাবে কোন type? — float কখনো নয়; string থেকে তৈরি Decimal আর quantize, অথবা পয়সায় int.

৮ · AI-এর যুগে — হিসাবের কোডে AI-এর তিনটি চেনা ভুল

“একটি বিলিং ফাংশন লিখে দাও” বললে AI চ্যাটবট মুহূর্তে পরিষ্কার, চলে এমন কোড দেয়। সমস্যা হলো, ইন্টারনেটের অসংখ্য টিউটোরিয়াল টাকাকে float-এ রাখে — আর AI সেখান থেকেই শিখেছে। কোডটি ছোট পরীক্ষায় ঠিক উত্তর দেয়, তাই ভুল ধরা পড়ে না; ধরা পড়ে মাস শেষে হিসাব মেলাতে গিয়ে। নিচের টেবিলের ভুলগুলো খুঁজুন:

AI প্রায়ই যা লেখেসমস্যাঠিক করার উপায়
price = 19.99, total += price * qtyটাকা float-এ; ভুল জমেDecimal("19.99") বা পয়সায় int
if total == 100.0:float-এ হুবহু তুলনাDecimal-এ তুলনা, বা math.isclose
round(amount, 2) দিয়ে VAT গোলbanker's rounding আর representation — নিয়ম অস্পষ্টquantize(..., rounding=ROUND_HALF_UP)
Decimal(price) যেখানে price একটি floatfloat-এর ভুল Decimal-এ কপিDecimal(str(price)), বা শুরু থেকেই string
C বা Java থেকে অনুবাদে a / bPython-এ float আসে, int নয়; ঋণাত্মকে // ভিন্নইচ্ছা অনুযায়ী / বা // বেছে নিন, ঋণাত্মক দিয়ে টেস্ট

AI-কে টাকার কোড লিখতে দিলে নিয়মগুলো আগেই বলে দিন। নিচের কাঠামোটি কপি করে ব্যবহার করুন:

আমার জন্য Python 3.14-এ একটি ফাংশন লেখো, যা রেস্টুরেন্টের বিলে ১৫% VAT আর ৫% সার্ভিস চার্জ যোগ করে মোট বের করে। শর্ত: ১. টাকার কোনো মান float-এ রাখবে না। decimal.Decimal ব্যবহার করবে, আর Decimal সবসময় string থেকে তৈরি করবে। ২. প্রতিটি চার্জ আলাদাভাবে পয়সা (0.01) পর্যন্ত quantize করবে, ROUND_HALF_UP দিয়ে। round() ব্যবহার করবে না। ৩. কোথাও float-এ == তুলনা থাকবে না। ৪. ঠিক মাঝখানে পড়ে এমন একটি টেস্ট কেস দাও (যেমন চার্জ x.xx5 হয়), আর প্রত্যাশিত উত্তর হাতে হিসাব করে দেখাও। ৫. কোন জায়গায় গোল করার নিয়ম ব্যবসার নীতির ওপর নির্ভর করে, তা আলাদা করে বলো।

AI-এর লেখা হিসাবের কোড পেলে — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলো আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১IEEE 754 মান কেন তৈরি হয়েছিল? float-এ এত ঝামেলা থাকলে পুরো দুনিয়া এখনো এটিই ব্যবহার করে কেন?

১৯৭০-এর দশকে প্রতিটি কম্পিউটার কোম্পানির নিজস্ব floating-point ফরম্যাট ছিল — IBM, DEC, Cray, প্রত্যেকে আলাদা। একই প্রোগ্রাম দুই কোম্পানির মেশিনে চালালে ভিন্ন উত্তর আসত; কোনো মেশিনে খুব ছোট সংখ্যা হঠাৎ শূন্য হয়ে যেত, কোনোটিতে শূন্য দিয়ে ভাগ করলে পুরো প্রোগ্রাম থেমে যেত। বিজ্ঞানী আর প্রকৌশলীদের জন্য এটি ছিল দুঃস্বপ্ন: একটি সংখ্যাগত পদ্ধতি এক মেশিনে কাজ করত, অন্যটিতে না।

এই বিশৃঙ্খলা দূর করতে IEEE একটি কমিটি গঠন করে, আর ১৯৮৫ সালে প্রকাশিত হয় IEEE 754. এর প্রধান স্থপতি ছিলেন ক্যালিফোর্নিয়া বিশ্ববিদ্যালয় (Berkeley)-এর গণিতবিদ William Kahan, যিনি Intel-এর 8087 গণিত-coprocessor-এর নকশাতেও পরামর্শ দিয়েছিলেন। এই কাজের জন্য তিনি ১৯৮৯ সালে Turing Award পান। মানটি পরে ২০০৮ আর ২০১৯ সালে হালনাগাদ হয়েছে।

IEEE 754-এর মূল প্রতিশ্রুতি দুটি। প্রথমত, পুনরুৎপাদনযোগ্যতা: যোগ, বিয়োগ, গুণ, ভাগ আর বর্গমূলের ফল হতে হবে “সঠিক উত্তরটি নিয়ে নির্দিষ্ট নিয়মে গোল করলে যা হয়” — সব মেশিনে একই। দ্বিতীয়ত, ভদ্রভাবে ব্যর্থ হওয়া: inf, nan আর খুব ছোট “subnormal” সংখ্যা রাখা হয়েছে, যাতে একটি অর্থহীন হিসাব পুরো প্রোগ্রাম না থামিয়ে চিহ্নসহ ফল দেয়। nan যে নিজের সমান নয়, সেটিও ইচ্ছাকৃত — যাতে x != x দিয়ে nan ধরা যায়।

তাহলে float এখনো কেন? কারণ বিকল্পগুলোর দাম বেশি। float হার্ডওয়্যারে সরাসরি চলে — আধুনিক CPU আর GPU প্রতি সেকেন্ডে শত কোটি থেকে লক্ষ কোটি float হিসাব করে। আবহাওয়ার পূর্বাভাস, গেমের গ্রাফিক্স, মেশিন লার্নিংয়ের প্রশিক্ষণ — এসব কাজে মাপার ডেটা এমনিতেই অনুমাননির্ভর, তাই ১৫ অঙ্কের নির্ভুলতা যথেষ্টর বেশি। Decimal সফটওয়্যারে চলে, অনেক ধীর। আর AI মডেলগুলো আজ উল্টো দিকে যাচ্ছে — ১৬ বিট, এমনকি ৮ বিটের float, কারণ সেখানে গতি নির্ভুলতার চেয়ে বেশি দামি।

মূল উপলব্ধি: float কোনো ভুল নকশা নয়; এটি গতি, পরিসর আর নির্ভুলতার মধ্যে একটি সচেতন আপস, যা সব মেশিনে একইভাবে আচরণ করে। সমস্যা float-এ নয় — ভুল কাজে float ব্যবহারে।

প্র ০২Python 2-তে 7 / 2 ছিল 3, Python 3-এ হলো 3.5. ভাষার এত মৌলিক একটি অপারেটরের অর্থ বদলানো হলো কেন, আর কীভাবে?

Python 2-তে / C-র নিয়ম মানত: দুটি int হলে পূর্ণসংখ্যার ভাগ (floor), অন্তত একটি float হলে আসল ভাগ। শুনতে নিরীহ, কিন্তু Python-এ এটি ছিল এক বিপজ্জনক ফাঁদ। কারণটি dynamic typing: C-তে আপনি ভ্যারিয়েবলের type ঘোষণা করেন, তাই জানেন কোন ভাগ হবে। Python-এ একটি ফাংশন def average(total, n): return total / n যদি কখনো int পায় আর কখনো float, তবে একই কোড কখনো সঠিক, কখনো ভুল উত্তর দেয় — আর কোনো error-ও দেখায় না। average(7, 2) চুপচাপ 3 দিত।

২০০১ সালে Moshe Zadka আর Guido van Rossum PEP 238 (“Changing the Division Operator”) প্রস্তাব করেন। মূল যুক্তি: একটি অপারেটরের অর্থ তার ইনপুটের type-এর ওপর নয়, গাণিতিক উদ্দেশ্যের ওপর নির্ভর করা উচিত। তাই দুটি আলাদা অপারেটর: / সবসময় আসল ভাগ, // সবসময় floor ভাগ — ইনপুট int হোক বা float.

বদলটি একবারে চাপিয়ে দেওয়া হয়নি। Python 2.2-তে // যোগ হলো, আর যে চায় সে ফাইলের শুরুতে from __future__ import division লিখে নতুন আচরণ আগেই পেতে পারত। পুরোনো আচরণের ব্যবহার খুঁজে বের করতে সতর্কবার্তার ব্যবস্থাও রাখা হলো। শেষে ২০০৮ সালে Python 3.0-তে নতুন অর্থ ডিফল্ট হলো। এই __future__ কৌশল পরে আরও অনেক পরিবর্তনে ব্যবহৃত হয়েছে — ভাষা বদলানোর একটি ভদ্র, ধাপে ধাপে পথ।

দাম কম ছিল না। হাজার হাজার প্রোগ্রামে / খুঁজে দেখতে হয়েছে কোনটি পূর্ণসংখ্যার ভাগ বোঝাত। Python 2 থেকে 3-এ যাওয়ার দীর্ঘ, কষ্টকর পথে (Python 2-এর সমর্থন শেষ হয় ২০২০ সালে) এটি ছিল অনেকগুলো ভাঙা পরিবর্তনের একটি। তবু ফিরে তাকালে প্রায় সবাই একমত — নতুন নিয়মে নতুন শিক্ষার্থীর জন্য / ঠিক তাই করে, যা গণিত ক্লাসে শিখেছে।

মূল উপলব্ধি: dynamic typing-এর ভাষায় অপারেটরের অর্থ type-এর ওপর নির্ভর করলে নীরব ভুল জন্মায়। ভালো নকশা উদ্দেশ্য অনুযায়ী আলাদা অপারেটর দেয় — আর বড় পরিবর্তন আনে ধাপে ধাপে, পালানোর পথ রেখে।

প্র ০৩Python-এর int সীমাহীন — তাহলে ২০২২ সালে বড় int-কে string-এ বদলানোর ওপর ৪৩০০ অঙ্কের সীমা বসানো হলো কেন?

শুরুতে Python-এ দুটি পূর্ণসংখ্যার type ছিল: int (মেশিনের আকারের, দ্রুত) আর long (সীমাহীন, ধীর; লেখার শেষে L). সীমা পার হলে প্রোগ্রামারকে নিজে সামলাতে হতো। PEP 237 (২০০১) দুটিকে মেলানোর পরিকল্পনা করে: Python 2.2 থেকে int সীমা পার হলে নিজে থেকেই long হয়ে যেত, আর Python 3.0-তে long নামটিই মুছে শুধু একটি int রাখা হলো — যা দরকার অনুযায়ী বড় হয়। ভেতরে CPython সংখ্যাটিকে ৩০ বিটের “অঙ্কের” একটি সারি হিসেবে রাখে, ঠিক যেমন আমরা খাতায় দশমিক অঙ্কের সারি লিখি।

এখানেই একটি লুকানো খরচ। Binary-তে রাখা একটি বিশাল int-কে দশমিক লেখায় (str()) বা দশমিক লেখা থেকে int-এ (int("...")) বদলাতে CPython-এর অ্যালগরিদমের সময় লাগত অঙ্কসংখ্যার বর্গের অনুপাতে। দশ গুণ লম্বা সংখ্যা মানে প্রায় একশো গুণ সময়। সাধারণ প্রোগ্রামে কেউ টের পায় না। কিন্তু একটি ওয়েব সার্ভার যদি ব্যবহারকারীর পাঠানো JSON পড়ে, আর কেউ ইচ্ছা করে কয়েক লাখ অঙ্কের একটি সংখ্যা পাঠায় — সার্ভারের CPU কয়েক সেকেন্ড বা আরও বেশি আটকে যায়। কয়েকটি অনুরোধেই সার্ভার অচল: একে বলে denial-of-service আক্রমণ।

এই দুর্বলতার নাম CVE-2020-10735. ২০২২ সালের সেপ্টেম্বরে Python-এর নিরাপত্তা হালনাগাদে (3.10.7, 3.9.14 আর সে সময়ের অন্যান্য সমর্থিত সংস্করণ, এবং পরে 3.11.0) একটি সীমা যোগ হয়: ডিফল্টে ৪৩০০ অঙ্কের বেশি দশমিক int-str রূপান্তর করলে ValueError আসে। দরকার হলে সীমা বদলানো যায় — sys.set_int_max_str_digits() ফাংশন, PYTHONINTMAXSTRDIGITS environment variable, বা -X int_max_str_digits অপশন দিয়ে; 0 দিলে সীমা উঠে যায়। খেয়াল করুন, সীমা শুধু দশমিকের মতো ভিত্তিতে; hex বা binary রূপান্তরে (যেগুলো দ্রুত) নেই, আর int-এর ওপর হিসাবেও কোনো সীমা নেই।

সিদ্ধান্তটি বিতর্কিত ছিল — একটি সাধারণ ফিচারের আচরণ patch release-এ বদলানো, যা কিছু গাণিতিক কোড ভেঙে দেয়। কিন্তু Python core দল নিরাপত্তাকে অগ্রাধিকার দিয়েছিল, কারণ অসংখ্য সার্ভার অজান্তে বাইরের ইনপুট থেকে int বানায়।

মূল উপলব্ধি: “সীমাহীন” ফিচার মানে সীমাহীন খরচের সম্ভাবনা — আর যে খরচ বাইরের কেউ নিয়ন্ত্রণ করতে পারে, তা নিরাপত্তার প্রশ্ন। সুবিধা আর সুরক্ষার মধ্যে একটি যুক্তিসংগত ডিফল্ট বেছে নেওয়াই প্রকৌশল।

প্র ০৪Banker's rounding স্কুলের নিয়মের চেয়ে “ভালো” কেন? পরিসংখ্যানবিদ আর IEEE 754 এটিকে ডিফল্ট বানাল কেন?

স্কুলের নিয়ম — মাঝখানে পড়লে সবসময় ওপরে — দেখতে নিরপেক্ষ, কিন্তু আসলে একপেশে। এক দশমিক ঘরের সংখ্যা ১০টি শেষ-অঙ্কে শেষ হতে পারে: 0 থেকে 9. শেষ অঙ্ক 0 হলে কিছু করার নেই। 1, 2, 3, 4 নিচে যায়; 6, 7, 8, 9 ওপরে যায় — এরা একে অপরকে কাটাকাটি করে। কিন্তু 5 সবসময় ওপরে যায়, আর তার বিপরীতে কেউ নেই। তাই অনেক সংখ্যা গোল করে যোগ করলে মোট গড়ে একটু বেশি আসে — একটি পদ্ধতিগত পক্ষপাত (bias).

একটি ছোট উদাহরণ: 0.5, 1.5, 2.5, 3.5 — আসল যোগফল 8. স্কুলের নিয়মে গোল করলে 1 + 2 + 3 + 4 = 10, ভুল 2. Banker's rounding-এ 0 + 2 + 2 + 4 = 8 — হুবহু। মাঝখানের ঘটনাগুলোর অর্ধেক ওপরে, অর্ধেক নিচে যায় (জোড় অঙ্কের দিকে), তাই দীর্ঘ মেয়াদে ভুল কাটাকাটি হয়। হাজার হাজার মাপ গোল করে গড় বের করা পরিসংখ্যানবিদ বা বিজ্ঞানীর কাছে এটাই চাই। নাম “banker's” হলেও ঐতিহাসিকভাবে সব ব্যাংক এটি ব্যবহার করেছে, এমন নিশ্চিত প্রমাণ নেই — নামটি প্রচলিত, কিন্তু উৎস নিয়ে মতভেদ আছে। এটিকে “Gaussian rounding” বা “round half to even”-ও বলে।

IEEE 754 এটিকে ডিফল্ট বানিয়েছে একই কারণে: float-এর প্রতিটি যোগ-গুণের ফল গোল হয়, আর একটি হিসাবে কোটি কোটি ধাপ থাকতে পারে। প্রতি ধাপে সামান্য ঊর্ধ্বমুখী পক্ষপাত হলে বড় সিমুলেশনে তা জমে বড় ভুল হতো। Python 3 round()-এ একই নিয়ম আনল (Python 2-এর round() মাঝখানে শূন্য থেকে দূরে যেত), যাতে ভাষার গোল করা আর হার্ডওয়্যারের গোল করা সামঞ্জস্যপূর্ণ থাকে।

তবু banker's rounding সবসময় ঠিক নয়। অনেক দেশের কর আইন, বিদ্যুৎ বিল বা মোবাইল ব্যাংকিংয়ের নীতিতে স্পষ্ট লেখা থাকে “অর্ধেক বা তার বেশি হলে ওপরে” — সেখানে ROUND_HALF_UP বাধ্যতামূলক। আবার পরীক্ষার নম্বর গোল করার নিয়ম প্রতিষ্ঠানভেদে আলাদা। প্রোগ্রামারের দায়িত্ব নিজের পছন্দ চাপানো নয়, নিয়মটি খুঁজে বের করে কোডে স্পষ্ট লেখা।

মূল উপলব্ধি: গোল করা একটি ছোট সিদ্ধান্ত মনে হয়, কিন্তু লক্ষ বার পুনরাবৃত্ত হলে পক্ষপাত বড় অঙ্ক হয়ে দাঁড়ায়। “কোন নিয়ম” প্রশ্নের উত্তর গণিতে নয়, ব্যবহারের প্রেক্ষাপটে।

প্র ০৫সংখ্যার প্রতিনিধিত্বের ছোট ভুল বাস্তবে কতটা বিপদ ঘটাতে পারে? ইতিহাস থেকে দুটি উদাহরণ দিন।

Patriot ক্ষেপণাস্ত্র, ১৯৯১: উপসাগরীয় যুদ্ধের সময় ২৫ ফেব্রুয়ারি ১৯৯১, সৌদি আরবের ধাহরানে একটি মার্কিন Patriot ক্ষেপণাস্ত্র-প্রতিরোধ ব্যবস্থা একটি আগত Scud ক্ষেপণাস্ত্র আটকাতে ব্যর্থ হয়। Scud-টি একটি সেনা ব্যারাকে আঘাত করে, ২৮ জন মার্কিন সৈন্য নিহত হন। ১৯৯২ সালের মার্কিন সরকারি (GAO) তদন্ত-প্রতিবেদন অনুযায়ী, সিস্টেমটি সময় গুনত এক সেকেন্ডের দশ ভাগের এক ভাগ (0.1 সেকেন্ড) এককে, আর 0.1-কে একটি ২৪-বিটের binary রেজিস্টারে রাখত — যা এই পর্বে দেখেছেন, 0.1 binary-তে নিখুঁত হয় না। প্রতি টিকে ক্ষুদ্র ভুল জমতে থাকে। সিস্টেমটি প্রায় ১০০ ঘণ্টা একটানা চালু ছিল; ততক্ষণে সময়ের ভুল দাঁড়ায় প্রায় এক সেকেন্ডের এক-তৃতীয়াংশ। শব্দের চেয়ে কয়েক গুণ দ্রুত Scud এই সময়ে আধা কিলোমিটারেরও বেশি সরে যায়, তাই রাডার ভুল জায়গায় খুঁজেছিল। লক্ষণীয়: এটি IEEE float নয়, fixed-point হিসাব ছিল — কিন্তু মূল সমস্যা একই, 0.1-এর binary রূপ। সংশোধিত সফটওয়্যার পৌঁছায় ঘটনার পরদিন।

Vancouver Stock Exchange সূচক, ১৯৮২–৮৩: কানাডার এই স্টক এক্সচেঞ্জ ১৯৮২ সালে ১০০০ মান দিয়ে একটি নতুন সূচক চালু করে। প্রতিটি লেনদেনের পর সূচক নতুন করে হিসাব হতো — দিনে কয়েক হাজার বার — আর প্রতিবার ফলাফল তিন দশমিক ঘরে গোল না করে কেটে ফেলা হতো। প্রতিবার কাটায় হারাত এক হাজার ভাগের এক ভাগেরও কম, কিন্তু সবসময় নিচের দিকে। প্রচলিত বিবরণ অনুযায়ী, প্রায় দুই বছর পর বাজার যখন আসলে বেড়েছিল, সূচক দেখাচ্ছিল ৫২০-এর কাছাকাছি; ১৯৮৩ সালের নভেম্বরে সঠিকভাবে পুনর্গণনার পর রাতারাতি তা দাঁড়ায় প্রায় ১১০০-তে। এখানে ভুলটি একপেশে ছিল — banker's rounding (প্র ০৪) কেন গুরুত্বপূর্ণ, তার নিখুঁত উদাহরণ।

দুই ঘটনার মিল লক্ষ করুন: প্রতিটি একক ভুল ছিল নগণ্য, পরীক্ষায় ধরা পড়ার মতো নয়। বিপদ এসেছে পুনরাবৃত্তি থেকে — লক্ষ বার জমে, দীর্ঘ সময় চালু থাকায়। এজন্যই আধুনিক নিরাপত্তা-সংবেদনশীল সিস্টেমে সময় পূর্ণসংখ্যার টিক (যেমন ন্যানোসেকেন্ড, int-এ) হিসেবে রাখা হয়, আর আর্থিক সিস্টেমে দশমিক বা পয়সা-int.

মূল উপলব্ধি: একটি হিসাব একবার ঠিক হলেই যথেষ্ট নয়; জিজ্ঞেস করুন “এটি দশ লাখ বার চললে, বা একশো ঘণ্টা চালু থাকলে কী হবে?” ক্ষুদ্র ভুলের জমা হওয়াই বাস্তব জগতের বড় বিপদ।

অনুশীলন

বেশিরভাগ কাজে আগে খাতায় অনুমান করুন, তারপর Python-এ মিলিয়ে নিন।

  1. আগে অনুমান, পরে চালানো: খাতায় লিখুন এগুলোর ফল কী হবে, তারপর REPL-এ মিলিয়ে নিন — 17 // 5, -17 // 5, 17 % -5, 2 ** -1, 9 / 3, int(-3.7), math.floor(-3.7).

    17 // 5 = 3; -17 // 5 = -4 (−3.4 থেকে নিচের দিকে); 17 % -5 = -3 — ভাগশেষের চিহ্ন ভাজকের মতো, আর যাচাই: −5 × −4 + (−3) = 17. 2 ** -1 = 0.5 (ঋণাত্মক ঘাতে float আসে); 9 / 3 = 3.0 (/ সবসময় float). int(-3.7) = -3 (শূন্যের দিকে কাটে), কিন্তু math.floor(-3.7) = -4. শেষ দুটির পার্থক্যই // বনাম C-র ভাগের পার্থক্য।

  2. চিরকাল চলা লুপ খুঁজুন: একজন লিখেছেন x = 0.0, তারপর while x != 1.0: x += 0.1. এটি কেন কখনো থামবে না? দুটি ভিন্ন উপায়ে ঠিক করুন।

    দশবার যোগের পর x হয় 0.9999999999999999, পরেরবার 1.0999999999999999 — কখনো ঠিক 1.0 হয় না, তাই != সবসময় True. উপায় ১: পূর্ণসংখ্যায় গুনুন — for i in range(10) চালিয়ে দরকার হলে x = i / 10 বানান; int-এর গোনায় কোনো ভুল জমে না। উপায় ২: শর্ত বদলান — while x < 1.0 - 1e-9, অথবা টাকা-জাতীয় হলে Decimal("0.1") দিয়ে যোগ করুন। মূল নীতি: লুপের শর্ত float-এর হুবহু সমতার ওপর রাখবেন না।

  3. DPS-এর হিসাব — কখন গোল করবেন: প্রতি মাসের শুরুতে ১০০০ টাকা জমা, মাসে ০.৫% সুদ, ১২ মাস। একবার প্রতি মাসের শেষে মোটকে পয়সায় গোল করে (ROUND_HALF_UP) হিসাব করুন, আরেকবার কোথাও গোল না করে শুধু শেষে। দুই উত্তর কি এক? কোনটি “ঠিক”? (এটিই এই পর্বের একমাত্র কোড লেখার কাজ; Decimal আর একটি for লুপ লাগবে — লুপ বিস্তারিত পর্ব ১২-এ, এখানে শুধু for m in range(12): অনুকরণ করুন।)

    প্রতি মাসে total = ((total + 1000) * Decimal("1.005")).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) করলে ১২ মাস পর পাবেন 12397.26. গোল না করে শেষে একবার quantize করলে পাবেন 12397.24 (আসল মান 12397.2401847…). দুই পয়সার পার্থক্য! কোনটি “ঠিক”, তা গণিত বলে না — বলে ব্যাংকের নিয়ম। বাস্তবে ব্যাংক প্রতি মাসে খাতায় পয়সা পর্যন্ত সুদ তোলে, তাই সাধারণত প্রথম পদ্ধতিটি তাদের বিবরণীর সাথে মেলে। float দিয়ে করলে পাবেন 12397.240184764403 — আসল মানের কাছাকাছি, কিন্তু শেষ কয়েক অঙ্ক Decimal-এর হিসাব থেকে আলাদা। শিক্ষা: “কখন গোল হবে” নিয়মের অংশ, আর Decimal সেটি স্পষ্টভাবে লেখার সুযোগ দেয়।

  4. AI অডিট: যেকোনো AI-কে বলুন: “Python-এ একটি দোকানের বিল হিসাবের প্রোগ্রাম লেখো, যেখানে পণ্যের দাম আর পরিমাণ থেকে মোট আর ৫% ছাড় বের হয়।” কোনো শর্ত দেবেন না। উত্তরটি এই পর্বের চেকলিস্ট দিয়ে যাচাই করুন। কয়টি ঘর টিক পেল?

    প্রায়ই দেখবেন দাম লেখা price = 120.50 জাতীয় float-এ, ছাড় হিসাব total * 0.05, আর শেষে round(total, 2) — অর্থাৎ প্রথম, তৃতীয় আর চতুর্থ ঘর ব্যর্থ। কখনো কখনো f"{total:.2f}" দিয়ে শুধু দেখানো হয়, ভেতরের ভুল থেকে যায়। এরপর এই পর্বের prompt-টি দিয়ে আবার চেষ্টা করুন — সাধারণত Decimal আর quantize সহ অনেক ভালো কোড আসবে। তবু “মাঝখানে পড়া” টেস্ট কেসের উত্তর হাতে হিসাব করে মিলিয়ে নিন, কারণ AI নিজের প্রত্যাশিত উত্তরেও ভুল করতে পারে।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমPython-এ গাণিতিক অপারেটরের অগ্রাধিকার সবচেয়ে বেশি থেকে সবচেয়ে কম — সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. ( ) — বন্ধনী
  2. ** — ঘাত
  3. -x — একক ঋণাত্মক চিহ্ন
  4. * / // % — গুণ ও ভাগ
  5. + - — যোগ ও বিয়োগ

Python-এ print(0.1 + 0.2 == 0.3) কী ছাপে, এবং কেন?

  • True, কারণ 0.1 + 0.2 = 0.3
  • TypeError, কারণ float তুলনা করা যায় না
  • False, কারণ float-এ 0.1 আর 0.2 আনুমানিক মান
  • False, কারণ এটি Python-এর একটি পরিচিত বাগ যা অন্য ভাষায় নেই

Python-এ -7 // 2 আর -7 % 2-এর মান কত?

  • -3 আর -1
  • -4 আর 1
  • -4 আর -1
  • -3.5 আর 1

Python 3-এ round(2.5) আর round(3.5)-এর ফল কী?

  • 2 আর 4
  • 3 আর 4
  • 2 আর 3
  • 3.0 আর 4.0

টাকার হিসাবের জন্য Python-এ ০.১০ টাকাকে সবচেয়ে নিরাপদভাবে কীভাবে রাখা যায়?

  • price = 0.10
  • price = Decimal(0.10)
  • price = round(0.10, 2)
  • price = Decimal("0.10")

Python-এ -2 ** 2 আর 2 ** 3 ** 2-এর মান কত?

  • 4 আর 64
  • -4 আর 64
  • -4 আর 512
  • 4 আর 512

C-র ৩২-বিট int-এর তুলনায় Python-এর int সম্পর্কে কোন কথাটি সঠিক?

  • Python-এর int-ও ৩২ বা ৬৪ বিটে সীমিত, তাই বড় হলে overflow হয়
  • Python-এর int দরকার অনুযায়ী বড় হয়, তাই overflow হয় না
  • বড় হলে Python-এর int নিজে থেকে float-এ বদলে যায়
  • বড় সংখ্যার জন্য Python 3-এ long লিখতে হয়

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. int নিখুঁত আর সীমাহীন (overflow নেই); float ৬৪-বিটের IEEE 754 আনুমানিক মান, প্রায় ১৫–১৭ অঙ্কের নির্ভুলতা।
  2. / সবসময় float; // নিচের দিকে গোল করে (-7 // 2 == -4), %-এর চিহ্ন ভাজকের মতো; দুটি একসাথে divmod().
  3. ** একক চিহ্নের আগে আর ডান থেকে: -2 ** 2 == -4, 2 ** 3 ** 2 == 512. সন্দেহ হলে বন্ধনী।
  4. float-এ == নয়, math.isclose(); round() banker's rounding মানে, আর float-এর “মাঝখান” প্রায়ই আসলে মাঝখান নয়।
  5. টাকা: Decimal("...") (string থেকে) + quantize(..., rounding=ROUND_HALF_UP), অথবা পয়সায় int. গোল করার নিয়ম ব্যবসার নীতি থেকে জেনে স্পষ্ট লিখুন।

পরের পর্বে: হিসাব তো হলো, এবার সিদ্ধান্তের পালা। তুলনা (<, ==), যুক্তি (and, or, not), walrus :=, আর বহুল ভুল-বোঝা প্রশ্ন — is আর ==-এর পার্থক্য।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান