অধ্যায় 18

কম্প্রিহেনশন — লুপকে এক লাইনে

লিস্ট, ডিকশনারি আর সেট কম্প্রিহেনশন, ছাঁকার if আর বেছে নেওয়ার if/else-এর পার্থক্য, আর কখন কম্প্রিহেনশন লিখতে নেই।

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

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

গত ছয়টি অধ্যায়ে একটি লেখার ধরন বারবার ফিরে এসেছে:

python
marks = [72, 45, 90, 33]

doubled = []
for mark in marks:
    doubled.append(mark * 2)
print(doubled)

doubled = [mark * 2 for mark in marks]
print(doubled)
text
[144, 90, 180, 66]
[144, 90, 180, 66]

উপরের চারটি লাইন আর নিচের একটি লাইন হুবহু একই কাজ করে।

চারটি লাইনের মধ্যে তিনটি আসলে কাজের কথা নয়, ব্যবস্থাপনার কথা — একটি খালি লিস্ট বানাও, তার উপর ঘোরো, প্রতিটিতে append করো। আসল সিদ্ধান্তটা কেবল একটাই: mark * 2। বাকিটা প্রতিবার একই।

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

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

  • লিস্ট কম্প্রিহেনশন লিখতে ও পড়তে
  • if দিয়ে ছেঁকে নিতে, আর if/else দিয়ে বেছে নিতে
  • ডিকশনারি আর সেট কম্প্রিহেনশন লিখতে
  • বলতে কখন কম্প্রিহেনশন লিখতে নেই
  • একটি অপাঠ্য কম্প্রিহেনশনকে আবার লুপে ভেঙে দিতে

আগে যা জানা লাগবে: নেস্টেড ডেটা — ডিকশনারির লিস্ট।


গঠনটা

text
[ যা বানাতে চাই   for  প্রতিটি   in  যেখান থেকে ]

ইংরেজিতে পড়লে বাক্যটা সোজা: "`mark 2 for each mark in marks`"*।

মনে রাখার একটি উপায়: for অংশটা আগে পড়ুন, তারপর শুরুর অংশটা। কোথা থেকে আসছে সেটা আগে জানলে কী বানানো হচ্ছে সেটা বোঝা সহজ।

ছেঁকে নেওয়া

শেষে একটি if জুড়লে কেবল যেগুলো শর্ত মেলে সেগুলোই আসে:

python
marks = [72, 45, 90, 33]

passed = [mark for mark in marks if mark >= 40]
print(passed)
text
[72, 45, 90]

এখানে প্রথম অংশটা কেবল mark — কোনো হিসাব নয়, যেমন আছে তেমনই। ছাঁকা আর রূপান্তর, দুটোই একসাথে করা যায়:

python
names = ["  rafi ", "AHMED", " dia"]

clean = [name.strip().lower() for name in names if name.strip()]
print(clean)
text
['rafi', 'ahmed', 'dia']

শর্তটি name.strip() — কোনো তুলনা নেই। অষ্টম অধ্যায়ের সত্যতার নিয়ম মনে করুন: খালি লেখা মিথ্যা। তাই কেউ কেবল স্পেস লিখে থাকলে সেটি বাদ পড়ে যায়।

if শেষে বনাম if শুরুতে

দুটি আলাদা জিনিস, আর জায়গাটাই পার্থক্য:

python
marks = [72, 33, 90]

labels = ["pass" if m >= 40 else "fail" for m in marks]
print(labels)
text
['pass', 'fail', 'pass']

শেষের if ছাঁকে — কিছু জিনিস ফলে আসে না, তাই ফলের দৈর্ঘ্য কম হতে পারে।

শুরুর if ... else বেছে নেয় — প্রতিটি জিনিসের জন্য কিছু না কিছু আসেই, তাই দৈর্ঘ্য একই থাকে। এটি পঞ্চদশ অধ্যায়ে দেখা সেই এক-লাইনের শর্ত, কেবল কম্প্রিহেনশনের ভেতরে।

