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

ডেটার জন্য Python — list, dict, comprehension আর function-এর ঝালাই

Python for Data — Records, Tables, Comprehensions, Grouping, Generators, Dataclasses and Number Traps

Taught in Bangla, this lesson refreshes the Python a data worker uses daily: records as dicts and tables as lists of dicts, comprehensions, Counter, defaultdict, sorting, zip and enumerate, the mutable default trap, generators and dataclasses. It covers float pitfalls, money in integer paisa, Bangla string length, and a real in-browser timing of a Python loop against NumPy.

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

  • বাজারঘরের অর্ডারকে dict আর list of dict হিসেবে সাজানো, আর সারিভিত্তিক বনাম কলামভিত্তিক রূপের পার্থক্য বোঝা
  • Comprehension, Counter, defaultdict, sorted, itemgetter, zip, enumerate দিয়ে ছাঁকা, গোনা, দলে ভাগ আর সাজানো
  • স্পষ্ট input-output-এর function লেখা, mutable default-এর ফাঁদ এড়ানো, আর lambda কখন নয় তা জানা
  • Generator দিয়ে বড় ফাইল অল্প মেমরিতে পড়া, আর dataclass দিয়ে রেকর্ডের ছাঁচ বানানো
  • Float-এর ফাঁদ, পয়সায় টাকা, math.isclose, Decimal, বাংলা string-এর দৈর্ঘ্য, আর NumPy কেন দ্রুত তা মেপে দেখা

এই পর্ব Python শেখানোর পর্ব নয়। Python ভাষাটি ধাপে ধাপে শেখানো হয়েছে ABCL TECH-এর Python প্রোগ্রামিং কোর্সে। এখানে শুধু সেই অংশগুলো ঝালাই করা হবে, যা পরের মডিউলে NumPy আর pandas ধরার আগে প্রতিদিন লাগবে, আর যে ফাঁদগুলোতে ডেটার কাজে সবচেয়ে বেশি মানুষ পড়েন। কোথাও অচেনা লাগলে পাশের লিংকে Python কোর্সের সেই পর্বটি দেখে আসুন। কাজের পরিবেশ (Python install, venv, package) এসেছে পর্ব ৭-এ, আর notebook এসেছে পর্ব ৮-এ; এই পর্বের কোড পাতাতেই চলবে, তাই কিছু install করতে হবে না।

১ · রেকর্ড আর টেবিল — dict, list of dict আর বাজারঘরের অর্ডার

চায়ের দোকানের হিসাব খাতার কথা ভাবুন। প্রতিটি লাইনে একজন খদ্দেরের হিসাব: নাম, কী নিলেন, কত টাকা। একটি লাইন হলো একটি record; অনেক লাইন মিলে পুরো খাতা, মানে একটি টেবিল। ডেটার প্রায় সব কাজ এই দুটি জিনিস দিয়ে শুরু হয়।

Python-এ একটি রেকর্ডের সবচেয়ে স্বাভাবিক রূপ হলো dict: প্রতিটি ঘরের একটি নাম (চাবি বা key) আর একটি মান। বাজারঘরের (এই কোর্সের কাল্পনিক অনলাইন দোকান) একটি অর্ডার:

order = {"order_id": 1001, "district": "ঢাকা", "amount_paisa": 30200}
order["district"]        # 'ঢাকা'  — অবস্থান নয়, নাম দিয়ে খোঁজা

খেয়াল করুন, টাকা রাখা হয়েছে পয়সায়: ৩০২ টাকা মানে 30200. কেন, তা অংশ ৮-এ দেখবেন। অনেকগুলো অর্ডার একসঙ্গে রাখলে পাওয়া যায় একটি list, যার প্রতিটি উপাদান একটি dict. একে বলে “list of dict”, আর এটি একটি টেবিলের সবচেয়ে সরল Python রূপ। প্রতিটি dict একটি সারি (row), আর সব dict-এর একই চাবি মিলে কলাম। dict কীভাবে কাজ করে (hashing, গড়ে দ্রুত খোঁজা), সেটি আছে Python কোর্সের dictionary পর্বে, আর list-এর খুঁটিনাটি list পর্বে।

কিন্তু একই টেবিল আরেকভাবেও রাখা যায়: প্রতিটি কলামের জন্য একটি list, আর সেই list-গুলো কলামের নাম দিয়ে একটি dict-এ। একে বলে “dict of list” বা কলামভিত্তিক রূপ। নিচের ছবিতে একই তিনটি অর্ডার দুই রূপে দেখুন।

একই টেবিল, দুই রূপ — সারি ধরে আর কলাম ধরে

Row-oriented list of dicts vs column-oriented dict of lists

সারিভিত্তিক আর কলামভিত্তিক টেবিল list of dict — প্রতিটি সারি একটি dict {id: 1001, district: ঢাকা, paisa: 30200} {id: 1002, district: রাজশাহী, paisa: 120300} {id: 1003, district: চট্টগ্রাম, paisa: 77150} একটি অর্ডার যোগ করা সহজ: append একটি dict একই তথ্য, টেবিল হিসেবে order_id district amount_paisa 1001ঢাকা30200 1002রাজশাহী120300 1003চট্টগ্রাম77150 dict of list — প্রতিটি কলাম একটি list id: [1001, 1002, 1003] district: [ঢাকা, রাজশাহী, চট্টগ্রাম] paisa: [30200, 120300, 77150] একটি কলামের সব সংখ্যা পাশাপাশি → দ্রুত যোগফল
ছবি ৯.১ — ওপরে সারিভিত্তিক রূপ (list of dict): একটি অর্ডার পুরোটা একসঙ্গে, নতুন অর্ডার যোগ করা সহজ। নিচে কলামভিত্তিক রূপ (dict of list): একটি কলামের সব মান পাশাপাশি। হলুদ দাগের কলামটির যোগফল চাইলে নিচের রূপে শুধু একটি list হাঁটলেই হয়। NumPy আর pandas ভেতরে প্রায় এই কলামভিত্তিক ধারণাই ব্যবহার করে।

দুই রূপের মধ্যে আসা-যাওয়া zip দিয়ে এক লাইনেই হয় (অংশ ৪-এ দেখবেন)। কোনটি কখন? একটি একটি করে অর্ডার আসছে, প্রতিটিকে আলাদা করে যাচাই বা পাঠাতে হবে, তখন সারিভিত্তিক রূপ স্বাভাবিক; JSON API-ও সাধারণত এই আকারে ডেটা দেয়। আর একটি কলামের ওপর হিসাব (মোট, গড়, সর্বোচ্চ) করতে হলে কলামভিত্তিক রূপ সুবিধার। পর্ব ২০-এ দেখবেন, pd.DataFrame(orders) লিখলে pandas ঠিক এই list of dict নিয়েই একটি কলামভিত্তিক টেবিল বানায়।

মূল ধারণা

রেকর্ড = dict, টেবিল = list of dict. চাবির নাম সব রেকর্ডে এক রাখুন (amount_paisa একটিতে, amount আরেকটিতে নয়), একক নামেই লিখে দিন (_paisa, _days), আর একটি কলামে একই ধরনের মান রাখুন। এই তিনটি অভ্যাস পরের প্রতিটি পর্বের কাজ সহজ করবে।

২ · Comprehension — ছাঁকা আর রূপ বদলানো এক বাক্যে

ডেটার সবচেয়ে সাধারণ দুটি কাজ হলো কিছু সারি বেছে নেওয়া (filter) আর প্রতিটি সারি থেকে একটি নতুন মান বানানো (map). Python-এ দুটি কাজ একসঙ্গে লেখার সবচেয়ে পরিচিত উপায় list comprehension:

[রূপ_বদল  for রেকর্ড in টেবিল  if শর্ত]

বাংলায় পড়লে: “টেবিলের প্রতিটি রেকর্ডের জন্য, শর্ত সত্যি হলে, রূপ বদলানো মানটি নিন।” নিচের ধাপে দেখুন, প্রথম পাঁচটি অর্ডার থেকে শুধু ঢাকার অর্ডারের টাকা (পয়সা বাদ দিয়ে পূর্ণ টাকা) কীভাবে বের হয়। “পরে” চাপুন।

