অধ্যায় 17

নেস্টেড ডেটা — ডিকশনারির লিস্ট

বাস্তব ডেটার আকার: লিস্টের ভেতরে ডিকশনারি, ডিকশনারির ভেতরে লিস্ট। ভেতরে পৌঁছানো, গ্রুপ করা, আর কেন copy() এখানে যথেষ্ট নয়।

35 মিনিটPython 3.12
  1. 1সমস্যা
  2. 2বোঝা
  3. 3উদাহরণ
  4. 4অনুমান
  5. 5নিজে করা
  6. 6কঠিন করা

যে সমস্যাটা আমরা সমাধান করছি

গত তিনটি অধ্যায়ে চারটি পাত্র শেখা হয়েছে — লিস্ট, টাপল, ডিকশনারি, সেট। প্রতিটি উদাহরণে একটি করে, একস্তরে।

বাস্তব ডেটা এভাবে আসে না। একটি অর্ডারের তালিকা আসে এরকম:

text
customer   item     quantity   price
rafi       pen      3          15.0
ahmed      bag      1          850.0

এখানে দুটি স্তর। বাইরে একটি তালিকা — কয়টি অর্ডার হবে জানা নেই, তাই লিস্ট। ভেতরে প্রতিটি অর্ডার নিজেই একটি রেকর্ড, যার প্রতিটি অংশের একটি নাম আছে — তাই ডিকশনারি।

একটি API থেকে যা আসে, একটি CSV ফাইল পড়লে যা পাওয়া যায়, একটি ডেটাবেস যা ফেরত দেয় — প্রায় সবসময়ই এই আকার: ডিকশনারির একটি লিস্ট। আজকের অধ্যায়ে নতুন কোনো পাত্র নেই। আছে কেবল সেগুলোকে একটির ভেতরে আরেকটি রেখে ব্যবহার করা, আর তাতে যে ভুলগুলো হয় সেগুলো চেনা।

এই অধ্যায় শেষে আপনি পারবেন

  • ডিকশনারির লিস্টের ভেতরে পৌঁছাতে আর তার উপর ঘুরতে
  • গভীরে থাকা একটি KeyError বা IndexError পড়ে বলতে কোন স্তরে সমস্যা
  • ডিকশনারির লিস্ট থেকে গ্রুপ করে হিসাব বের করতে
  • .get()-এর ডিফল্ট দিয়ে অসম্পূর্ণ রেকর্ড সামলাতে
  • বলতে কেন নেস্টেড ডেটায় copy() যথেষ্ট নয়

আগে যা জানা লাগবে: সেট — পুনরাবৃত্তিহীন সংগ্রহ।


আকারটা চিনে নেওয়া

python
orders = [
    {"customer": "rafi", "item": "pen", "quantity": 3, "price": 15.0},
    {"customer": "ahmed", "item": "bag", "quantity": 1, "price": 850.0},
]

print(len(orders))
print(orders[0])
print(orders[0]["customer"])
print(orders[1]["price"])
text
2
{'customer': 'rafi', 'item': 'pen', 'quantity': 3, 'price': 15.0}
rafi
850.0

orders[0]["customer"] লাইনটাই এই অধ্যায়ের মূল দক্ষতা, আর সেটা বাঁ থেকে ডানে এক ধাপ করে পড়তে হয়:

  • orders — একটি লিস্ট
  • orders[0] — তার প্রথম জিনিস, যা একটি ডিকশনারি
  • orders[0]["customer"] — সেই ডিকশনারির customer কী-এর মান

সন্দেহ হলে থেমে গিয়ে মাঝের ধাপটা ছেপে দেখুন। print(orders[0]) লিখে চোখে দেখা যে ওটা একটি ডিকশনারি — এই একটা অভ্যাস নেস্টেড ডেটার অর্ধেক সমস্যা মিটিয়ে দেয়।

ঘোরা

python
orders = [
    {"customer": "rafi", "item": "pen", "quantity": 3, "price": 15.0},
    {"customer": "ahmed", "item": "bag", "quantity": 1, "price": 850.0},
]

for order in orders:
    total = order["quantity"] * order["price"]
    print(f"{order['customer']:<8} {order['item']:<6} {total:>8.2f}")
text
rafi     pen       45.00
ahmed    bag      850.00