দৈর্ঘ্য বদলায় কি না — এই প্রশ্নটাই দুটিকে আলাদা করার সবচেয়ে সহজ উপায়।

ডিকশনারি আর সেটও

একই গঠন, কেবল বন্ধনী আলাদা:

python
marks = {"rafi": 72, "ahmed": 45, "bilal": 90}

passed = {name: mark for name, mark in marks.items() if mark >= 50}
print(passed)

doubled = {name: mark * 2 for name, mark in marks.items()}
print(doubled)
text
{'rafi': 72, 'bilal': 90}
{'rafi': 144, 'ahmed': 90, 'bilal': 180}

কার্লি বন্ধনী আর একটি কোলন — ডিকশনারি কম্প্রিহেনশন। marks.items() প্রতিটি চক্রে একটি টাপল দেয়, আর name, mark সেটি আনপ্যাক করে, ঠিক যেমন লুপে করত।

কোলন ছাড়া একই কার্লি বন্ধনী দিলে সেট:

python
orders = [
    {"item": "pen"},
    {"item": "bag"},
    {"item": "pen"},
]

items = {order["item"] for order in orders}
print(sorted(items))
text
['bag', 'pen']

ষোড়শ অধ্যায়ের «আলাদা কয়টি» প্রশ্নটি এক লাইনে। pen দুবার এলেও সেটে একবারই থাকে।

সংখ্যাকে লেখায় — যেটি আপনি আগেই দেখেছেন

ত্রয়োদশ অধ্যায়ে এই লাইনটি এসেছিল, আর তখন বলা হয়েছিল ব্যাখ্যা পরে আসবে:

python
marks = [72, 45, 90]
print(", ".join(str(m) for m in marks))
print([str(m) for m in marks])
text
72, 45, 90
['72', '45', '90']

এখন পুরোটা পড়া যায়। str(m) for m in marks একটি কম্প্রিহেনশনই, কেবল বর্গবন্ধনী ছাড়া — এই রূপটির নাম জেনারেটর এক্সপ্রেশন, আর সে গোটা লিস্টটা একবারে বানায় না, একটি একটি করে join-কে দেয়। ফাংশনের একমাত্র আর্গুমেন্ট হলে বর্গবন্ধনী বাদ দেওয়া যায়, আর বড় ডেটায় সেটি স্মৃতি বাঁচায়।

কখন লিখবেন না

কম্প্রিহেনশন লেখা যায় বলেই লিখতে হবে, এমন নয়। এটি দেখুন:

python
rows = [["1", "2"], ["3", "x"]]

numbers = [int(c) for row in rows for c in row if c.isdigit()]
print(numbers)
text
[1, 2, 3]

লাইনটি ঠিক কাজ করে। কিন্তু এতে দুটি for আর একটি if আছে, আর ক্রমটা মনে রাখতে হয়: for গুলো বাইরে থেকে ভেতরে পড়তে হয়, যেভাবে নেস্টেড লুপে লেখা হতো। ছয় মাস পর এই লাইনটা পড়ে বোঝা কষ্ট।

লুপে লিখলে:

python
rows = [["1", "2"], ["3", "x"]]

numbers = []
for row in rows:
    for c in row:
        if c.isdigit():
            numbers.append(int(c))

print(numbers)
text
[1, 2, 3]

বেশি লাইন, কম চিন্তা। কোডের উদ্দেশ্য সংক্ষিপ্ত হওয়া নয়, পরিষ্কার হওয়া — আর বেশিরভাগ ক্ষেত্রে সংক্ষিপ্ততাই পরিষ্কার করে, কিন্তু সবসময় নয়।

কাজের নিয়ম হিসেবে: একটি for, আর বড়জোর একটি if — কম্প্রিহেনশন। তার বেশি হলে লুপ। আর কম্প্রিহেনশনের ভেতরে কখনো print বা append লিখবেন না; সেটি একটি মান বানানোর জিনিস, কাজ করার জিনিস নয়।


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

views.py:

python
# One list of records in, three different views out
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},
]