1dhaka = [o["amount_paisa"] // 1002         for o in orders[:5]3         if o["district"] == "ঢাকা"]
মান (Watch)
o["order_id"]
o["district"]
o["amount_paisa"]
শর্ত
এ পর্যন্ত
  1. সারি ১০০১, ঢাকা। শর্ত True, তাই বাঁ দিকের রূপ বদল চলে: 30200 // 100 = 302. নতুন list-এ প্রথম মান 302.
  2. সারি ১০০২, রাজশাহী। শর্ত False. রূপ বদলের অংশটি এই সারির জন্য চলেই না; list একই থাকে।
  3. সারি ১০০৩, চট্টগ্রাম। আবার False, কিছু যোগ হয় না।
  4. সারি ১০০৪, ঢাকা। শর্ত True. 297150 // 100 = 2971; শেষের ৫০ পয়সা // (পূর্ণসংখ্যার ভাগ) বাদ দিয়ে দিয়েছে। list এখন [302, 2971].
  5. সারি ১০০৫, খুলনা। False. পাঁচটি সারি দেখা শেষ।
  6. ফল। পাঁচটির মধ্যে দুটি সারি শর্ত পেরিয়েছে, তাই নতুন list-এ দুটি মান। মূল orders একটুও বদলায়নি; comprehension সবসময় একটি নতুন list বানায়।

একই ছাঁচে dict আর set-ও বানানো যায়: {o["order_id"]: o["district"] for o in orders} হলো অর্ডার নম্বর থেকে জেলার একটি lookup dict, আর {o["district"] for o in orders} হলো জেলাগুলোর set, মানে পুনরাবৃত্তি ছাড়া তালিকা। বর্গবন্ধনী [ ]-এর জায়গায় গোল বন্ধনী ( ) দিলে list তৈরি হয় না, একটি generator expression তৈরি হয়, যা অংশ ৬-এর বিষয়। সব রূপ, nested comprehension আর walrus (:=) আছে Python কোর্সের comprehension পর্বে।

দুই জায়গার if গুলিয়ে ফেলবেন না। শেষে if মানে ছাঁকনি: সারি বাদ পড়ে, list ছোট হয়। আর সামনে a if শর্ত else b মানে রূপ বদলের ভেতরে বাছাই: কোনো সারি বাদ পড়ে না, list-এর দৈর্ঘ্য একই থাকে। ["বড়" if o["amount_paisa"] >= 100000 else "ছোট" for o in orders] সব অর্ডারের জন্য একটি করে লেবেল দেয়। আর comprehension তিন লাইন ছাড়িয়ে গেলে বা ভেতরে দুটি for আর দুটি শর্ত জমলে সাধারণ for loop প্রায়ই বেশি পরিষ্কার।

৩ · দলে ভাগ আর গোনা — Counter আর defaultdict

বাজারঘরের ম্যানেজার জানতে চান: কোন জেলা থেকে কয়টি অর্ডার এসেছে, আর প্রতিটি জেলার মোট বিক্রি কত। এই কাজের নাম grouping: সারিগুলোকে একটি চাবি (এখানে জেলা) ধরে দলে ভাগ করে প্রতিটি দলের একটি সারাংশ বের করা। SQL-এ এটি GROUP BY, pandas-এ groupby (পর্ব ২৪)। খাঁটি Python-এ এর দুটি প্রধান হাতিয়ার আছে collections module-এ।

  • Counter: কোন জিনিস কতবার এলো, গোনে। Counter(o["district"] for o in orders) লিখলেই প্রতিটি জেলার অর্ডার-সংখ্যা। .most_common(k) সবচেয়ে বেশি k-টি দেয়।
  • defaultdict: একটি dict, যেখানে না থাকা চাবি চাইলে KeyError না দিয়ে একটি শুরুর মান বসিয়ে দেয়। defaultdict(int) শুরু করে 0 দিয়ে (যোগফলের জন্য), defaultdict(list) শুরু করে [] দিয়ে (দলের সব সারি জমানোর জন্য)।

সাধারণ dict দিয়েও কাজটি করা যায়: total[d] = total.get(d, 0) + p. কিন্তু প্রতিবার .get লিখতে হয়, আর ভুলে total[d] += p লিখলে প্রথম নতুন জেলাতেই KeyError. নিচের ধাপে প্রথম পাঁচটি অর্ডার দিয়ে একটি defaultdict ভরে উঠতে দেখুন।

1total = defaultdict(int)2for o in orders[:5]:3    total[o["district"]] += o["amount_paisa"]
মান (Watch)
o
নতুন চাবি?
total
  1. শূন্য খাতা। defaultdict(int) তৈরি হলো। এখনো কোনো চাবি নেই। int এখানে একটি “কারখানা”: না থাকা চাবির জন্য int() ডাকা হবে, যার মান 0.
  2. ১০০১, ঢাকা। “ঢাকা” চাবি নেই, তাই আগে 0 বসল, তারপর += 30200. সাধারণ dict হলে ঠিক এই লাইনে KeyError হতো।
  3. ১০০২, রাজশাহী। নতুন চাবি, 0 + 120300. খাতায় এখন দুটি জেলা।
  4. ১০০৩, চট্টগ্রাম। নতুন চাবি, 0 + 77150.
  5. ১০০৪, আবার ঢাকা। চাবি আগেই আছে, তাই নতুন কিছু বসে না; শুধু যোগ: 30200 + 297150 = 327350.
  6. ১০০৫, খুলনা। নতুন চাবি, 0 + 55600. পাঁচটি অর্ডার থেকে চারটি জেলা। চাবিগুলো এসেছে প্রথম দেখার ক্রমে, কারণ Python 3.7 থেকে dict ঢোকানোর ক্রম মনে রাখে।

এবার পুরো কাজটি চালিয়ে দেখি: seed দেওয়া random দিয়ে বাজারঘরের ২০টি অর্ডার বানানো, জেলাভিত্তিক গোনা আর যোগফল, তারপর মোট বিক্রি অনুযায়ী সাজিয়ে টাকায় ছাপা। হিসাব পুরোটাই পূর্ণসংখ্যার পয়সায়; টাকায় রূপ দেওয়া হয় শুধু ছাপার মুহূর্তে। প্রথমবার চালালে পাতায় Python নামতে কয়েক সেকেন্ড লাগতে পারে।

import random
from collections import Counter, defaultdict
from operator import itemgetter

rng = random.Random(42)                 # seed: সবার একই ডেটা
districts = ["ঢাকা", "চট্টগ্রাম", "রাজশাহী", "সিলেট", "খুলনা"]
orders = [
    {"order_id": 1001 + i,
     "district": rng.choice(districts),
     "amount_paisa": rng.randrange(20_000, 300_000, 50)}  # ২০০–৩০০০ টাকা
    for i in range(20)
]
print("প্রথম সারি:", orders[0])

def taka(paisa):                        # 125050 -> '৳1,250.50'
    t, p = divmod(paisa, 100)
    return f"৳{t:,}.{p:02d}"

count = Counter(o["district"] for o in orders)
total = defaultdict(int)                # নতুন চাবির শুরুর মান 0
for o in orders:
    total[o["district"]] += o["amount_paisa"]

rows = sorted(total.items(), key=itemgetter(1), reverse=True)
for name, paisa in rows:
    print(f"{name:<10}{count[name]:>3}টি {taka(paisa):>10}")
print("মোট:", taka(sum(total.values())))
print("সবচেয়ে বেশি অর্ডার:", count.most_common(2))

আসল আউটপুট:

প্রথম সারি: {'order_id': 1001, 'district': 'ঢাকা', 'amount_paisa': 30200}
খুলনা       6টি  ৳7,063.50
চট্টগ্রাম   4টি  ৳6,659.00
ঢাকা        6টি  ৳6,124.00
সিলেট       2টি  ৳2,189.50
রাজশাহী     2টি  ৳2,039.50
মোট: ৳24,075.50
সবচেয়ে বেশি অর্ডার: [('ঢাকা', 6), ('খুলনা', 6)]

পাঁচটি কাজ একসঙ্গে ঘটেছে। (১) random.Random(42) একটি নিজস্ব random generator, seed ৪২, তাই প্রত্যেক পাঠকের পর্দায় একই ২০টি অর্ডার। (২) Counter একটি generator expression থেকে জেলা গুনেছে। (৩) defaultdict(int) জেলাভিত্তিক পয়সা যোগ করেছে। (৪) sorted(total.items(), key=itemgetter(1), reverse=True) প্রতিটি (জেলা, পয়সা) জোড়ার দ্বিতীয় ঘর ধরে বড় থেকে ছোট সাজিয়েছে। (৫) divmod(paisa, 100) একসঙ্গে ভাগফল (টাকা) আর ভাগশেষ (পয়সা) দিয়েছে। শেষ লাইনে খেয়াল করুন: ঢাকা আর খুলনা দুটিরই ৬টি অর্ডার। most_common সমান গোনার জিনিসগুলো প্রথম দেখার ক্রমে দেয়, আর ২০টির প্রথম অর্ডারটিই ঢাকার, তাই ঢাকা আগে। “সবচেয়ে বেশি অর্ডার কোন জেলার” প্রশ্নের উত্তরে শুধু most_common(1) দেখালে খুলনার সমান দাবি চাপা পড়ে যেত। আর জেলার নামের পরের ফাঁকা জায়গা হয়তো পর্দায় সমান দেখাচ্ছে না: {name:<10} code point গুনে ফাঁকা বসায়, চোখে দেখা অক্ষর নয় (অংশ ৮)।

দলের সব সারি চাইলে defaultdict(list). groups[o["district"]].append(o) লিখলে প্রতিটি জেলার জন্য সেই জেলার অর্ডারগুলোর একটি list তৈরি হয়; তারপর যেকোনো সারাংশ (গড়, median, সর্বোচ্চ) আলাদা করে বের করা যায়। এটিই split-apply-combine ধারণার খাঁটি Python রূপ: ভাগ করা, প্রতিটি দলে হিসাব, ফল জোড়া। itertools.groupby নামের function-ও আছে, কিন্তু সেটি শুধু পাশাপাশি থাকা সমান চাবিগুলোকে এক দলে আনে, তাই আগে সাজিয়ে না নিলে ভুল ফল দেয়। বিস্তারিত Python কোর্সের lambda, functools আর itertools পর্বে।

৪ · সাজানো, zip, enumerate আর unpacking

সাজানো — key= আসলে কী করে

ক্রিকেটের স্কোরকার্ডে ব্যাটারদের রান অনুযায়ী সাজাতে হলে প্রতিটি ব্যাটারের পুরো তথ্য নয়, শুধু “রান” ঘরটি তুলনা করতে হয়। sorted(rows, key=...)-এর key ঠিক এই কাজ করে: প্রতিটি সারি থেকে তুলনার একটি মান বের করার একটি function. Python প্রতিটি সারির জন্য key একবার হিসাব করে, তারপর শুধু সেই মানগুলো তুলনা করে।

from operator import itemgetter
by_amount = sorted(orders, key=itemgetter("amount_paisa"), reverse=True)
by_two    = sorted(orders, key=itemgetter("district", "amount_paisa"))
top3      = by_amount[:3]

itemgetter("amount_paisa") একটি তৈরি function, যা lambda o: o["amount_paisa"]-এর সমান কাজ করে, পড়তে একটু পরিষ্কার আর সামান্য দ্রুত। দুটি নাম দিলে সে একটি tuple ফেরত দেয়, তাই by_two আগে জেলা ধরে, জেলা সমান হলে টাকা ধরে সাজায়। মনে রাখার তিনটি কথা:

  • sorted() নতুন list দেয়, list.sort() নিজের list বদলায় আর None ফেরত দেয়। x = rows.sort() লিখলে x-এ None থাকে; এটি খুব পরিচিত বাগ।
  • Python-এর sort stable: সমান key-র সারিগুলো আগের ক্রমেই থাকে। তাই “জেলা ধরে, তার ভেতরে টাকা ধরে” চাইলে আগে টাকা ধরে সাজিয়ে তারপর জেলা ধরে সাজালেও একই ফল আসে।
  • বাংলা শব্দ সাজালে ক্রম হয় Unicode code point ধরে, অভিধানের ক্রম নয়। ছোট তালিকায় প্রায়ই ঠিক দেখায়, কিন্তু যুক্তাক্ষর আর কার-চিহ্ন এলে অবাক করতে পারে। ভাষা মেনে সাজানোর (collation) জন্য আলাদা লাইব্রেরি লাগে।

zip, enumerate আর unpacking

zip(a, b) দুটি (বা বেশি) সংগ্রহকে চেইনের দাঁতের মতো জোড়ায় জোড়ায় মেলায়। কলামভিত্তিক ডেটা থেকে সারি বানাতে এটিই সবচেয়ে ছোট পথ, আর zip(*rows) উল্টো দিকে সারি থেকে কলাম বানায়:

cols = {"order_id": [1001, 1002, 1003],
        "district": ["ঢাকা", "রাজশাহী", "চট্টগ্রাম"],
        "amount_paisa": [30200, 120300, 77150]}
rows = [dict(zip(cols, vals)) for vals in zip(*cols.values())]
rows[1]   # {'order_id': 1002, 'district': 'রাজশাহী', 'amount_paisa': 120300}

for rank, (name, paisa) in enumerate(rows_sorted, start=1):
    print(rank, name, paisa)

শেষ লাইনে একসঙ্গে দুটি জিনিস: enumerate(..., start=1) প্রতিটি উপাদানের সঙ্গে ১ থেকে একটি ক্রমিক নম্বর দেয় (হাতে i = i + 1 লেখার দরকার নেই), আর (name, paisa) হলো unpacking: একটি জোড়ার দুই ঘরে আলাদা নাম বসানো। first, *rest = rows লিখলে প্রথমটি আলাদা, বাকি সব একটি list-এ। একটি সাবধানতা: zip সবচেয়ে ছোট সংগ্রহ শেষ হলেই থেমে যায়, কোনো সতর্কবার্তা ছাড়া। দুটি কলামের দৈর্ঘ্য না মিললে শেষের সারিগুলো চুপচাপ হারায়। Python 3.10 থেকে zip(a, b, strict=True) লিখলে দৈর্ঘ্য না মিললে ValueError হয়; ডেটার কাজে এটিই নিরাপদ। Unpacking-এর পুরো নিয়ম আছে tuple আর unpacking পর্বে।

৫ · Function — স্পষ্ট input-output, default আর mutable default-এর ফাঁদ

ডেটার কাজে একটি ভালো function দেখতে রান্নার রেসিপির মতো: কী কী উপকরণ লাগবে (input), শেষে কী পাবেন (output), আর রান্নাঘরের বাইরে কিছু বদলাবে না। যে function শুধু input দেখে output দেয়, বাইরের কোনো variable বদলায় না, তাকে বলে pure function. একই input-এ সবসময় একই output, তাই পরীক্ষা করা সহজ, notebook-এর cell আগে-পরে চালালেও ফল বদলায় না।

def district_totals(orders, key="district"):
    """প্রতিটি দলের মোট পয়সা; orders বদলায় না।"""
    total = defaultdict(int)
    for o in orders:
        total[o[key]] += o["amount_paisa"]
    return dict(total)

এখানে key="district" একটি default argument: না দিলে জেলা ধরে দল হয়, চাইলে district_totals(orders, key="payment") লিখে অন্য কলাম ধরেও দল করা যায়। ফেরত দেওয়ার আগে dict(total) করা হয়েছে, যাতে বাইরে কেউ ভুল চাবি পড়লে চুপচাপ নতুন 0 তৈরি না হয়। Function-এর ভেতরের যন্ত্র (call stack, keyword argument, docstring, type hint) আছে Python কোর্সের ফাংশন পর্বে; type hint আর ছোট test দিয়ে AI-এর কোড পরিচ্ছন্ন রাখা আসবে পর্ব ১১-এ।

যে ফাঁদে অভিজ্ঞরাও পড়েন — mutable default

Python-এ default মান তৈরি হয় একবার, যখন def লাইনটি চলে; প্রতিটি call-এ নতুন করে নয়। default যদি একটি সংখ্যা বা string হয়, তাতে সমস্যা নেই, কারণ এগুলো বদলানো যায় না। কিন্তু default যদি একটি list বা dict হয় (যা বদলানো যায়, অর্থাৎ mutable), তাহলে সব call একই list ভাগ করে নেয়। একে বলে mutable default argument-এর ফাঁদ। ধাপে ধাপে দেখুন।

1def add_item(item, basket=[]):2    basket.append(item)3    return basket4a = add_item("চাল")5b = add_item("ডাল")6c = add_item("তেল", [])7d = add_item("লবণ")
মান (Watch)
default list
a
b
c
d
  1. def চলল। এই মুহূর্তেই একটি খালি list তৈরি হয়ে function-এর গায়ে জমা থাকল (add_item.__defaults__-এ)। পরের প্রতিটি call, যেখানে basket দেওয়া হবে না, এই একটি list-ই পাবে।
  2. প্রথম call, “চাল”. basket দেওয়া হয়নি, তাই default list. তাতে “চাল” যোগ হলো, আর সেই list-টিই ফেরত গেল। a এখন default list-এর আরেকটি নাম।
  3. দ্বিতীয় call, “ডাল”. আশা ছিল নতুন ঝুড়ি ['ডাল']. কিন্তু default list আগের “চাল” নিয়েই বসে আছে, তাই ফল ['চাল', 'ডাল']. a আর b একই object, তাই a-ও বদলে গেছে যেন নিজে থেকে।
  4. তৃতীয় call, নিজের [] দিয়ে। এবার বাইরে থেকে একটি নতুন list পাঠানো হয়েছে, তাই c পেল শুধু ['তেল']. Default list ছোঁয়াই হয়নি। এ কারণেই বাগটি পরীক্ষায় প্রায়ই ধরা পড়ে না: test-এ সবাই নিজের list পাঠায়।
  5. চতুর্থ call, “লবণ”. আবার default list, তাতে এখন তিনটি জিনিস। a, b, d তিনটি নাম একটিই ঝুড়ি দেখায়। Notebook-এ একই cell বারবার চালালে এই ঝুড়ি ক্রমেই বড় হয়।

সমাধানটি ছোট আর প্রচলিত: default হিসেবে None দিন, আর function-এর ভেতরে None পেলে নতুন list বানান। নিচের কোডে ফাঁদ আর সমাধান পাশাপাশি চালিয়ে দেখুন; __defaults__ দেখায় function-এর গায়ে জমা থাকা default মান।

def add_item(item, basket=[]):          # ফাঁদ: default list একবারই তৈরি হয়
    basket.append(item)
    return basket

a = add_item("চাল")
b = add_item("ডাল")
print("a =", a)
print("b =", b, "| a is b:", a is b)
print("লুকানো default:", add_item.__defaults__)


def add_item_safe(item, basket=None):   # সমাধান: None, তারপর ভেতরে নতুন list
    if basket is None:
        basket = []
    basket.append(item)
    return basket

c = add_item_safe("চাল")
d = add_item_safe("ডাল")
print("c =", c, "| d =", d, "| c is d:", c is d)
print("নিরাপদ default:", add_item_safe.__defaults__)

আসল আউটপুট:

a = ['চাল', 'ডাল']
b = ['চাল', 'ডাল'] | a is b: True
লুকানো default: (['চাল', 'ডাল'],)
c = ['চাল'] | d = ['ডাল'] | c is d: False
নিরাপদ default: (None,)

প্রথম তিন লাইন ফাঁদটি সরাসরি দেখায়: a কখনো “ডাল” পায়নি, তবু তাতে “ডাল” আছে, কারণ a is b সত্যি, দুটি নাম একই list-এর। __defaults__-এ দেখা যাচ্ছে function-এর গায়ে জমে থাকা list-টি ইতিমধ্যে দুটি জিনিস নিয়ে বসে আছে। সমাধানের function-এ default None, যা বদলানো যায় না, আর প্রতিটি call নিজের নতুন list পায়: c is d মিথ্যা। শর্তটি if basket is None, if not basket নয়; কারণ কেউ ইচ্ছা করে খালি list পাঠালে not basket সত্যি হয়ে তার list ফেলে নতুন একটি বানিয়ে ফেলত। কেন mutable object ভাগ হয়ে যায়, তার গভীর ব্যাখ্যা আছে mutability আর copy পর্বে।

lambda — কখন হ্যাঁ, কখন না

lambda হলো নামহীন, এক expression-এর ছোট function: lambda o: o["amount_paisa"]. এর সবচেয়ে ভালো জায়গা হলো অন্য function-কে এক লাইনের নিয়ম দেওয়া, যেমন sorted(..., key=lambda o: (o["district"], -o["amount_paisa"])): জেলা ধরে ছোট থেকে বড়, তার ভেতরে টাকা বড় থেকে ছোট। কিন্তু তিন জায়গায় lambda এড়িয়ে চলুন:

  • নাম দিয়ে রাখতে চাইলে: fee = lambda p: p * 2 // 100 না লিখে def fee(p): লিখুন। Error-এর বার্তায় তখন function-এর আসল নাম আসে, <lambda> নয়, আর docstring লেখা যায়। PEP 8 (Python-এর লেখার নিয়ম) নিজেই এটি বলে।
  • নিয়ম একটি expression-এ ধরে না: lambda-তে শর্ত-শাখা, loop বা একাধিক লাইন চলে না। ঠেসে লিখলে কেউ পড়তে পারবে না।
  • তৈরি function আছে: itemgetter, attrgetter, len, str.lower সরাসরি key হিসেবে দেওয়া যায়।

৬ · Generator — বড় ফাইল একটি একটি সারি করে

বাজারঘরের এক বছরের অর্ডারের ফাইল হতে পারে কয়েক কোটি সারির। পুরোটা একটি list-এ তুলে ফেললে ল্যাপটপের মেমরি ফুরিয়ে যেতে পারে। অথচ অনেক প্রশ্নের জন্য পুরো ফাইল একসঙ্গে লাগেই না; “ঢাকার মোট বিক্রি কত” জানতে একটি একটি সারি দেখে যোগ করে গেলেই হয়। টিকিট কাউন্টারের লাইনের মতো: সবাই একসঙ্গে নয়, একজন একজন করে জানালায় আসেন।

Generator হলো এমন একটি function, যা return-এর বদলে yield দিয়ে একটি একটি মান দেয়, আর প্রতিটি মান দেওয়ার পর থেমে থাকে যতক্ষণ না পরের মান চাওয়া হয়। একে বলে lazy evaluation: কাজ হয় শুধু দরকারের মুহূর্তে। Python-এর ফাইল object নিজেই lazy: for line in f একবারে একটি লাইন পড়ে। নিচের কোডে একটি বড় CSV ফাইলের বদলে মেমরিতে বানানো এক লাখ সারির লেখা ব্যবহার করা হয়েছে, আর io.StringIO সেই লেখাকে ঠিক একটি খোলা ফাইলের মতো আচরণ করায়।

import io, sys, random

# বড় CSV ফাইলের বদলে মেমরিতে বানানো লেখা: ১,০০,০০০ সারি
rng = random.Random(7)
districts = ["ঢাকা", "চট্টগ্রাম", "খুলনা"]
lines = ["order_id,district,amount_paisa"]
for i in range(100_000):
    lines.append(f"{i},{rng.choice(districts)},{rng.randrange(20_000, 300_000)}")
csv_text = "\n".join(lines) + "\n"

def read_all(f):                    # সব সারি একসঙ্গে list-এ
    next(f)                         # header বাদ
    return [line.rstrip("\n").split(",") for line in f]

def read_lazy(f):                   # generator: চাইলে তবেই পরের সারি
    next(f)
    for line in f:
        yield line.rstrip("\n").split(",")

all_rows = read_all(io.StringIO(csv_text))      # ফাইলের মতোই পড়া যায়
lazy_rows = read_lazy(io.StringIO(csv_text))
print("list:     ", sys.getsizeof(all_rows), "bytes,", len(all_rows), "সারি")
print("generator:", sys.getsizeof(lazy_rows), "bytes")
print("প্রথম সারি:", next(lazy_rows))
print("দ্বিতীয় সারি:", next(lazy_rows))

dhaka = sum(int(r[2]) for r in read_lazy(io.StringIO(csv_text))
            if r[1] == "ঢাকা")
print("ঢাকার মোট:", dhaka, "পয়সা")

আসল আউটপুট:

list:      400492 bytes, 100000 সারি
generator: 108 bytes
প্রথম সারি: ['0', 'চট্টগ্রাম', '99088']
দ্বিতীয় সারি: ['1', 'চট্টগ্রাম', '45315']
ঢাকার মোট: 5371261249 পয়সা

দুটি function একই কাজ করে, পার্থক্য শুধু কখন। read_all এক লাখ সারি এখনই বানিয়ে একটি list-এ রাখে; read_lazy শুধু একটি “বুকমার্ক” তৈরি করে, যার আকার ১০৮ বাইট, ফাইল এক লাখ সারির হোক বা এক কোটির। next() চাইলে তবেই সে একটি লাইন পড়ে, ভাগ করে, দেয়, আর আবার থামে। ঢাকার মোট বের করার সময় একটি নতুন generator তৈরি করে sum-এ দেওয়া হয়েছে; প্রতিটি সারি যোগ হয়েই ফেলে দেওয়া যায়, তাই কখনো সব সারি একসঙ্গে মেমরিতে থাকে না। একটি সততার কথা: sys.getsizeof শুধু list-এর নিজের ঠিকানার সারি মাপে, ভেতরের সারি আর string নয়। ব্রাউজারের Python ৩২-বিট (WebAssembly), তাই প্রতি ঠিকানা ৪ বাইট আর ফল প্রায় ৪ লাখ বাইট; লেখকের ৬৪-বিট যন্ত্রে একই কোডে ফল ছিল 800984. সেখানে tracemalloc দিয়ে ভেতরের সব কিছুসহ মাপলে এই list প্রায় ৩২ MB নিয়েছিল (একটি run, Python 3.14.4). অর্থাৎ আসল খরচ getsizeof-এর দেখানো সংখ্যার কয়েক ডজন গুণ।

Generator একবারই হাঁটা যায়। শেষ মান দেওয়ার পর সে খালি; দ্বিতীয়বার for চালালে কোনো error ছাড়াই কিছুই আসে না। g = (o["amount_paisa"] for o in orders) লিখে প্রথমে sum(g), তারপর max(g) চাইলে দ্বিতীয়টি ValueError: max() iterable argument is empty দেয়। দুবার লাগলে হয় list বানান, নয়তো generator আবার তৈরি করুন (উপরের কোডে যেমন read_lazy(...) দ্বিতীয়বার ডাকা হয়েছে)। iterator-এর ভেতরের কথোপকথন আছে iterator, generator আর yield পর্বে।

৭ · dataclass — রেকর্ডের নাম-দেওয়া ছাঁচ

dict দিয়ে রেকর্ড রাখা সহজ, কিন্তু একটি দুর্বলতা আছে: বানান ভুল কেউ ধরে না। o["ammount_paisa"] লিখলে প্রোগ্রাম চলার সময় KeyError, আর o.get("ammount_paisa", 0) লিখলে আরও খারাপ: চুপচাপ 0. রেকর্ডের ধরন যখন স্থির (প্রতিটি অর্ডারে একই চারটি ঘর), তখন dataclass একটি পরিষ্কার ছাঁচ দেয়। ছাপানো ফর্মের মতো: কোন ঘরে কী লিখতে হবে, আগে থেকেই ছাপা।

from dataclasses import dataclass, field, asdict

@dataclass
class Order:
    order_id: int
    district: str
    amount_paisa: int
    items: list[str] = field(default_factory=list)

o = Order(101, "ঢাকা", 125050)
print(o)
print(o == Order(101, "ঢাকা", 125050))
print(asdict(o))

@dataclass
class Bad:
    items: list = []          # mutable default — dataclass নিজেই আটকায়

এই পর্বের লেখার সময় Python 3.14.4 দিয়ে নিজের যন্ত্রে সত্যিকারের run-এর আউটপুট (শেষ লাইনটি Bad class-এর সংজ্ঞা থেকে আসা error, try/except দিয়ে ধরে ছাপানো):

Order(order_id=101, district='ঢাকা', amount_paisa=125050, items=[])
True
{'order_id': 101, 'district': 'ঢাকা', 'amount_paisa': 125050, 'items': []}
ValueError: mutable default <class 'list'> for field items is not allowed: use default_factory

@dataclass চারটি ঘরের নাম আর type hint দেখে নিজেই __init__ (তৈরি করার function), পড়ার মতো ছাপা (__repr__) আর তুলনা (__eq__) লিখে দেয়। asdict আবার dict-এ ফেরায়, যাতে JSON বা pandas-এ পাঠানো যায়। আর অংশ ৫-এর ফাঁদটি এখানে আর ঘটতেই পারে না: list default দিলে dataclass class তৈরির মুহূর্তেই ValueError দিয়ে default_factory ব্যবহার করতে বলে, যা প্রতিটি নতুন অর্ডারের জন্য আলাদা নতুন list বানায়।

দুটি সতর্কতা। প্রথমত, type hint শুধু তথ্য; Order("১০১", 5, "অনেক") লিখলেও Python আপত্তি করে না। যাচাই চাইলে __post_init__-এ নিজে লিখতে হয়, বা mypy-এর মতো type checker চালাতে হয় (পর্ব ১১)। দ্বিতীয়ত, লক্ষ লক্ষ সারির সংখ্যার হিসাবে প্রতিটি সারিকে একটি Python object বানানো ধীর আর মেমরি-খরুচে; সেখানে কলামভিত্তিক NumPy বা pandas-ই ঠিক টুল। frozen=True (বদলানো যায় না এমন রেকর্ড), slots=True (কম মেমরি) আর property-র কথা আছে Python কোর্সের property আর dataclass পর্বে।

৮ · সংখ্যা আর লেখার ফাঁদ — float, পয়সা, Decimal আর বাংলা string

কেন 0.1 + 0.2 ঠিক 0.3 নয়

১ কে ৩ দিয়ে ভাগ করলে দশমিকে লিখতে হয় 0.3333…, কোথাও না কোথাও থামতে হয়, তাই একটু ভুল থেকেই যায়। কম্পিউটারের floating-point সংখ্যা (Python-এর float) দশমিকে নয়, দুইয়ের ভিত্তিতে (binary) লেখা। আর binary-তে 0.1 ঠিক তেমনই অসীম ভগ্নাংশ, যেমন দশমিকে ১/৩। তাই 0.1 আসলে জমা থাকে 0.1-এর খুব কাছের একটি সংখ্যা হিসেবে, আর দুটি কাছাকাছি সংখ্যা যোগ করলে ফলও শুধু “কাছাকাছি”।

import math
from decimal import Decimal, ROUND_HALF_UP

print(0.1 + 0.2)
print(0.1 + 0.2 == 0.3)
print(math.isclose(0.1 + 0.2, 0.3))
print(f"{0.1:.20f}")
print(round(2.675, 2))
print(Decimal("0.1") + Decimal("0.2"))
print(Decimal(0.1))
print(Decimal("2.675").quantize(Decimal("0.01"), rounding=ROUND_HALF_UP))

Python 3.14.4 দিয়ে সত্যিকারের run-এর আউটপুট:

0.30000000000000004
False
True
0.10000000000000000555
2.67
0.3
0.1000000000000000055511151231257827021181583404541015625
2.68
  • দুটি float কখনো == দিয়ে তুলনা নয়। math.isclose(a, b) দেখে দুটি সংখ্যা আপেক্ষিকভাবে খুব কাছাকাছি কি না (default সহনশীলতা প্রায় ১০−৯ ভাগ)। শূন্যের কাছের সংখ্যা তুলনা করলে abs_tol-ও দিতে হয়, কারণ আপেক্ষিক সহনশীলতা শূন্যের কাছে প্রায় কিছুই ছাড় দেয় না।
  • round(2.675, 2) কেন 2.67? কারণ 2.675 আসলে জমা আছে 2.67499999…-এর মতো একটি সংখ্যা হিসেবে। তার ওপর Python-এর round ঠিক মাঝখানের মানে জোড় সংখ্যার দিকে যায় (round(2.5) হলো 2, round(3.5) হলো 4), যাকে banker's rounding বলে।
  • Decimal(0.1) বনাম Decimal("0.1"): float থেকে বানালে float-এর ভুলটিই হুবহু চলে আসে, ৫৫ অঙ্ক পর্যন্ত। Decimal সবসময় string থেকে বানান।

টাকা রাখুন পূর্ণসংখ্যার পয়সায়

এ কারণেই এই কোর্সে বাজারঘরের সব দাম amount_paisa, একটি int. Python-এর int যত বড় চান তত বড় হয়, আর পূর্ণসংখ্যার যোগ-বিয়োগে কোনো গোলমাল নেই: ১৯.৯৯ + ২৫.৫০ + ০.১০ টাকা হলো 1999 + 2550 + 10 = 4559 পয়সা, ঠিক ৳45.59. ভাগ বা শতাংশ এলে (১৫% ছাড়, তিন কিস্তিতে ভাগ) কোথায় round করবেন তা আগে থেকে ঠিক করুন, আর ভাগশেষের পয়সা কার ভাগে যাবে তা-ও: ১০০০ পয়সা তিন ভাগে 333 + 333 + 334, মোট যেন ঠিক 1000 থাকে। হিসাবের নিয়ম আইন বা চুক্তিতে লেখা থাকলে (ভ্যাট, সুদ) Decimal আর তার rounding নিয়ম ব্যবহার করুন। মেশিন লার্নিং-এর feature হিসেবে অবশ্য float-ই স্বাভাবিক; সেখানে প্রশ্নটি হিসাবের পাই-পয়সা নয়, মডেলের অনুমান। পুরো আলোচনা আছে সংখ্যা ও হিসাব পর্বে।

বাংলা string — len() অক্ষর গোনে না

Python-এর string হলো Unicode code point-এর সারি। ইংরেজিতে একটি code point প্রায় সবসময় চোখে দেখা একটি অক্ষর। বাংলায় তা নয়: “বা” হলো দুটি code point (ব + া-কার), আর “ক্ষ” তিনটি (ক + হসন্ত + ষ)। চোখে যে একককে আমরা একটি অক্ষর বলি, Unicode-এর ভাষায় তা grapheme cluster. Python-এর standard library এটি গুনতে পারে না; তৃতীয় পক্ষের regex package-এর \X পারে।

import regex, unicodedata
for w in ["বাজারঘর", "ক্ষমা", "কৃষ্ণ", "রাজশাহী"]:
    print(w, len(w), len(regex.findall(r"\X", w)))

a = "ড়"            # ড় — একটি code point
b = "ড়"      # ড + নুক্তা — দুটি code point
print(a == b, len(a), len(b))
def nfc(s):
    return unicodedata.normalize("NFC", s)
print(nfc(a) == nfc(b))

Python 3.14.4 আর regex 2026.9.29 দিয়ে সত্যিকারের run-এর আউটপুট (প্রতিটি শব্দ, তারপর code point-এর সংখ্যা, তারপর grapheme-এর সংখ্যা):

বাজারঘর 7 5
ক্ষমা 5 2
কৃষ্ণ 5 2
রাজশাহী 7 4
False 1 2
True

“বাজারঘর” চোখে পাঁচটি অক্ষর (বা, জা, র, ঘ, র), কিন্তু len বলে ৭। এর ফল বাস্তবে: “নাম ২০ অক্ষরের বেশি হবে না” নিয়ম len দিয়ে লিখলে বাংলা নাম অকারণে আটকে যায়; লেখা মাঝখান থেকে কাটলে (name[:4]) একটি কার-চিহ্ন বা হসন্ত একা ঝুলে থাকতে পারে; আর অংশ ৩-এর ছাপায় সারি সমান দেখায় না। নতুন Unicode নিয়মে (সংস্করণ ১৫.১, ২০২৩ থেকে) বাংলার যুক্তাক্ষরও একটি grapheme ধরা হয়, তাই পুরোনো লাইব্রেরি “ক্ষমা”-কে ৩ বলতে পারে। আর শেষ দুই লাইন আরও বড় ফাঁদ দেখায়: “ড়” একটি code point দিয়েও লেখা যায়, দুটি দিয়েও; পর্দায় হুবহু এক, কিন্তু == বলে আলাদা। দুই জায়গা থেকে আসা ডেটায় একই জেলার নাম দুই রূপে থাকলে Counter সেটিকে দুটি জেলা গুনবে। সমাধান হলো তুলনার আগে Unicode normalization (সাধারণত NFC). ফাইলের encoding আর normalization পর্ব ১০-এর বিষয়, আর pandas-এ বাংলা লেখা পরিষ্কার পর্ব ২৩-এর; string-এর ভেতরের খুঁটিনাটি আছে Python কোর্সের string পর্বে।

৯ · কেন Python loop সংখ্যায় ধীর — আর NumPy কী বদলায়

এই পর্বের সব হাতিয়ার কয়েক হাজার, এমনকি কয়েক লাখ রেকর্ডে স্বচ্ছন্দে চলে। কিন্তু যখন কাজটি হলো লক্ষ লক্ষ সংখ্যার ওপর একই হিসাব (প্রতিটি দামকে বর্গ করা, প্রতিটি pixel-কে ২ দিয়ে গুণ করা), তখন Python-এর for loop ধীর হয়ে পড়ে। কারণটি বুঝতে নিচের ছবি দেখুন।

Python list বনাম NumPy array — মেমরিতে

A list holds pointers to separate int objects; an array holds raw numbers side by side

list আর array-এর মেমরির ছবি Python list: ঠিকানার সারি →→→→ type: intমান 0 type: intমান 1 type: intমান 2 type: intমান 3 প্রতি ধাপে: ঠিকানা ধরা, type দেখা, নতুন object NumPy array: dtype int64, টানা মেমরি 0123 ৮ বাইট করে পাশাপাশি; পুরো loop চলে C-এ
ছবি ৯.২ — Python list-এর প্রতিটি ঘরে শুধু একটি ঠিকানা; আসল সংখ্যাটি মেমরির অন্য কোথাও একটি পূর্ণ Python object, যার গায়ে type-সহ বাড়তি তথ্য। NumPy array-তে একই dtype-এর কাঁচা সংখ্যা পাশাপাশি, type লেখা থাকে একবারই। তাই loop-টি Python-এর প্রতি-ধাপের যাচাই ছাড়াই compiled C কোডে চলতে পারে।

Python-এর loop-এ প্রতিটি s += x * x-এর জন্য interpreter-কে কয়েকটি কাজ করতে হয়: x কোন type-এর তা দেখা, সেই type-এর গুণের নিয়ম খোঁজা, ফলের জন্য নতুন একটি int object বানানো, পুরোনোটি ফেলে দেওয়া। আসল গুণটি এক মুহূর্তের; বাকিটা হিসাবনিকাশের খরচ। NumPy-তে পুরো array-এর জন্য এই যাচাই একবার হয়, তারপর C-এর একটি টানা loop সংখ্যাগুলোর ওপর চলে। একে বলে vectorization. চলুন সত্যিই মাপি, time.perf_counter() দিয়ে, যা ছোট সময় মাপার জন্য সবচেয়ে সূক্ষ্ম ঘড়ি। প্রথমবার NumPy নামাতে কয়েক সেকেন্ড লাগতে পারে।

import time
import numpy as np

n = 1_000_000
data = list(range(n))                     # Python list: ১০ লাখ int object
arr = np.arange(n, dtype=np.int64)        # NumPy array: এক টানা মেমরি

t0 = time.perf_counter()
s_loop = 0
for x in data:                            # প্রতিটি ধাপে Python নিজে কাজ করে
    s_loop += x * x
t1 = time.perf_counter()
s_np = int(np.dot(arr, arr))              # পুরো লুপ C-এর ভেতরে
t2 = time.perf_counter()

print("ফল মিলেছে:", s_loop == s_np, s_np)
print(f"Python loop: {(t1 - t0) * 1000:8.1f} ms")
print(f"NumPy dot  : {(t2 - t1) * 1000:8.1f} ms")
print(f"অনুপাত প্রায় {(t1 - t0) / (t2 - t1):.0f} গুণ")
print("dtype না বললে:", np.arange(3).dtype)

ব্রাউজারে (Pyodide) একটি run-এর আসল আউটপুট:

ফল মিলেছে: True 333332833333500000
Python loop:    126.0 ms
NumPy dot  :      2.6 ms
অনুপাত প্রায় 48 গুণ
dtype না বললে: int32

দুটি পথে ফল হুবহু এক, 333,332,833,333,500,000; সূত্র দিয়েও মেলানো যায়: ০ থেকে n − 1 পর্যন্ত বর্গের যোগফল (n − 1)·n·(2n − 1) / 6. পার্থক্য শুধু সময়ে: এই run-এ Python loop প্রায় ১২৬ মিলিসেকেন্ড, NumPy প্রায় ২.৬, মানে প্রায় ৪৮ গুণ। এটি একটি যন্ত্রে, একটি ব্রাউজারে, একটি run-এর সংখ্যা; আপনার পর্দায় অন্য সংখ্যা আসবে, আর আবার চালালেও বদলাবে; একই ব্রাউজারে পরের দুটি run-এ অনুপাত এসেছিল প্রায় ৭৪ আর ৭৬ গুণ। লেখকের যন্ত্রে সাধারণ Python 3.14.4 আর NumPy 2.5.4 দিয়ে একই কোডে অনুপাত এসেছিল প্রায় ১১৫ গুণ (একটি run). শেষ লাইনটি একটি সত্যিকারের ফাঁদ দেখায়: ব্রাউজারের ৩২-বিট Python-এ dtype না বললে NumPy-র পূর্ণসংখ্যা int32, যার সীমা প্রায় ২১৪ কোটি। তখন np.arange(n)-এর বর্গ (১০১২ পর্যন্ত) উপচে পড়ে ভুল ফল দিত, কোনো error ছাড়া। ৬৪-বিট যন্ত্রে NumPy 2 থেকে default int64 (Windows-এও), তাই একই কোড এক যন্ত্রে ঠিক, আরেকটিতে ভুল হতে পারে। এ কারণেই কোডে dtype=np.int64 স্পষ্ট লেখা হয়েছে।

এখান থেকে তিনটি শিক্ষা পরের মডিউলে বারবার কাজে লাগবে। এক, সংখ্যার বড় কলামের ওপর হিসাব হলে list আর loop নয়, array. দুই, NumPy-তে dtype নিজে বেছে নেওয়া একটি সিদ্ধান্ত: int64-এর সীমা প্রায় ৯.২ × ১০১৮, তার বেশি হলে NumPy Python-এর মতো বড় হয় না, চুপচাপ উপচে পড়ে (overflow). তিন, সব কাজ vectorize করা যায় না, আর সবসময় করার দরকারও নেই; ১০০টি অর্ডারের জন্য সাধারণ loop-ই যথেষ্ট আর পরিষ্কার। Array-এর shape আর dtype আসবে পর্ব ১৩-এ, vectorization আর broadcasting পর্ব ১৫-এ, আর কখন loop-ই ঠিক তা পর্ব ১৯-এ। মাপজোকের সাধারণ নিয়ম আছে Python কোর্সের পারফরম্যান্স পর্বে।

আরও গভীরে। Float-এর আচরণ ঠিক করে IEEE 754 standard (প্রথম সংস্করণ ১৯৮৫, পরে ২০০৮ আর ২০১৯-এ হালনাগাদ), যার প্রধান স্থপতিদের একজন William Kahan. David Goldberg-এর ১৯৯১ সালের প্রবন্ধ “What Every Computer Scientist Should Know About Floating-Point Arithmetic” আজও এই বিষয়ে সবচেয়ে বেশি পড়া লেখাগুলোর একটি। একটি সূক্ষ্ম পরিবর্তন: Python 3.12 থেকে float-এর sum() একটি বেশি নির্ভুল যোগের পদ্ধতি ব্যবহার করে, তাই sum([0.1] * 10) এখন 1.0 দেয় (Python 3.14.4-এ নিজে চালিয়ে দেখা), অথচ loop-এ একটি একটি করে যোগ করলে আগের মতোই 0.9999999999999999. আর বাংলা grapheme-এর নিয়ম আছে Unicode-এর UAX #29 দলিলে।

পরীক্ষা ও ইন্টারভিউতে (বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এ ধরনের প্রশ্ন সাধারণ):

  • def f(x, acc=[])-এ সমস্যা কী? Default list def-এর সময় একবার তৈরি হয়, তাই সব call একই list ভাগ করে; None দিয়ে ভেতরে নতুন list বানান।
  • list comprehension আর generator expression-এর পার্থক্য? প্রথমটি সব মান এখনই একটি list-এ রাখে; দ্বিতীয়টি দরকারমতো একটি একটি মান দেয়, মেমরি কম লাগে, কিন্তু একবারই হাঁটা যায়।
  • 0.1 + 0.2 == 0.3 কেন False? Binary float-এ 0.1 আর 0.2 ঠিকঠাক রাখা যায় না; তুলনায় math.isclose, টাকায় পূর্ণসংখ্যার পয়সা বা Decimal.
  • sorted() আর list.sort()-এর পার্থক্য? প্রথমটি নতুন list ফেরত দেয়; দ্বিতীয়টি নিজের list বদলায় আর None দেয়। দুটিই stable.
  • NumPy কেন Python loop-এর চেয়ে দ্রুত? একই dtype-এর সংখ্যা টানা মেমরিতে, আর loop চলে compiled C কোডে, প্রতি ধাপে Python-এর type যাচাই ছাড়া।

১০ · AI-এর যুগে — AI সহকারীর লেখা ডেটা-কোড যাচাই

“এই অর্ডারগুলো জেলা ধরে যোগ করো” লিখলে একটি AI সহকারী কয়েক সেকেন্ডে কোড দেবে, আর বেশির ভাগ সময় তা চলবেও। সমস্যা হলো, এমন কোড চলেও ভুল ফল দিতে পারে, আর ভুলগুলো প্রায়ই এই পর্বের বিষয়গুলোতেই। AI সহকারীরা প্রায়ই যা করে:

  • টাকা float-এ রাখে আর == দিয়ে তুলনা করে; default হিসেবে [] বা {} বসায়; zip-এ দৈর্ঘ্য না মেলার কথা ভাবে না।
  • পুরোনো বা বানানো API লেখে: pandas-এ সরিয়ে দেওয়া DataFrame.append, NumPy 2-এ সরানো np.float বা np.int, বা এমন parameter যা আপনার version-এ নেই।
  • বাংলা নামের দৈর্ঘ্য len দিয়ে মাপে, normalization ছাড়াই নাম মেলায়, আর except: pass বা warnings.filterwarnings("ignore") দিয়ে error আর সতর্কবার্তা চাপা দেয়।
  • বিশ্বাসযোগ্য শোনায় এমন কিন্তু না-মাপা দাবি করে (“এটি ১০ গুণ দ্রুত”), আর পরে মডেল বানানোর সময় test ডেটা train-এ মিশিয়ে ফেলে বা train ডেটাতেই মাপা ফল দেখায়।

কোড চাওয়ার সময় ডেটার আকার, নিয়ম আর যাচাইয়ের উপায় নিজেই বলে দিন। নিচের প্রম্পটে খেয়াল করুন: আসল ডেটা নেই, বানানো তিনটি সারি; টাকার একক বলা আছে; আর ফাঁদগুলো আগেই নিষেধ করা হয়েছে।

আমি Python 3.14-এ শুধু standard library দিয়ে কাজ করছি। আমার ডেটা একটি list of dict, যেমন (বানানো উদাহরণ): [{"order_id": 1001, "district": "ঢাকা", "amount_paisa": 30200}, {"order_id": 1002, "district": "রাজশাহী", "amount_paisa": 120300}, {"order_id": 1003, "district": "ঢাকা", "amount_paisa": 297150}] ১. জেলা ধরে অর্ডার-সংখ্যা আর মোট টাকা বের করার একটি pure function লেখো। টাকা পুরো সময় int পয়সায় রাখবে, float ব্যবহার করবে না। ২. কোনো mutable default argument দেবে না, error চাপা দেবে না। ৩. জেলার নাম তুলনার আগে Unicode NFC normalization করবে, আর কেন, এক লাইনে বলো। ৪. মোট টাকা সমান হলে কোন জেলা আগে আসবে, সেটি স্পষ্ট করে বলো। ৫. ফলের সঙ্গে ৩টি assert দিয়ে ছোট test দাও, যার একটি খালি list-এর জন্য। কোনো function বা parameter নিয়ে নিশ্চিত না হলে বলো, বানিয়ে লিখবে না।

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

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

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।

প্র ০১Float-এর ছোট্ট ভুল কি বাস্তবে কখনো বড় ক্ষতি করেছে? ১৯৯১ সালের Patriot ক্ষেপণাস্ত্রের ঘটনা কী শেখায়?

১৯৯১ সালের ২৫ ফেব্রুয়ারি, উপসাগরীয় যুদ্ধের সময়, সৌদি আরবের ধাহরানে একটি মার্কিন Patriot ক্ষেপণাস্ত্র-প্রতিরক্ষা ব্যবস্থা একটি আগত Scud ক্ষেপণাস্ত্র আটকাতে ব্যর্থ হয়। Scud-টি একটি সেনা ব্যারাকে আঘাত করে, আর ২৮ জন মার্কিন সৈন্য নিহত হন। পরে ১৯৯২ সালে মার্কিন সরকারের হিসাব-নিরীক্ষা সংস্থা GAO-এর প্রতিবেদন কারণ হিসেবে একটি সংখ্যা-প্রকাশের সমস্যাকে চিহ্নিত করে।

ব্যবস্থাটি সময় গুনত দশমাংশ সেকেন্ডে, আর সেকেন্ডে রূপ দিতে গুণ করত ১/১০ দিয়ে। কিন্তু ১/১০ binary-তে অসীম ভগ্নাংশ, ঠিক যেমন অংশ ৮-এ দেখেছেন। যন্ত্রটি এটিকে একটি সীমিত বিটের (প্রায়ই ২৪ বিট বলা হয়) fixed-point সংখ্যায় কেটে রাখত, ফলে প্রতি গুণে খুব সামান্য ভুল থাকত। ভুলটি সময়ের সঙ্গে জমে। প্রতিবেদন অনুযায়ী ব্যবস্থাটি টানা প্রায় ১০০ ঘণ্টা চালু ছিল, আর তাতে জমা ভুল দাঁড়িয়েছিল প্রায় এক সেকেন্ডের এক-তৃতীয়াংশ। সেকেন্ডে দেড় কিলোমিটারের বেশি গতির একটি ক্ষেপণাস্ত্রের জন্য এটি কয়েকশো মিটারের ব্যবধান, ফলে রাডার ভুল জায়গায় লক্ষ্য খুঁজেছিল। বিভিন্ন বিশ্লেষণে খুঁটিনাটি সংখ্যায় ছোট পার্থক্য আছে, কিন্তু মূল কারণ নিয়ে মতৈক্য আছে। আরও দুঃখজনক তথ্য: ঘটনার আগেই সমস্যাটি জানা ছিল, সংশোধিত software পাঠানোও হচ্ছিল, আর নিয়মিত restart করার পরামর্শও ছিল।

ডেটার কাজে এর শিক্ষা সরাসরি। প্রথমত, একটি ছোট ভুল একবার ঘটলে ক্ষতি সামান্য, কিন্তু লাখো ধাপে জমলে বড় হয়। এক কোটি লেনদেনের যোগফল float-এ একটি একটি করে যোগ করলে শেষ অঙ্কগুলো নড়ে যায়। দ্বিতীয়ত, “ক্ষুদ্র ভুল” আপেক্ষিক: এক টাকার এক-লাখ ভাগের এক ভাগ ভুল দোকানে অদৃশ্য, কিন্তু সময়কে গতি দিয়ে গুণ করলে তা মিটার হয়ে যায়। তৃতীয়ত, সংখ্যা কোন এককে আর কোন আকারে রাখবেন (দশমাংশ সেকেন্ড, পয়সা, int64), তা নকশার সিদ্ধান্ত, পরে ঠিক করার খুঁটিনাটি নয়।

মূল উপলব্ধি: Float-এর ভুল গোলমাল নয়, নিয়মমাফিক; কোথায় তা জমবে আর কতটা জমলে ক্ষতি, সেটি আগে থেকে ভাবাই নিরাপদ সংখ্যা-নকশার কাজ।

প্র ০২টাকার হিসাবে float, পূর্ণসংখ্যার পয়সা আর Decimal — কোনটি কখন? round করার নিয়ম কেন এত গুরুত্বপূর্ণ?

একটি বহুল উদ্ধৃত ঘটনা দিয়ে শুরু করি। ১৯৮২ সালে কানাডার Vancouver Stock Exchange একটি নতুন সূচক চালু করে, শুরুর মান ১০০০। প্রচলিত বিবরণ অনুযায়ী প্রতিটি লেনদেনের পর সূচক নতুন করে হিসাব হতো আর তিন দশমিক ঘরে কেটে (round না করে) রাখা হতো। প্রতিবার কাটায় হারাত সামান্য, কিন্তু দিনে হাজার হাজার হালনাগাদে তা জমে প্রায় দুই বছরে সূচক নেমে আসে পাঁচশোর ঘরে, যখন আসল মান হওয়ার কথা ছিল এক হাজারের ওপরে। ১৯৮৩ সালের শেষ দিকে ভুলটি সংশোধন করা হয়। সংখ্যাগুলো বিভিন্ন সূত্রে সামান্য আলাদা, কিন্তু শিক্ষাটি একই: ভুলটি গণনার গোলমাল নয়, rounding নিয়মের, আর সবসময় এক দিকে হওয়ায় তা কাটাকাটি হয়নি, জমেছে।

এখন তিনটি পথের তুলনা। Float দ্রুত আর বৈজ্ঞানিক হিসাবে চমৎকার, কিন্তু ০.১ টাকাও ঠিকঠাক রাখতে পারে না; তাই হিসাবের খাতায় অনুপযুক্ত। পূর্ণসংখ্যার পয়সা (বা যেকোনো ক্ষুদ্রতম একক) সবচেয়ে সরল আর দ্রুত: যোগ-বিয়োগ নিখুঁত, database-এ BIGINT, JSON-এ সাধারণ সংখ্যা। কিন্তু ভাগ, শতাংশ বা বিনিময় হার এলে নিজেকেই ঠিক করতে হয় কোথায় round হবে। Decimal দশমিক ভিত্তিতে হিসাব করে, ০.১ ঠিক ০.১, আর rounding নিয়ম (half-up, half-even ইত্যাদি) স্পষ্ট করে বলা যায়; দাম হলো গতি, আর NumPy-তে স্বাভাবিকভাবে চলে না।

Rounding নিয়ম কেন এত গুরুত্বপূর্ণ? ঠিক মাঝখানের মান (২.৫) সবসময় ওপরে নিলে অনেক হিসাবের যোগফলে একটি নিয়মিত ঊর্ধ্বমুখী ঝোঁক জমে। Banker's rounding (জোড়ের দিকে) গড়ে এই ঝোঁক কাটায়; Python-এর round আর Decimal-এর default context দুটিই এই নিয়মে চলে। কিন্তু ক্রেতার রসিদে প্রচলিত নিয়ম হয়তো half-up; তাই কোনটি ঠিক, তা প্রোগ্রামার নয়, হিসাবের নীতি ঠিক করে।

মূল উপলব্ধি: টাকার জন্য পূর্ণসংখ্যার ক্ষুদ্রতম একক বা Decimal, কখনো float নয়; আর round কোথায়, কোন নিয়মে, ভাগশেষ কার, তা কোডে স্পষ্ট লিখে রাখাই আসল কাজ।

প্র ০৩Python কেন default মান একবারই তৈরি করে? এটি কি নকশার ভুল, নাকি ভেবেচিন্তে নেওয়া সিদ্ধান্ত?

Python-এর অফিসিয়াল tutorial নিজেই এই ফাঁদ নিয়ে একটি “গুরুত্বপূর্ণ সতর্কতা” রেখেছে: default মান একবারই হিসাব হয়, আর list বা dict-এর মতো mutable object হলে তা পরের call-গুলোতে জমতে থাকে। অর্থাৎ এটি কোনো লুকানো বাগ নয়, ভাষার লিখিত নিয়ম। প্রশ্নটি হলো, কেন এমন নিয়ম।

উত্তর লুকিয়ে আছে Python-এর একটি সরল নীতিতে: def একটি সাধারণ statement, যা চলার সময় একটি function object তৈরি করে। default-এর expression তখনই একবার হিসাব হয় আর ফলটি function-এর গায়ে (__defaults__) জমা থাকে। এতে নিয়মটি সহজ আর অনুমানযোগ্য: default-এ যা লেখা, তা def-এর মুহূর্তের মান। প্রতি call-এ নতুন করে হিসাব করলে প্রতিবার expression চালানোর খরচ লাগত, আর default-এর ভেতরে বাইরের কোনো variable থাকলে কোন মুহূর্তের মান নেওয়া হবে, সেই প্রশ্নও জটিল হতো।

অনেক ভাষা অন্য পথ নিয়েছে। যেমন JavaScript-এ default parameter প্রতি call-এ নতুন করে হিসাব হয়, তাই সেখানে এই ফাঁদ নেই। Python-এও “late-bound default” আনার প্রস্তাব এসেছে, PEP 671 (২০২১ সালে প্রস্তাবিত), যাতে আলাদা একটি চিহ্ন দিয়ে প্রতি call-এ হিসাব হওয়া default লেখা যেত; লেখার সময় পর্যন্ত এটি গৃহীত হয়নি। সমাজের ঐকমত্য তাই None দিয়ে ভেতরে নতুন object বানানোর পুরোনো রীতিতেই। Linter (যেমন Ruff বা Pylint) এই ভুল ধরতে পারে, আর dataclass তো সরাসরি নিষেধ করে।

মূল উপলব্ধি: এটি একটি সরল নিয়মের অপ্রত্যাশিত ফল, ভুল নয়; নিয়মটি জানলে None আর linter দিয়ে ফাঁদটি পুরোপুরি এড়ানো যায়।

প্র ০৪Python সংখ্যার হিসাবে এত ধীর হয়েও কীভাবে AI আর ডেটার প্রধান ভাষা হলো?

অংশ ৯-এ দেখেছেন, খাঁটি Python loop সংখ্যার হিসাবে NumPy-র চেয়ে অনেক গুণ ধীর। তবু আজ ডেটা সায়েন্স আর মেশিন লার্নিং-এর বেশির ভাগ কাজ Python-এ হয়। এই আপাত বৈপরীত্যের উত্তর হলো: ভারী হিসাব Python-এ হয়ই না। Python এখানে আঠার মতো (glue language): মানুষ Python-এ লেখে কী করতে হবে, আর আসল সংখ্যার কাজ চলে C, C++, Fortran বা GPU-র কোডে।

এই পথের ইতিহাস কয়েক দশকের। ১৯৯০-এর দশকের মাঝামাঝি Jim Hugunin-সহ কয়েকজন Numeric নামে একটি array package বানান। পরে Numarray নামে আরেকটি আসে, আর দুটির মধ্যে বিভক্তি দেখা দেয়। ২০০৫–২০০৬ সালে Travis Oliphant দুটিকে মিলিয়ে NumPy তৈরি করেন। তার ওপর গড়ে ওঠে SciPy, matplotlib, ২০০৮ সালে Wes McKinney-র শুরু করা pandas, ২০০৭ সালে শুরু হওয়া scikit-learn, আর পরে deep learning-এর framework-গুলো, যারা ভেতরে C++ আর GPU কোড চালায় কিন্তু ওপরে Python দেখায়। প্রায় সবাই একই array-ধারণা ভাগ করে, তাই একটির ফল আরেকটিতে সহজে যায়।

এর দাম আছে। যে কাজ vectorize করা যায় না (একটি একটি করে সিদ্ধান্ত নেওয়া জটিল loop), সেখানে Python ধীরই থাকে। গবেষকেরা একে কখনো “দুই ভাষার সমস্যা” বলেন: ধারণা পরীক্ষা হয় সহজ ভাষায়, দ্রুত সংস্করণ লিখতে হয় আরেক ভাষায়। Julia-র মতো ভাষা (২০১২ সালে প্রকাশ্যে আসে) এই সমস্যা কাটানোর লক্ষ্য নিয়েই এসেছিল, আর Numba, Cython বা নতুন Rust-ভিত্তিক টুল Python-এর ভেতরেই গতি আনার চেষ্টা করে। কোনটি সেরা, তা নিয়ে সমাজে মতভেদ আছে, আর উত্তর কাজের ধরনের ওপর নির্ভর করে।

মূল উপলব্ধি: Python-এর শক্তি গতি নয়, পড়ার সহজতা আর বিশাল ecosystem; গতি আসে কাজটিকে array-র ভাষায় লিখে compiled কোডের হাতে তুলে দেওয়া থেকে, যা পরের মডিউলের মূল দক্ষতা।

প্র ০৫সারিভিত্তিক (list of dict) আর কলামভিত্তিক (dict of list) রাখা — ডেটাবেস আর ডেটা-টুলের জগতে এই পছন্দ কেন এত বড় প্রশ্ন?

ছবি ৯.১-এ একই টেবিল দুই রূপে দেখেছেন। এটি শুধু Python-এর রুচির প্রশ্ন নয়; ডেটাবেসের ইতিহাসে এটি একটি বড় নকশা-সিদ্ধান্ত। দোকানের দৈনন্দিন কাজে (একটি অর্ডার ঢোকানো, একজন ক্রেতার ঠিকানা বদলানো) পুরো সারি একসঙ্গে লাগে, তাই সাধারণ ডেটাবেস সারি ধরে ডিস্কে রাখে। একে বলে OLTP ধরনের কাজ। কিন্তু বিশ্লেষণে (গত বছরের প্রতিটি জেলার মোট বিক্রি) কোটি সারির শুধু দুটি কলাম লাগে; সারি ধরে রাখলে বাকি সব কলামও অকারণে পড়তে হয়।

এ কারণে বিশ্লেষণের জন্য কলামভিত্তিক ডেটাবেসের ধারণা জনপ্রিয় হয়। ২০০৫ সালে Michael Stonebraker-সহ গবেষকদের C-Store প্রবন্ধ এর একটি প্রভাবশালী উদাহরণ। কলাম ধরে রাখলে তিনটি লাভ: শুধু দরকারি কলাম পড়া যায়; একই ধরনের মান পাশাপাশি থাকায় compression ভালো হয় (একটি জেলা-কলামে একই নাম বারবার); আর CPU একটানা একই type-এর সংখ্যার ওপর দ্রুত চলে, ঠিক অংশ ৯-এর NumPy-র মতো। আজকের Parquet ফাইল (পর্ব ১০-এ আসবে) আর Apache Arrow (২০১৬ সালে শুরু হওয়া একটি মেমরি-ভেতরের কলামভিত্তিক format) এই একই ধারণার ফল; pandas-এর নতুন সংস্করণ আর Polars-ও Arrow-এর সঙ্গে ঘনিষ্ঠভাবে কাজ করে।

তাহলে সবসময় কলাম কেন নয়? কারণ কলামভিত্তিক রূপে একটি সারি যোগ বা বদলানো ব্যয়বহুল: প্রতিটি কলামের list-এ আলাদা করে হাত দিতে হয়, আর compression-এর কারণে মাঝখানে বদলানো আরও কঠিন। তাই বাস্তবে দুটি একসঙ্গে চলে: লেনদেন আসে সারিভিত্তিক ব্যবস্থায়, পরে নিয়মিত কলামভিত্তিক বিশ্লেষণের জায়গায় কপি হয়। Python-এ ছোট পরিসরে একই ছবি: API থেকে আসা JSON সারি ধরে, pandas-এ ঢুকে তা কলাম হয়ে যায়।

মূল উপলব্ধি: ডেটা কীভাবে সাজানো, তা ঠিক করে কোন প্রশ্ন সস্তা আর কোনটি দামি; সারি লেখার জন্য, কলাম পড়া আর হিসাবের জন্য।

অনুশীলন

প্রথমটি হিসাব আর কোডের, দ্বিতীয়টি আউটপুট অনুমানের, তৃতীয়টি নকশা ও সমালোচনার, শেষটি কলাম থেকে সারি বানানোর।

  1. হিসাব আর কোড: ছয়টি অর্ডার (জেলা, পয়সা): (ঢাকা, 30200), (খুলনা, 55600), (ঢাকা, 29700), (সিলেট, 125000), (খুলনা, 4400), (ঢাকা, 100). (ক) প্রতিটি জেলার মোট আর গড় অর্ডার, টাকায়, হাতে হিসাব করুন। (খ) মোট অনুযায়ী বড় থেকে ছোট stable সাজালে ক্রম কী হবে? (গ) defaultdict দিয়ে কোড লিখে মিলিয়ে দেখুন।

    (ক) ঢাকা: 30200 + 29700 + 100 = 60000 পয়সা = ৳৬০০.০০, তিনটি অর্ডার, গড় ৳২০০.০০. খুলনা: 55600 + 4400 = 60000 = ৳৬০০.০০, দুটি অর্ডার, গড় ৳৩০০.০০. সিলেট: 125000 = ৳১,২৫০.০০, একটি অর্ডার, গড়ও তাই।

    (খ) সিলেট, তারপর ঢাকা, তারপর খুলনা। ঢাকা আর খুলনার মোট সমান; stable sort তাদের আগের ক্রম রাখে, আর defaultdict-এ ঢাকা আগে ঢুকেছিল।

    from collections import defaultdict
    orders = [("ঢাকা", 30200), ("খুলনা", 55600), ("ঢাকা", 29700),
              ("সিলেট", 125000), ("খুলনা", 4400), ("ঢাকা", 100)]
    total, n = defaultdict(int), defaultdict(int)
    for d, p in orders:
        total[d] += p
        n[d] += 1
    print(dict(total), {d: total[d] // n[d] for d in total})

    আসল আউটপুট (Python 3.14.4): {'ঢাকা': 60000, 'খুলনা': 60000, 'সিলেট': 125000} {'ঢাকা': 20000, 'খুলনা': 30000, 'সিলেট': 125000}. গড় এখানে পূর্ণসংখ্যার ভাগে (//) পয়সায়; ভাগ না মিললে পয়সার ভগ্নাংশ বাদ যায়, যা ছাপার আগে নিয়ম হিসেবে লিখে রাখা উচিত।

  2. আউটপুট অনুমান করুন: চালানোর আগে প্রতিটি লাইনের আউটপুট লিখুন, তারপর অংশ ৬-এর কোডের নিচে বসিয়ে চালিয়ে মিলিয়ে দেখুন।
    g = (x * x for x in [1, 2, 3])
    print(sum(g))
    print(sum(g))
    nums = [3, 1, 2]
    print(sorted(nums), nums.sort())
    print(nums)
    14
    0
    [1, 2, 3] None
    [1, 2, 3]

    প্রথম sum generator থেকে 1 + 4 + 9 = 14 পায়, আর generator খালি হয়ে যায়। দ্বিতীয় sum খালি generator পায়; কোনো error নেই, শুধু শুরুর মান 0. তৃতীয় লাইনে argument বাঁ থেকে ডানে হিসাব হয়: আগে sorted(nums) একটি নতুন সাজানো list বানায়, তারপর nums.sort() নিজের list সাজায় আর ফেরত দেয় None. শেষ লাইন দেখায় nums সত্যিই বদলে গেছে।

  3. নকশা ও সমালোচনা: একজন সহকর্মী (বা একটি AI সহকারী) নিচের function লিখেছেন। অন্তত পাঁচটি সমস্যা চিহ্নিত করে একটি ভালো সংস্করণ লিখুন।
    report = {}
    def add_order(o, seen=[]):
        seen.append(o["order_id"])
        amt = float(o["amount"]) * 1.15
        if amt == 115.0:
            print("ঠিক ১১৫ টাকা!")
        report[o["district"]] = report.get(o["district"], 0) + amt
        return seen
    • Mutable default: seen=[] সব call-এ একই list; অর্ডার নম্বর জমতেই থাকবে।
    • বাইরের variable বদলায়: report একটি global dict; function pure নয়, notebook-এ cell আবার চালালে যোগফল দ্বিগুণ হবে।
    • টাকা float-এ: * 1.15-এর ফল float, তাই যোগফলে ক্ষুদ্র ভুল জমবে; আর amt == 115.0 তুলনা প্রায় কখনো ভরসাযোগ্য নয়। 100 * 1.15 আসলে 114.99999999999999.
    • একক অস্পষ্ট: amount টাকা না পয়সা, নাম দেখে বোঝা যায় না; ১৫% কেন, কোথায় round হবে, লেখা নেই।
    • কাজ মিশে গেছে: একটি function একসঙ্গে মনে রাখে, হিসাব করে, ছাপায় আর global বদলায়; test করা প্রায় অসম্ভব।
    VAT_PERCENT = 15
    
    def with_vat(paisa: int) -> int:
        """পয়সায় দাম, ১৫% ভ্যাটসহ; অর্ধেক পয়সা ওপরে round."""
        return (paisa * (100 + VAT_PERCENT) + 50) // 100
    
    def district_totals(orders: list[dict]) -> dict[str, int]:
        total = defaultdict(int)
        for o in orders:
            total[o["district"]] += with_vat(o["amount_paisa"])
        return dict(total)

    এখন সব হিসাব পূর্ণসংখ্যায়, rounding-এর নিয়ম একটি লাইনে স্পষ্ট (ভ্যাটের আসল নিয়ম আপনার হিসাবের নীতি থেকে নিন), আর function-দুটি কিছু বাইরে বদলায় না। দেখা-হওয়া অর্ডারের তালিকা দরকার হলে সেটি আলাদা একটি set হিসেবে, ডাকার জায়গায় রাখুন।

  4. কলাম থেকে সারি আর ক্রমিক নম্বর: তিনটি কলাম দেওয়া আছে: names = ["রিনা", "সুমন", "আলম"], spent = [120050, 98000, 120050] (পয়সায়)। zip, sorted আর enumerate দিয়ে খরচ অনুযায়ী বড় থেকে ছোট একটি র‍্যাঙ্ক-তালিকা ছাপুন, ১ থেকে নম্বর দিয়ে। সমান খরচে কে আগে আসবে, আর কেন?
    pairs = sorted(zip(names, spent, strict=True),
                   key=lambda t: t[1], reverse=True)
    for rank, (name, p) in enumerate(pairs, start=1):
        print(rank, name, p)

    আসল আউটপুট (Python 3.14.4):

    1 রিনা 120050
    2 আলম 120050
    3 সুমন 98000

    রিনা আর আলমের খরচ সমান। Python-এর sort stable, আর reverse=True দিলেও সমান মানের উপাদানগুলোর মূল ক্রম বজায় থাকে; মূল তালিকায় রিনা আগে, তাই রিনা ১ নম্বরে। তবে বাস্তবের র‍্যাঙ্কিং-এ সমান খরচে দুজনকেই “১” দেওয়া উচিত কি না, সেটি নীতির প্রশ্ন; pandas-এর rank(method=...) এই পছন্দগুলো আলাদা করে দেয় (পর্ব ২৪-এর কাছাকাছি)। strict=True রাখা হয়েছে যাতে দুই কলামের দৈর্ঘ্য না মিললে চুপচাপ সারি না হারায়।

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

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

ক্রমএকটি বড় অর্ডার-ফাইল থেকে জেলাভিত্তিক বিক্রির রিপোর্ট বানানোর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📄 Generator দিয়ে ফাইল থেকে একটি একটি সারি পড়া
  2. 🧾 প্রতিটি সারিকে dict-এ রূপ দেওয়া, টাকা int পয়সায় আর জেলার নাম NFC-তে
  3. 🧹 ভাঙা বা অচেনা সারি ছেঁকে বাদ দেওয়া
  4. 🗂️ জেলা ধরে defaultdict(int)-এ পয়সা যোগ করা
  5. 📊 মোট অনুযায়ী sorted(key=...) দিয়ে সাজানো
  6. 🖨️ শুধু ছাপার মুহূর্তে পয়সাকে টাকায় রূপ দেওয়া

from collections import defaultdict; d = defaultdict(int); d["ঢাকা"] += 500; print(d["ঢাকা"], d["খুলনা"], len(d)) — আউটপুট কী?

  • KeyError: 'ঢাকা'
  • 500 0 1
  • 500 0 2
  • 500 None 1

একটি অনলাইন দোকানের কোডে if price_a + price_b == 0.3: লেখা, যেখানে price_a = 0.1, price_b = 0.2 (টাকায়, float). কোনটি সবচেয়ে ভালো সংশোধন?

  • দুই পাশে round(..., 2) দিয়ে তুলনা করা
  • দুটিকে Decimal(0.1) আর Decimal(0.2)-তে বদলানো
  • শুধু math.isclose দিয়ে তুলনা করা
  • দাম পূর্ণসংখ্যার পয়সায় রাখা (10 + 20 == 30)

def f(x, acc=[]): acc.append(x); return len(acc). তারপর f(1); f(2); print(f(3)) — কী ছাপা হবে?

  • 1
  • 3
  • [1, 2, 3]
  • TypeError

এক কোটি সারির একটি CSV ফাইল থেকে একটি কলামের যোগফল দরকার, একবারই। [parse(l) for l in f]-এর বদলে (parse(l) for l in f) ব্যবহারের প্রধান লাভ কী?

  • সব সারি একসঙ্গে মেমরিতে রাখতে হয় না
  • প্রতিটি সারির হিসাব বহুগুণ দ্রুত হয়
  • ফলটি বারবার হাঁটা যায়
  • যেকোনো সারিতে index দিয়ে সরাসরি যাওয়া যায়

Python 3-এ len("বাজারঘর")-এর মান কত, আর কেন?

  • 5, কারণ শব্দটিতে পাঁচটি অক্ষর
  • 21, কারণ প্রতিটি বাংলা অক্ষর ৩ বাইট
  • 7, কারণ len Unicode code point গোনে
  • Error, কারণ len বাংলা বোঝে না

১০ লাখ সংখ্যার বর্গের যোগফলে NumPy (np.dot(arr, arr)) খাঁটি Python loop-এর চেয়ে অনেক দ্রুত হওয়ার প্রধান কারণ কোনটি?

  • NumPy সবসময় GPU-তে চলে
  • একই ধরনের সংখ্যা টানা মেমরিতে, আর loop চলে compiled কোডে
  • NumPy কিছু সংখ্যা বাদ দিয়ে আনুমানিক ফল দেয়
  • Python-এর int-এ overflow হয় বলে loop ধীর

শব্দকোষ

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

মনে রাখুন

  1. রেকর্ড = dict, টেবিল = list of dict; একটি কলামের ওপর হিসাবে কলামভিত্তিক রূপ সুবিধার, আর zip দুই রূপের সেতু।
  2. Comprehension দিয়ে ছাঁকা ও রূপ বদল, Counter দিয়ে গোনা, defaultdict দিয়ে দলে যোগ, sorted(key=...) দিয়ে stable সাজানো।
  3. Function-এর input-output স্পষ্ট আর pure রাখুন; default-এ কখনো [] বা {} নয়, None দিয়ে ভেতরে নতুন বানান; নাম দিতে চাইলে lambda নয়, def.
  4. বড় ফাইলে generator (একবারই হাঁটা যায়); স্থির রেকর্ডে dataclass; টাকা পূর্ণসংখ্যার পয়সায়, float তুলনায় math.isclose, Decimal সবসময় string থেকে।
  5. বাংলায় len code point গোনে, অক্ষর নয়, আর তুলনার আগে NFC; সংখ্যার বড় হিসাবে Python loop ধীর, NumPy array দ্রুত, কিন্তু dtype আপনার সিদ্ধান্ত।

পরের পর্বে: ডেটা আসে ফাইলে। CSV, JSON আর Parquet-এর পার্থক্য, বাংলা লেখার encoding আর normalization, আর একটি ফাইল ঠিকঠাক পড়া হলো কি না কীভাবে যাচাই করবেন।

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

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