for order in orders প্রতিটি চক্রে একটি ডিকশনারি দেয়, আর তার ভেতরে নাম ধরে পৌঁছানো যায়। সূচকের হিসাব কোথাও নেই।

f-string-এর ভেতরের কোটেশন। f"{order['customer']}" লাইনে ভেতরে একক কোটেশন ব্যবহার করা হয়েছে, কারণ বাইরের f-string দ্বৈত কোটেশনে লেখা। একই ধরনের কোটেশন ভেতরে-বাইরে দিলে পাইথন বুঝবে না লেখাটা কোথায় শেষ। দুটির একটি ভেতরে রাখুন, অন্যটি বাইরে।

অসম্পূর্ণ রেকর্ড

বাস্তব ডেটায় সব রেকর্ডে সব কী থাকে না:

python
orders = [{"customer": "rafi"}]
print(orders[0]["discount"])
text
KeyError: 'discount'

KeyError বলছে কী-টা নেই, কিন্তু কোন রেকর্ডে নেই তা বলছে না। বারো হাজার অর্ডারের মধ্যে একটিতে এটি ঘটলে খুঁজে বের করা কষ্টকর — সেজন্য enumerate দিয়ে ঘুরলে সংখ্যাটা হাতে থাকে।

কী-টা না থাকা স্বাভাবিক হলে .get():

python
orders = [{"customer": "rafi"}]
print(orders[0].get("discount", 0))
text
0

পঞ্চদশ অধ্যায়ের নিয়মটি এখানেও: থাকার কথা হলে বর্গবন্ধনী, না থাকা স্বাভাবিক হলে .get()। নেস্টেড ডেটায় প্রলোভনটা বেশি, কারণ .get() দিয়ে সব ত্রুটি চুপ করানো যায় — কিন্তু তাতে অসম্পূর্ণ ডেটা নীরবে শূন্য হয়ে হিসাবে ঢুকে যায়।

অন্য আকার — ডিকশনারির ভেতরে লিস্ট

python
by_customer = {
    "rafi": ["pen", "ink"],
    "ahmed": ["bag"],
}

by_customer["rafi"].append("bottle")
by_customer["dia"] = ["eraser"]

print(by_customer)

for customer, items in by_customer.items():
    print(f"{customer}: {len(items)} item(s) - {', '.join(items)}")
text
{'rafi': ['pen', 'ink', 'bottle'], 'ahmed': ['bag'], 'dia': ['eraser']}
rafi: 3 item(s) - pen, ink, bottle
ahmed: 1 item(s) - bag
dia: 1 item(s) - eraser

by_customer["rafi"].append("bottle") লাইনটি দুটি ধাপ এক লাইনে: আগে by_customer["rafi"] একটি লিস্ট বের করে আনে, তারপর সেই লিস্টে append করে।

এই আকারটি পঞ্চদশ অধ্যায়ের একটি সমস্যার উত্তর — সেখানে বলা হয়েছিল একটি কী একবারই থাকতে পারে, তাই একজনের একাধিক রেকর্ড রাখা যায় না। মান হিসেবে একটি লিস্ট রাখলে সেটা যায়।

গ্রুপ করা — সবচেয়ে কাজের প্যাটার্ন

ডিকশনারির লিস্ট থেকে «কার কী কী» বানানো:

python
orders = [
    {"customer": "rafi", "item": "pen"},
    {"customer": "ahmed", "item": "bag"},
    {"customer": "rafi", "item": "ink"},
]

grouped = {}
for order in orders:
    customer = order["customer"]
    if customer not in grouped:
        grouped[customer] = []
    grouped[customer].append(order["item"])

print(grouped)
text
{'rafi': ['pen', 'ink'], 'ahmed': ['bag']}

মাঝের তিনটি লাইনই পুরো কৌশল: কী-টা না থাকলে আগে একটি খালি লিস্ট বসিয়ে নাও, তারপর নির্দ্বিধায় append করো। এটি না করলে প্রথম অর্ডারেই KeyError আসত।

একই জিনিস এক লাইনে লেখা যায়:

python
orders = [
    {"customer": "rafi", "item": "pen"},
    {"customer": "rafi", "item": "ink"},
]

grouped = {}
for order in orders:
    grouped.setdefault(order["customer"], []).append(order["item"])