totals = [order["quantity"] * order["price"] for order in orders]
print("Totals    :", totals)

big = [order["item"] for order in orders if order["quantity"] * order["price"] > 100]
print("Over 100  :", big)

items = {order["item"] for order in orders}
print("Distinct  :", sorted(items))

by_item = {order["item"]: order["price"] for order in orders}
print("Price list:", by_item)

print("Grand     :", sum(totals))
text
Totals    : [45.0, 850.0, 240.0, 75.0]
Over 100  : ['bag', 'ink']
Distinct  : ['bag', 'ink', 'pen']
Price list: {'pen': 15.0, 'bag': 850.0, 'ink': 120.0}
Grand     : 1210.0

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

তিনটি জিনিস লক্ষ করার মতো।

totals আর sum(totals) আলাদা লাইনে। মোট বের করতে sum(order["quantity"] * order["price"] for order in orders) লেখা যেত, কিন্তু তাহলে প্রতিটি লাইনের মান আর হাতে থাকত না। একটি মধ্যবর্তী নাম প্রায়ই একটি এক-লাইনের কৌশলের চেয়ে দামি।

big-এর ভেতরে একই হিসাব দুবার। শর্তেও quantity * price, আর ফলেও লাগত। এখানে ফলে শুধু নাম নেওয়া হয়েছে বলে সমস্যা হয়নি — কিন্তু দুটোই লাগলে এটি কম্প্রিহেনশন ছেড়ে লুপে ফেরার একটি ভালো কারণ, কারণ লুপে মানটি একবার হিসাব করে একটি নামে রাখা যায়।

by_item একটি রেকর্ড হারিয়েছে। চারটি অর্ডার, কিন্তু ফলে তিনটি জোড়া — pen দুবার আছে, আর পঞ্চদশ অধ্যায়ের নিয়ম মতো শেষেরটি আগেরটিকে চুপচাপ বদলে দিয়েছে। এখানে দুটি pen-এর দাম একই বলে ক্ষতি হয়নি; আলাদা হলে নীরবে ভুল হতো। কম্প্রিহেনশন এই সমস্যাটা তৈরি করে না, কিন্তু সংক্ষিপ্ত বলে সেটি চোখে পড়ার সুযোগও কমিয়ে দেয়।


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

SyntaxError — if আর else একসাথে শেষে লিখলে [m for m in marks if m >= 40 else 0] চলে না। ছাঁকার if শেষে বসে আর তার কোনো else হয় না; বেছে নেওয়ার if ... else শুরুতে বসে: [m if m >= 40 else 0 for m in marks]।

ফলটি None-এ ভরা কম্প্রিহেনশনের ভেতরে এমন কিছু ডাকা হয়েছে যা কিছু ফেরত দেয় না — যেমন [items.append(x) for x in other]। append None ফেরত দেয়, তাই ফলটি None-এর একটি লিস্ট। কম্প্রিহেনশন মান বানানোর জন্য; কাজ করাতে চাইলে সাধারণ লুপ লিখুন।

NameError — কম্প্রিহেনশনের ভেরিয়েবলটি বাইরে নেই [m * 2 for m in marks] লেখার পর বাইরে m ব্যবহার করা যায় না। একাদশ অধ্যায়ে for লুপের ভেরিয়েবল লুপের পরেও টিকে থাকত; কম্প্রিহেনশনের ভেরিয়েবল থাকে না — সে কেবল ভেতরেই বাঁচে।

ডিকশনারি কম্প্রিহেনশনে জোড়ার সংখ্যা কম একই কী একাধিকবার তৈরি হয়েছে, আর শেষেরটি আগেরগুলোকে বদলে দিয়েছে। len() মিলিয়ে দেখুন।

লাইনটা পড়া যাচ্ছে না সেটাই যথেষ্ট কারণ। লুপে ভেঙে ফেলুন — কোডটা ধীর হবে না, আর পরের মানুষটি (সম্ভবত আপনি) কৃতজ্ঞ থাকবে।