print(grouped)
text
{'rafi': ['pen', 'ink']}

setdefault(key, []) বলে «এই কী-টা থাকলে তার মান দাও, না থাকলে এই খালি লিস্টটা বসিয়ে তারপর দাও» — আর যা ফেরত আসে তাতেই append করা হয়। .get()-এর সাথে পার্থক্যটা গুরুত্বপূর্ণ: .get() ডিকশনারিতে কিছু বসায় না, setdefault বসায়।

তিন লাইনের রূপটি পড়তে সহজ, এক লাইনেরটি লিখতে দ্রুত। দুটোই বাস্তব কোডে দেখবেন।

আরও গভীরে

python
shop = {
    "name": "Corner Store",
    "staff": [
        {"name": "rafi", "shifts": ["mon", "tue"]},
        {"name": "dia", "shifts": ["wed"]},
    ],
}

print(shop["staff"][0]["shifts"][1])
print(len(shop["staff"]))

for person in shop["staff"]:
    print(f"{person['name']}: {len(person['shifts'])} shift(s)")
text
tue
2
rafi: 2 shift(s)
dia: 1 shift(s)

shop["staff"][0]["shifts"][1] — চারটি ধাপ: ডিকশনারি → লিস্ট → ডিকশনারি → লিস্ট। বাঁ থেকে ডানে পড়লে জটিল নয়, শুধু লম্বা।

তিন স্তরের বেশি গভীরে গেলে থামুন আর ভাবুন। লম্বা শিকল ভেঙে মাঝের ধাপটা একটি নামে রাখলে কোডটা পড়া যায়:

python
first_person = shop["staff"][0]
print(first_person["shifts"][1])

নেস্টেড ডেটায় copy() যথেষ্ট নয়

দ্বাদশ অধ্যায়ে দেখেছিলাম b = a দুটি নামকে একই লিস্টে লাগিয়ে দেয়, আর copy() তার সমাধান। নেস্টেড ডেটায় copy() শুধু এক স্তর রক্ষা করে:

python
import copy

template = {"name": "", "items": []}

a = template.copy()
b = copy.deepcopy(template)

a["items"].append("pen")
b["items"].append("bag")

print(template)
print(a)
print(b)
text
{'name': '', 'items': ['pen']}
{'name': '', 'items': ['pen']}
{'name': '', 'items': ['bag']}

a বানানো হয়েছিল copy() দিয়ে — নতুন একটি বাইরের ডিকশনারি, কিন্তু ভেতরের লিস্টটি সেই একই লিস্ট। তাই a["items"].append("pen") মূল template-কেও বদলে দিয়েছে।

b বানানো হয়েছিল deepcopy() দিয়ে, যা ভেতরের সবকিছুরও কপি বানায়। তাই b-এর বদল কোথাও ছড়ায়নি।

copy একটি মডিউল, তাই ব্যবহারের আগে import copy লাগে — মডিউল নিয়ে বিস্তারিত দ্বাবিংশ অধ্যায়ে।

কখন deepcopy লাগবে? যখন একটি নেস্টেড কাঠামোর কপি নিয়ে সেটি বদলাচ্ছেন, আর মূলটি অক্ষত রাখতে চান। খেয়াল রাখবেন deepcopy ধীর, আর বেশিরভাগ সময় দরকারও হয় না — বেশিরভাগ কোড ডেটা বদলায় না, নতুন ডেটা বানায়।

একটা সম্পূর্ণ উদাহরণ

orders.py:

python
# The shape data actually arrives in: a list of records
orders = [
    {"customer": "rafi", "item": "pen", "quantity": 3, "price": 15.0},
    {"customer": "ahmed", "item": "bag", "quantity": 1, "price": 850.0},
    {"customer": "rafi", "item": "ink", "quantity": 2, "price": 120.0},
    {"customer": "dia", "item": "pen", "quantity": 5, "price": 15.0},
]

print("Customer  Item    Qty      Total")
grand_total = 0.0
for order in orders:
    total = order["quantity"] * order["price"]
    grand_total = grand_total + total
    print(f"{order['customer']:<9} {order['item']:<6} {order['quantity']:>3} {total:>10.2f}")

print(f"{'':<20}{grand_total:>10.2f}")
print()

# Group the spend by customer: the key may not exist yet, so .get supplies a start
spend = {}
for order in orders:
    customer = order["customer"]
    spend[customer] = spend.get(customer, 0.0) + order["quantity"] * order["price"]

# Sort by amount by putting the amount first in a tuple
pairs = []
for customer, amount in spend.items():
    pairs.append((amount, customer))
pairs.sort(reverse=True)

print("Spend per customer, highest first:")
for amount, customer in pairs:
    print(f"  {customer:<8} {amount:>8.2f}")

distinct = set()
for order in orders:
    distinct.add(order["item"])

print()
print("Distinct items:", sorted(distinct))
print("Best customer :", pairs[0][1])
text
Customer  Item    Qty      Total
rafi      pen      3      45.00
ahmed     bag      1     850.00
rafi      ink      2     240.00
dia       pen      5      75.00
                       1210.00

Spend per customer, highest first:
  ahmed      850.00
  rafi       285.00
  dia         75.00

Distinct items: ['bag', 'ink', 'pen']
Best customer : ahmed

এই ছোট প্রোগ্রামটিতে গত চারটি অধ্যায়ের সবকটি পাত্র কাজে লেগেছে, আর প্রতিটি ঠিক যেখানে তার জায়গা সেখানেই।

লিস্ট — অর্ডারগুলো, কারণ কয়টি হবে জানা নেই আর ক্রম আছে।

ডিকশনারি — প্রতিটি অর্ডার, কারণ প্রতিটি অংশের একটি নাম দরকার; আর spend, কারণ সেখানে নাম ধরে জমা হচ্ছে।

টাপল — pairs-এর প্রতিটি জোড়া। আর এখানে একটি কৌশল: পরিমাণটা টাপলের প্রথমে রাখা হয়েছে, কারণ পাইথন টাপল সাজায় প্রথম মান ধরে। তাই সাধারণ .sort(reverse=True)-ই টাকার অঙ্ক ধরে সাজিয়ে দেয়, আর key= লাগে না।

সেট — distinct, কারণ প্রশ্নটাই ছিল «কয় রকমের», আর pen দুবার এসেছে।

আর spend.get(customer, 0.0) লাইনটি পঞ্চদশ অধ্যায়ের গোনার প্যাটার্নেরই রূপ — শুধু এক করে বাড়ানোর বদলে টাকার অঙ্ক যোগ হচ্ছে।


কিছু ভাঙা অবস্থা ও তার সমাধান

KeyError: 'discount' — কিন্তু কোন রেকর্ডে? for i, order in enumerate(orders): দিয়ে ঘুরুন, তাহলে ত্রুটির সময় i জানা থাকে। অথবা লুপের ভেতরে print(order) বসিয়ে শেষ যেটি ছাপা হয়েছিল সেটিই দোষী।

TypeError: string indices must be integers প্রায় সবসময় এর মানে আপনি ভেবেছিলেন হাতে একটি ডিকশনারি আছে, কিন্তু আছে একটি লেখা। যেমন orders যদি ডিকশনারির লিস্ট না হয়ে নামের লিস্ট হয়, তাহলে order["customer"] এই বার্তাটি দেবে। print(type(order)) দিয়ে দেখে নিন।

TypeError: list indices must be integers or slices, not str উল্টো ভুল — হাতে একটি লিস্ট, কিন্তু নাম দিয়ে খোঁজা হচ্ছে। সম্ভবত একটি [0] বাদ পড়েছে: orders["customer"] নয়, orders[0]["customer"]।

AttributeError: 'list' object has no attribute 'get' একই শ্রেণির ভুল, আরেক চেহারায়। যে জিনিসটিতে .get() ডাকছেন সেটি ডিকশনারি নয়, লিস্ট।

একটি কপি বদলালাম, মূলটাও বদলে গেল copy() অগভীর — ভেতরের লিস্ট বা ডিকশনারিগুলো ভাগ করা থেকে যায়। import copy করে copy.deepcopy(...) ব্যবহার করুন।

গ্রুপ করতে গিয়ে প্রথম চক্রেই KeyError grouped[key].append(...) লেখার আগে কী-টা তৈরি করা হয়নি। if key not in grouped: দিয়ে খালি লিস্ট বসান, অথবা setdefault ব্যবহার করুন।