সরাসরি পর্বে যান
পর্ব ১৫ · ৫৫-এর মধ্যে · মডিউল ৩

tuple আর unpacking — স্থির রেকর্ড আর বাক্স খোলা

Tuples & Unpacking — fixed records and opening the box

Taught in Bangla, this lesson covers Python tuples as fixed records: why the comma makes the tuple, what immutability does and does not guarantee, tuple methods, unpacking and its error messages, star unpacking, returning several values from a function and namedtuple.

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

  • কখন list আর কখন tuple — “একই রকম অনেক জিনিস” বনাম “কয়েকটি সম্পর্কিত মানের রেকর্ড”
  • tuple বানানোর সব উপায়, আর এক উপাদানের tuple-এর কমার ফাঁদ
  • immutability আসলে কতটুকু রক্ষা দেয়, আর কেন tuple dict-এর চাবি হতে পারে
  • basic, starred আর nested unpacking; unpacking-এর error পড়া; ফাংশন থেকে কয়েকটি মান ফেরত
  • namedtuple আর typing.NamedTuple দিয়ে পড়তে সহজ রেকর্ড, আর AI-এর লেখা tuple-কোড যাচাই

১ · tuple কী — একটি স্থির রেকর্ড

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

কিন্তু কিছু তথ্য এমন, যা একসাথে মিলে একটিই জিনিস বোঝায়। ঢাকার অবস্থান বলতে দুটি সংখ্যা লাগে — অক্ষাংশ প্রায় 23.81, দ্রাঘিমাংশ প্রায় 90.41. এই দুটি আলাদা থাকলে কোনো অর্থ নেই; একসাথে থাকলে একটি বিন্দু। একজন শিক্ষার্থীর রেকর্ড — নাম, রোল, GPA — তিনটি ভিন্ন ধরনের মান, কিন্তু একজন মানুষের। এমন “কয়েকটি সম্পর্কিত মান, যারা একসাথে থাকে” রাখার জন্য Python-এ আছে tuple.

📋 list — একই রকম অনেক, বদলায়

ক্লাসের সব শিক্ষার্থীর GPA. নতুন কেউ ভর্তি হলে যোগ হবে, কেউ চলে গেলে বাদ যাবে। প্রতিটি জিনিসের ভূমিকা একই — “একজনের GPA”.

gpas = [3.75, 3.90, 3.50]

📌 tuple — কয়েকটি সম্পর্কিত, স্থির

একজন শিক্ষার্থীর রেকর্ড। প্রতিটি ঘরের ভূমিকা আলাদা — প্রথমটি নাম, দ্বিতীয়টি রোল, তৃতীয়টি GPA. ঘরের সংখ্যা আর অর্থ নির্দিষ্ট।

student = ("রহিম", 12, 3.75)

পরীক্ষার প্রবেশপত্র (admit card)-এর কথা ভাবুন। তাতে ছাপা থাকে নাম, রোল, রেজিস্ট্রেশন নম্বর আর কেন্দ্র। কার্ড ছাপা হয়ে গেলে কেউ কলম দিয়ে রোল বদলে দিতে পারে না — ভুল থাকলে নতুন কার্ড ছাপতে হয়। এটা tuple. আর বাজারের ব্যাগ — আলু, পেঁয়াজ, ডিম ঢোকে, বের হয়, যতটা খুশি। এটা list.

tuple-এর মূল বৈশিষ্ট্য হলো এটি immutable: একবার তৈরি হলে এর ঘরগুলোতে অন্য object বসানো যায় না, ঘর যোগ বা বাদও দেওয়া যায় না। তবু এটি list-এর মতোই একটি sequence — ক্রম আছে, index দিয়ে পড়া যায়, লুপ চালানো যায়।

মূল ধারণা

tuple-কে শুধু “বদলানো যায় না এমন list” ভাবলে অর্ধেক বোঝা হয়। আসল প্রশ্ন হলো ঘরগুলোর অর্থ কি অবস্থান দিয়ে ঠিক হয়? (অক্ষাংশ, দ্রাঘিমাংশ)-এ প্রথম ঘর সবসময় অক্ষাংশ — তাই tuple. শিক্ষার্থীদের নামের সারিতে ৫ নম্বর নামের আলাদা কোনো অর্থ নেই — তাই list. মনে রাখুন: list হলো “অনেকগুলো একই রকম”, tuple হলো “একটি জিনিসের কয়েকটি দিক”.

২ · tuple বানানো — কমাই আসল, বন্ধনী নয়

সবচেয়ে পরিচিত রূপ হলো বন্ধনীর ভেতরে কমা দিয়ে মান লেখা: dhaka = (23.81, 90.41). কিন্তু এখানে একটি গোপন সত্য আছে — tuple বানায় কমা, বন্ধনী শুধু দলবদ্ধ করে পড়তে সাহায্য করে। dhaka = 23.81, 90.41 লিখলেও একই tuple তৈরি হয়। কয়েকটি মান কমা দিয়ে একটি tuple-এ ভরে ফেলাকে বলে packing. পর্ব ০৬-এ a, b = b, a দিয়ে যে swap শিখেছিলেন, তার ডান দিকে আসলে এই packing-ই ঘটে।

এই নিয়ম থেকেই নতুনদের সবচেয়ে পরিচিত ফাঁদটি আসে। ধাপে ধাপে দেখুন, চারটি প্রায় একই রকম লাইন কী কী বানায়:

1x = (5)2y = (5,)3z = 5,4e = ()
নাম → object আর তার type
x
y
z
e
  1. (5) — শুধু একটি সংখ্যা। গণিতে (2 + 3) * 4-এ বন্ধনী যেমন শুধু হিসাবের ক্রম ঠিক করে, এখানেও তাই। (5) আর 5 একই জিনিস — একটি int. কোনো tuple তৈরি হয়নি।
  2. (5,) — এক উপাদানের tuple. শেষের ওই ছোট কমা (trailing comma) Python-কে বলে দেয়: “এটি একটি tuple, যাতে একটিই জিনিস আছে।” দেখতে অদ্ভুত, কিন্তু এক উপাদানের tuple লেখার এটিই একমাত্র literal রূপ।
  3. 5, — বন্ধনী ছাড়াও tuple. প্রমাণ হলো কমাই আসল। এই কারণে return 5, লিখলে ফাংশন একটি tuple ফেরত দেয়, আর লাইনের শেষে ভুল করে একটি কমা পড়ে গেলে একটি সংখ্যা নিঃশব্দে tuple হয়ে যায় — পরে অদ্ভুত error আসে।
  4. () — খালি tuple. এখানে কমা ছাড়াই tuple, কারণ খালি বন্ধনীকে Python বিশেষভাবে চেনে। tuple() লিখলেও খালি tuple পাবেন। CPython-এ খালি tuple একটিই থাকে, সবাই সেটিই ভাগ করে নেয় — বদলানো যায় না বলে এতে কোনো ঝুঁকি নেই।
  5. এক লাইনের নিয়ম। y আর z দুটোই tuple, কারণ দুটোতেই কমা আছে। একটি মাত্র ব্যতিক্রম খালি tuple. সন্দেহ হলে REPL-এ type(...) দেখে নিন।

আরেকটি উপায় হলো tuple() ফাংশন, যা যেকোনো iterable থেকে tuple বানায়: tuple([1, 2, 3]) দেয় (1, 2, 3), tuple("ঢাকা") দেয় প্রতিটি অক্ষরের একটি tuple, tuple(range(3)) দেয় (0, 1, 2). একটি list-কে “জমিয়ে” স্থির করতে চাইলে এটিই সহজ পথ।

বন্ধনী কখন লাগবেই। কমাই tuple বানালেও কিছু জায়গায় বন্ধনী ছাড়া Python ভুল বোঝে। print(1, 2) দুটি আলাদা argument ছাপে, একটি tuple নয় — tuple ছাপতে print((1, 2)). একইভাবে ফাংশনে tuple পাঠাতে, বা অন্য অপারেটরের সাথে মেশাতে বন্ধনী দিন। সহজ নিয়ম: পড়তে সুবিধা হলে সবসময় বন্ধনী লিখুন, আর এক উপাদানে কমা ভুলবেন না।

৩ · immutability — কী বদলায় না, আর কী বদলাতে পারে

tuple-এর কোনো ঘরে নতুন কিছু বসাতে গেলে Python সরাসরি থামিয়ে দেয়: dhaka[0] = 24.0 লিখলে আসে TypeError: 'tuple' object does not support item assignment. tuple-এ append, remove, sort — এমন কোনো method-ই নেই। বদলাতে চাইলে নতুন tuple বানাতে হয়: dhaka = (24.0, dhaka[1]). এতে পুরোনো tuple বদলায়নি; dhaka নামের লেবেলটি নতুন একটি object-এ সরেছে (পর্ব ০৬-এর লেবেল-ছবি মনে করুন)।

কিন্তু “বদলায় না” কথাটির একটি সূক্ষ্ম সীমা আছে। পর্ব ০৫-এ দেখেছেন, Python-এ সবকিছু object, আর একটি সংগ্রহ আসলে object-গুলোর reference রাখে। tuple যা প্রতিজ্ঞা করে তা হলো: কোন ঘরে কোন object, তা কখনো বদলাবে না। সেই object নিজে যদি mutable হয় — যেমন একটি list — তবে তার ভেতরটা বদলাতেই পারে। একে বলে shallow immutability (অগভীর অপরিবর্তনীয়তা)। ধাপে ধাপে দেখুন:

1team = ("বাংলাদেশ", ["তামিম", "মুশফিক"])2team[1].append("সাকিব")3team[1] = ["নতুন দল"]4hash(team)5team[1] += ["লিটন"]
tuple-এর ঘর → কোন object (সবুজ লাইন এইমাত্র চলেছে)
team[0]
team[1]
list L-এর ভেতরে
  1. দুই ঘরের tuple. ঘর 0 একটি string-এর দিকে, ঘর 1 একটি list-এর দিকে নির্দেশ করে — ধরুন সেই list-এর নাম L. tuple নিজে L-এর কপি রাখেনি, শুধু L-এর ঠিকানা রেখেছে।
  2. list-এর ভেতরটা বদলাল — কোনো আপত্তি নেই। team[1] দিয়ে L-কে পাওয়া গেল, তারপর L-এর নিজের method append চলল। tuple-এর কোনো ঘরে কিছু বসানো হয়নি, তাই tuple-এর প্রতিজ্ঞা ভাঙেনি। কিন্তু print(team) করলে এখন তিনজন খেলোয়াড় দেখাবে।
  3. ঘরে নতুন object বসানো — নিষেধ। team[1] = [...] মানে ঘর 1-কে অন্য একটি list-এর দিকে ঘোরানো। এটিই tuple কখনো হতে দেয় না: TypeError. ঘর 1 এখনো সেই পুরোনো L-এর দিকে।
  4. hash চাইলে — ব্যর্থ। একটি tuple-এর hash তার সব উপাদানের hash মিলিয়ে হিসাব হয়। list-এর কোনো hash নেই (তার ভেতরটা বদলায়), তাই এই tuple-এরও hash নেই: TypeError: unhashable type: 'list'. ফলে এটি dict-এর চাবি বা set-এর সদস্য হতে পারবে না।
  5. বিখ্যাত ধাঁধা: +=. team[1] += ["লিটন"] দুই কাজ করে: প্রথমে L-কে নিজের জায়গায় বাড়ায় (সফল!), তারপর ফলটি team[1]-এ আবার বসাতে চায় (ব্যর্থ!)। ফল: error আসে, অথচ list-এ “লিটন” ঢুকে গেছে। error মানেই “কিছু ঘটেনি” — এই ধারণা এখানে ভুল।

tuple-কে পুরোপুরি স্থির চাইলে এর ভেতরেও শুধু immutable জিনিস রাখুন — সংখ্যা, string, অন্য tuple. খেলোয়াড়দের নামও স্থির রাখতে চাইলে ("বাংলাদেশ", ("তামিম", "মুশফিক")). object-এর ভেতরে object, alias আর copy নিয়ে বিস্তারিত পর্ব ১৯-এ দেখবেন।

৪ · বদলানো যায় না — তাতে লাভ কী?

প্রথমে মনে হতে পারে, যে জিনিস কম কাজ পারে, তা রাখার কারণ কী? কিন্তু “বদলানো যায় না” একটি প্রতিশ্রুতি, আর প্রতিশ্রুতির দাম আছে। তিনটি বড় লাভ:

১. নিরাপদে ভাগ করা যায়

একটি list ফাংশনে পাঠালে ফাংশনটি চাইলে তাতে কিছু যোগ বা মুছে ফেলতে পারে — আর আপনার মূল list-ও বদলে যায়, কারণ দুজনেই একই object দেখছে। tuple পাঠালে এই দুশ্চিন্তা নেই। জেলা সদরের স্থানাঙ্কের একটি tuple দশটি ফাংশনে পাঠান, কেউ ভুল করেও সেটি নষ্ট করতে পারবে না। বড় প্রোগ্রামে, বিশেষ করে একাধিক thread-এ (পর্ব ৪১), এই নিশ্চয়তা অনেক বাগ আগেই আটকে দেয়।

২. dict-এর চাবি আর set-এর সদস্য হতে পারে

পর্ব ১৬-এ dictionary শিখবেন — চাবি দিয়ে মান খোঁজা। চাবি হতে গেলে একটি object-কে hashable হতে হয়: তার একটি hash সংখ্যা থাকবে, যা জীবনভর একই থাকবে। tuple-এর ভেতরে সব উপাদান hashable হলে tuple-ও hashable. তাই একটি ম্যাপে স্থানাঙ্ক দিয়ে জায়গার নাম রাখা যায়:

places = {
    (23.81, 90.41): "ঢাকা",
    (22.36, 91.78): "চট্টগ্রাম",
    (24.37, 88.60): "রাজশাহী",
}
print(places[(22.36, 91.78)])     # চট্টগ্রাম
# [23.81, 90.41]-কে চাবি বানালে: TypeError: unhashable type: 'list'

দাবার বোর্ডের ঘর (row, col), গ্রিডের বিন্দু, (জেলা, বছর) জোড়া ধরে বৃষ্টিপাত — এমন “একাধিক অংশের চাবি” Python-এ সবসময় tuple দিয়েই লেখা হয়। set-এর ক্ষেত্রেও একই নিয়ম (পর্ব ১৭)।

৩. একটু হালকা, একটু দ্রুত

list-কে সবসময় বাড়ার জন্য কিছু বাড়তি জায়গা রাখতে হয়; tuple-এর আকার জন্ম থেকেই নির্দিষ্ট, তাই সাধারণত একটু কম মেমরি নেয়। এছাড়া কোডে (1, 2, 3)-এর মতো শুধু ধ্রুবক দিয়ে লেখা tuple-কে CPython bytecode তৈরির সময়েই একটি ধ্রুবক হিসেবে বানিয়ে রাখে (পর্ব ০৫-এর constant folding) — প্রতিবার নতুন করে বানাতে হয় না। তবে এই পার্থক্য ছোট; শুধু গতির জন্য tuple বেছে নেবেন না। বেছে নিন অর্থের জন্য: এটি কি একটি স্থির রেকর্ড?

tuple Python-এর একেবারে শুরু থেকে আছে। মজার তথ্য: Python 2.6-এর (২০০৮) আগে tuple-এর কোনো method-ই ছিল না — count আর index পরে যোগ হয়েছে। এর পেছনে ছিল Guido van Rossum-এর একটি দৃষ্টিভঙ্গি: tuple মূলত C-র struct-এর মতো একটি রেকর্ড, যাতে “কতবার আছে” বা “কোথায় আছে” খোঁজার প্রশ্ন সাধারণত ওঠেই না। ভাবনার প্রশ্ন ১-এ এই বিতর্ক বিস্তারিত দেখবেন।

৫ · tuple-এর method আর অপারেশন

tuple বদলায় না বলে এর কাজের তালিকা ছোট — শুধু পড়া আর নতুন tuple বানানো। পর্ব ০৯-এর string আর পর্ব ১৪-এর list-এর সাথে এর মিল অনেক:

লেখাফলমন্তব্য
t = (4, 7, 7, 2); t[0], t[-1]4, 2index আর negative index, list-এর মতোই
t[1:3](7, 7)slicing নতুন tuple দেয়
t.count(7)2কতবার আছে
t.index(7)1প্রথম কোথায়; না থাকলে ValueError
(1, 2) + (3,)(1, 2, 3)জোড়া লাগানো — নতুন tuple
(0,) * 3(0, 0, 0)পুনরাবৃত্তি
7 in tTrueসদস্য কি না
len(t), min(t), sum(t)4, 2, 20built-in সবই চলে
(1, 2) == [1, 2]Falseভিন্ন type, তাই সমান নয়

তুলনা: অভিধানের নিয়মে

দুটি tuple তুলনা হয় lexicographic order-এ — অভিধানে শব্দ সাজানোর মতো। প্রথম ঘর মেলানো হয়; আলাদা হলে সেখানেই সিদ্ধান্ত। সমান হলে দ্বিতীয় ঘর, তারপর তৃতীয়… (1, 2) < (1, 3) সত্য, কারণ প্রথম ঘর সমান আর দ্বিতীয়তে 2 < 3. আবার (2,) < (1, 9, 9) মিথ্যা — প্রথম ঘরেই 2 > 1, বাকিগুলো দেখাই হয় না।

এর বড় ব্যবহার সাজানোয়। sorted([(3, 1), (1, 2), (3, 0), (1, 1)]) দেয় [(1, 1), (1, 2), (3, 0), (3, 1)] — প্রথম ঘর ধরে সাজানো, প্রথম ঘর সমান হলে দ্বিতীয় ঘর ধরে। অর্থাৎ “আগে X ধরে, X সমান হলে Y ধরে সাজাও” — এই কাজ tuple আপনাআপনি করে দেয়। মেধাতালিকায় আগে GPA, GPA সমান হলে রোল — দুটিকে একটি tuple-এ রাখলেই হলো। নিচের কোডে দেখবেন।

৬ · unpacking — বাক্স খুলে প্রতিটি জিনিসে নাম দেওয়া

packing-এর উল্টো কাজ হলো unpacking: একটি tuple (বা যেকোনো iterable)-এর মানগুলো খুলে আলাদা আলাদা নামে বসানো। পর্ব ০৬-এ a, b = b, a আর পর্ব ১৩-এ ফাংশন থেকে দুটি মান ফেরত দেখেছেন — দুটোই unpacking. এবার এর পুরো ক্ষমতা দেখা যাক।

মৌলিক unpacking আর _

name, roll, gpa = ("রহিম", 12, 3.75) — বাম দিকে যতটি নাম, ডান দিকে ঠিক ততটি মান লাগবে। ডান দিক tuple না হয়ে list, string, range — যেকোনো iterable হতে পারে: name, age, city = "Rahim,23,Dhaka".split(",") একটি CSV লাইনকে এক লাইনেই তিনটি নামে ভাগ করে।

কোনো মান দরকার না হলে প্রচলিত রীতি হলো তাকে _ নামে বসানো: name, _, gpa = student. Python-এর কাছে _ একটি সাধারণ নাম — বিশেষ কিছু নয় — কিন্তু পাঠকের কাছে এটি একটি স্পষ্ট বার্তা: “এই মানটি ইচ্ছে করে ফেলে দিলাম।” একে বলে throwaway variable.

লুপে unpacking

জোড়ার একটি list-এ লুপ চালানোর সময় loop variable-এর জায়গাতেই unpacking করা যায় — পর্ব ১২-এ zip আর enumerate-এর সাথে এটিই করেছিলেন:

results = [("রহিম", 78), ("সুমি", 91), ("করিম", 29)]
for name, marks in results:          # প্রতিটি জোড়া খুলে দুটি নাম
    print(name, "পাস" if marks >= 33 else "ফেল")

for pair in results: লিখে ভেতরে pair[0], pair[1] লেখাও যায় — কিন্তু তখন পাঠককে মনে রাখতে হয় কোন index-এ কী। নাম দিয়ে খুলে ফেললে কোড নিজেই নিজের ব্যাখ্যা।

nested unpacking আর কয়েকটির swap

রেকর্ডের ভেতরে রেকর্ড থাকলে বাম দিকেও একই আকার মেনে বন্ধনী দিন — একে বলে nested unpacking: city, (lat, lon) = ("সিলেট", (24.89, 91.87)). লুপেও চলে: for i, (name, marks) in enumerate(results, start=1):.

swap শুধু দুটির মধ্যে নয়। a, b, c = b, c, a তিনটি মান এক ধাপ ঘুরিয়ে দেয়। কারণ Python আগে ডান দিকের পুরো tuple বানায় (পুরোনো মানগুলো দিয়ে), তারপর বাম দিকের নামগুলোতে একে একে বসায় — মাঝখানে কোনো মান হারিয়ে যায় না।

starred unpacking — “বাকি সব”

কতগুলো মান আসবে আগে জানা না থাকলে একটি নামের আগে * দিন — সেই নাম “বাকি সবগুলো” একটি list হিসেবে নেবে। একে বলে starred assignment বা extended unpacking, যা Python 3.0-তে এসেছে। ধাপে ধাপে দেখুন Python কীভাবে মান ভাগ করে:

1nums = [10, 20, 30, 40, 50]2first, *middle, last = nums3print(first, middle, last)4a, *b, c = [10]       # যদি এমন হতো?
বাঁধা হলো কোন নাম → কোন মান
হিসাব
first
last
middle
  1. আগে গোনা। Python দেখে ডান দিকে ৫টি মান, বাম দিকে তারা (*) ছাড়া নাম ২টি। তাই তারা-চিহ্নিত নামের ভাগে পড়বে ৫ − ২ = ৩টি। একটি assignment-এ তারা থাকতে পারে মাত্র একটি নামে — দুটি দিলে SyntaxError, কারণ তখন কে কতটি পাবে ঠিক করা যায় না।
  2. বাম দিক থেকে বসানো। তারার আগের নামগুলো বাম থেকে একে একে মান পায়: first = 10.
  3. ডান দিক থেকে বসানো। তারার পরের নামগুলো ডান প্রান্ত থেকে মান পায়: last = 50. তারা যেকোনো অবস্থানে বসতে পারে — শুরুতে, মাঝে বা শেষে।
  4. মাঝের সব তারার ভাগে। middle = [20, 30, 40]. লক্ষ করুন — ডান দিক tuple হলেও তারা-চিহ্নিত নাম সবসময় একটি list পায়। মাঝে কিছু না থাকলে (যেমন [10, 50]) সে পায় খালি list [] — কোনো error নয়।
  5. যথেষ্ট মান না থাকলে। তারা “শূন্য বা তার বেশি” নিতে পারে, কিন্তু তারা-ছাড়া নামগুলোর প্রত্যেকের একটি করে মান লাগবেই। [10]-এ মাত্র ১টি, তাই ValueError — বার্তায় “at least 2” মানে কমপক্ষে ২টি দরকার ছিল।

দুটি প্রচলিত রূপ মনে রাখুন: head, *tail = items (প্রথমটি আর বাকিগুলো) আর *_, last = items (শুধু শেষটি দরকার, বাকি সব ফেলে দাও)। এবার এই পর্যন্ত যা শিখলেন, সব একসাথে চালিয়ে দেখুন:

students = [("রহিম", 12, 3.75), ("সুমি", 7, 3.90), ("করিম", 21, 3.75)]
for name, _, gpa in students:              # roll লাগবে না, তাই _
    print(name, gpa)

ranking = sorted((gpa, roll) for name, roll, gpa in students)
print(ranking)                              # tuple তুলনা: আগে gpa, সমান হলে roll

def price_range(prices):
    return min(prices), max(prices)         # একটি tuple ফেরত যায়

low, high = price_range([120, 95, 140, 88, 110])
print(f"সর্বনিম্ন {low}, সর্বোচ্চ {high} টাকা")

name, age, city = "Rahim,23,Dhaka".split(",")
print(name, int(age) + 1, city)

first, *middle, last = [10, 20, 30, 40, 50]
print(first, middle, last)
*_, newest = ["v1", "v2", "v3"]
print(newest, *middle, sep=" | ")

প্রথম লুপে প্রতিটি রেকর্ড তিন নামে খোলা হয়েছে, রোল গেছে _-এ; আউটপুট “রহিম 3.75”, “সুমি 3.9”, “করিম 3.75”. দ্বিতীয় অংশে প্রতিটি শিক্ষার্থী থেকে (gpa, roll) জোড়া বানিয়ে সাজানো হয়েছে (বন্ধনীর ভেতরের লেখাটি একটি generator expression — পর্ব ১৮-এ শিখবেন)। ফল [(3.75, 12), (3.75, 21), (3.9, 7)] — রহিম আর করিমের GPA সমান, তাই দ্বিতীয় ঘর রোল দিয়ে সিদ্ধান্ত হয়েছে। বড় থেকে ছোট চাইলে reverse=True. price_range একটি tuple ফেরত দেয়, যা সঙ্গে সঙ্গে low = 88 আর high = 140-এ খোলা হয়েছে। CSV লাইন ভেঙে “Rahim 24 Dhaka” — split string দেয়, তাই বয়সে যোগ করার আগে int(). শেষে starred unpacking: 10 [20, 30, 40] 50, আর *_ দিয়ে শুধু শেষ version “v3”. শেষ লাইনের *middle একটি list-কে ছড়িয়ে আলাদা argument বানিয়েছে, তাই আউটপুট v3 | 20 | 30 | 40 — এই কৌশল অংশ ৮-এ।

৭ · unpacking-এর ভুল — error বার্তা পড়তে শিখুন

unpacking-এর একটি কঠোর নিয়ম: (তারা না থাকলে) বাম দিকের নামের সংখ্যা আর ডান দিকের মানের সংখ্যা ঠিক সমান হতে হবে। না মিললে Python অনুমান করে না, সরাসরি ValueError দেয়। ধরুন একটি GPS যন্ত্র অক্ষাংশ-দ্রাঘিমাংশের সাথে উচ্চতাও পাঠাচ্ছে, অথচ কোড দুটি নামই আশা করে:

point = (23.81, 90.41, 8)
lat, lon = point
Traceback (most recent call last):
  File "gps.py", line 2, in <module>
    lat, lon = point
    ^^^^^^^^
ValueError: too many values to unpack (expected 2, got 3)

বার্তাটি পড়ুন: “unpack করার জন্য মান বেশি — ২টি আশা করেছিলাম, পেয়েছি ৩টি।” উল্টোটা হলে, যেমন a, b, c = [1, 2], বার্তা হয় not enough values to unpack (expected 3, got 2). বার্তার “got 3” অংশটি নতুন version-এ যোগ হয়েছে; পুরোনো Python-এ শুধু (expected 2) দেখতে পারেন, আর string-এর মতো কিছু iterable-এ এখনো শুধু expected অংশটিই আসে।

লেখাযা ঘটেকারণ
lat, lon = (23.81, 90.41, 8)ValueError: too many values…নাম কম, মান বেশি
a, b, c = [1, 2]ValueError: not enough values…নাম বেশি, মান কম
x, y = 5TypeError: cannot unpack non-iterable int objectএকটি সংখ্যাকে খোলা যায় না
a, b = "ঢাকা"ValueErrorstring-এর প্রতিটি অক্ষর একটি মান; “ঢাকা”-তে ৪টি code point
*a, *b = 1, 2, 3SyntaxErrorএক assignment-এ তারা একটিই

ভুল দৈর্ঘ্যের ডেটা চুপচাপ মেনে নেবেন না। “Rahim,23,Dhaka” ধাঁচের ফাইলে কোনো লাইনে একটি বাড়তি কমা থাকলে (যেমন “Rahim,23,Dhaka,BD”) name, age, city = line.split(",") সঙ্গে সঙ্গে error দেয় — এটা আসলে ভালো খবর, কারণ ভুল ডেটা ধরা পড়ল। “error এড়াতে” name, age, city, *_ = ... লিখে দিলে ভুলটি লুকিয়ে যাবে। তারা ব্যবহার করুন তখনই, যখন বাড়তি মান আসা সত্যিই স্বাভাবিক। আসল CSV ফাইলের জন্য csv module (পর্ব ২৬) উদ্ধৃতিচিহ্নের ভেতরের কমাও ঠিকভাবে সামলায়।

৮ · ফাংশন থেকে কয়েকটি মান, আর * দিয়ে ছড়িয়ে দেওয়া

পর্ব ১৩-এ দেখেছেন, return min(prices), max(prices) লিখলে ফাংশন “দুটি মান” ফেরত দেয়। এখন আপনি জানেন আসলে কী ঘটে: কমা একটি tuple বানায়, ফাংশন একটিই object — সেই tuple — ফেরত দেয়, আর ডাকার জায়গায় low, high = ... তাকে খুলে ফেলে। Python-এ “একাধিক return value” মানে আসলে packing আর unpacking-এর জুটি।

একটি খুব প্রচলিত নকশা হলো “ফল আর বার্তা” একসাথে ফেরত দেওয়া। ধরুন একটি bKash নম্বর যাচাই করবেন:

def check_mobile(number):
    if len(number) != 11 or not number.isdigit():
        return False, "নম্বর ১১ অঙ্কের হতে হবে"
    if not number.startswith("01"):
        return False, "নম্বর 01 দিয়ে শুরু হবে"
    return True, "ঠিক আছে"

ok, message = check_mobile("01712345678")
if not ok:
    print("ভুল:", message)

এই (ok, message) নকশা ছোট ফাংশনে চমৎকার। তবে দুটি সতর্কতা: তিন-চারটির বেশি মান ফেরত দিলে ডাকার জায়গায় ক্রম মনে রাখা কঠিন হয় — তখন নিচের namedtuple ভালো; আর সত্যিকারের ভুলের পরিস্থিতিতে Python-এ সাধারণত exception ছোড়াই প্রচলিত (পর্ব ২৪)।

ডাকার সময় * — একটি sequence-কে ছড়িয়ে দেওয়া

unpacking-এর একটি আত্মীয় হলো argument unpacking. ফাংশন ডাকার সময় একটি list বা tuple-এর আগে * দিলে তার প্রতিটি উপাদান আলাদা argument হয়ে যায়:

  • items = ["চাল", "ডাল", "তেল"]; print(items) ছাপে ['চাল', 'ডাল', 'তেল'], কিন্তু print(*items) ছাপে চাল ডাল তেল — যেন আপনি print("চাল", "ডাল", "তেল") লিখেছেন।
  • dhaka = (23.81, 90.41) থাকলে distance_from(*dhaka) মানে distance_from(23.81, 90.41).
  • Python 3.5 থেকে list বা tuple বানানোর সময়ও ছড়ানো যায়: [*range(3), *"ab"] দেয় [0, 1, 2, 'a', 'b'], আর (*a, *b) দুটি tuple জোড়া লাগায়।

উল্টো দিকটি — ফাংশন নিজে যত খুশি argument গ্রহণ করবে (def total(*prices):) — পর্ব ২০-এ *args আর **kwargs-এ বিস্তারিত দেখবেন। মজার ব্যাপার, সেখানে বাড়তি argument-গুলো একটি tuple হিসেবেই আসে।

৯ · namedtuple — ঘরগুলোর নাম দেওয়া রেকর্ড

সাধারণ tuple-এর একটি দুর্বলতা আছে। কোডে student[2] দেখে কি বোঝা যায় এটি GPA? ছয় মাস পরে আপনি নিজেই ভুলে যাবেন। কেউ রেকর্ডে নতুন একটি ঘর (যেমন বয়স) মাঝখানে ঢোকালে সব [2] নিঃশব্দে ভুল জিনিস পড়বে। সমাধান: প্রতিটি ঘরের একটি নাম।

collections module-এর namedtuple একটি নতুন tuple-type বানিয়ে দেয়, যার ঘরগুলো নাম দিয়েও পড়া যায়। ধাপে ধাপে দেখুন নাম আর index কীভাবে একই ঘরে পৌঁছায়:

1City = namedtuple("City", ["name", "lat", "lon"])2ctg = City("চট্টগ্রাম", 22.36, 91.78)3ctg.lat4ctg[1]5ctg.lat = 0
কী তৈরি হলো, কোন ঘর পড়া হলো
City
ctg
পড়া হলো
  1. একটি “ছাঁচ” তৈরি। namedtuple নিজে কোনো রেকর্ড নয় — এটি একটি নতুন class বানায়, যার নাম City আর যার তিনটি ঘরের নাম name, lat, lon. ঘরের নাম আর অবস্থান এখানেই চিরকালের জন্য বাঁধা হলো।
  2. একটি রেকর্ড তৈরি। ctg একটি City, আর City আসলে tuple-এর একটি subclass — তাই ctg পুরোপুরি একটি tuple: index, unpacking, তুলনা, hash, সব চলে। ছাপলে দেখায় ঘরের নামসহ, যা ডিবাগিংয়ে বড় সুবিধা।
  3. নাম দিয়ে পড়া। ctg.lat — Python ঘরের নাম lat থেকে অবস্থান 1 খুঁজে সেই ঘরের মান দেয়: 22.36. কোড পড়লেই বোঝা যায় কী চাওয়া হয়েছে।
  4. index দিয়ে পড়া — একই ঘর। ctg[1]-ও 22.36. পুরোনো কোড যা tuple আশা করে, সেটি কোনো পরিবর্তন ছাড়াই namedtuple নিয়ে কাজ করবে। কিন্তু নতুন কোডে নামটাই লিখুন — [1] দেখে কেউ বোঝে না এটি অক্ষাংশ।
  5. এখনো immutable. নাম আছে বলে এটি বদলানো যায় এমন object হয়ে যায়নি: ctg.lat = 0 দেয় AttributeError. একটি ঘর বদলানো নতুন রেকর্ড চাইলে ctg._replace(lat=22.40) — মূল ctg যেমন ছিল তেমনই থাকে।

Python 3.6 থেকে একই জিনিস আরও পড়তে সুবিধাজনক class-রূপে লেখা যায় typing.NamedTuple দিয়ে — প্রতিটি ঘরের type hint আর চাইলে default মানসহ। নিচের কোডে দুটি রূপ আর tuple-এর ভেতরে list-এর আচরণ একসাথে দেখুন:

from collections import namedtuple
from typing import NamedTuple

City = namedtuple("City", ["name", "lat", "lon"])
dhaka = City("ঢাকা", 23.81, 90.41)
print(dhaka.lat == dhaka[1], dhaka)

class Student(NamedTuple):
    name: str
    roll: int
    gpa: float = 0.0

s = Student("সুমি", 7)
print(s, s._replace(gpa=3.9).gpa)

team = ("বাংলাদেশ", ["তামিম", "মুশফিক"])
team[1].append("সাকিব")                   # ভেতরের list বদলায়
print(team)
try:
    hash(team)
except TypeError as err:
    print("TypeError:", err)

প্রথম লাইনের আউটপুট True City(name='ঢাকা', lat=23.81, lon=90.41) — নাম আর index একই ঘর পড়ে, আর ছাপলে ঘরের নামসহ দেখায়। Student-এ gpa-র default 0.0, তাই দুটি মান দিয়েই রেকর্ড বানানো গেছে: Student(name='সুমি', roll=7, gpa=0.0). _replace নতুন একটি রেকর্ড দিয়েছে, যার gpa 3.9; মূল s অপরিবর্তিত। এরপর দেখুন অংশ ৩-এর স্টেপারের সত্যতা: tuple-এর ভেতরের list-এ “সাকিব” যোগ হলো কোনো error ছাড়াই, কিন্তু hash(team) ব্যর্থ — TypeError: unhashable type: 'list'. error-টি try/except দিয়ে ধরে ছাপা হয়েছে, যাতে প্রোগ্রাম না থামে; এই কৌশল পর্ব ২৪-এ শিখবেন। type hint (name: str) Python চালানোর সময় যাচাই করে না — এর পুরো আলোচনা পর্ব ৩৭-এ।

আরও নমনীয় রেকর্ড দরকার হলে — যেমন ঘরের মান বদলাতে চাইলে, বা method যোগ করতে চাইলে — Python 3.7 থেকে আছে dataclass. পর্ব ৩২-এ এটি দেখবেন। আপাতত সিদ্ধান্তের নিয়ম সহজ: ছোট, স্থির, tuple-এর মতো আচরণ চাইলে NamedTuple; বড়, বদলানো যায় এমন রেকর্ড চাইলে dataclass.

tuple বনাম list — এক নজরে

প্রশ্নtuplelist
লেখার রূপ(1, 2), 1, 2, (5,)[1, 2], [5]
তৈরির পর বদলানো যায়?না (ঘরগুলো স্থির)হ্যাঁ
সাধারণ ব্যবহারএকটি জিনিসের কয়েকটি দিক — রেকর্ডএকই রকম অনেক জিনিস — সংগ্রহ
ঘরের অর্থঅবস্থান দিয়ে ঠিক (০ = নাম, ১ = রোল)সব ঘরের ভূমিকা একই
methodশুধু count, indexappend, sort, remove… অনেক
dict-এর চাবি / set-এর সদস্যপারে (ভেতরের সবকিছু hashable হলে)পারে না
মেমরি ও গতিএকটু হালকাবাড়ার জন্য বাড়তি জায়গা রাখে
নাম দেওয়া রূপnamedtuple, NamedTuple(রেকর্ডের জন্য dataclass)
C/C++ জানলে

C-তে tuple নেই; কয়েকটি ভিন্ন ধরনের মান একসাথে রাখতে struct লাগে, আর ফাংশন থেকে একাধিক মান ফেরাতে struct বা pointer. Python-এর namedtuple অনেকটা একটি immutable struct. C++-এ std::pair আর std::tuple আছে, আর C++17-এর structured binding auto [lat, lon] = p; প্রায় Python-এর unpacking-এর মতো। পার্থক্য হলো C++-এর tuple-এর আকার আর type compile-এর সময়েই ঠিক, Python-এ চালানোর সময়। struct আর enum-এর বিস্তারিত দেখতে C++ কোর্সের enum ও struct পর্ব দেখুন।

পরীক্ষা ও ইন্টারভিউতে
  • tuple আর list-এর প্রধান পার্থক্য? — tuple immutable (ঘরে নতুন মান বসানো যায় না), list mutable; tuple সাধারণত রেকর্ড, list একই রকম জিনিসের সংগ্রহ।
  • type((5)) আর type((5,)) কী? — প্রথমটি int, দ্বিতীয়টি tuple; কমাই tuple বানায়।
  • tuple কি সবসময় dict-এর চাবি হতে পারে? — না; ভেতরের সব উপাদান hashable হলে তবেই। (1, [2]) পারে না।
  • a, *b = (1, 2, 3)-এর পর b কী? — [2, 3], একটি list (tuple নয়)।
  • t = ([1],)-এ t[0] += [2] চালালে কী হয়? — TypeError আসে, তবু t হয়ে যায় ([1, 2],).

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

tuple-এর কোড AI সাধারণত ঠিকঠাকই লেখে, কিন্তু কয়েকটি ভুল বারবার ফিরে আসে। বেশিরভাগই “চলে, কিন্তু পড়তে কঠিন” বা “চলে, যতক্ষণ না ডেটা একটু বদলায়” ধরনের:

AI প্রায়ই যা লেখেসমস্যাভালো রূপ
student = ["রহিম", 12, 3.75]স্থির রেকর্ডের জন্য list; কেউ ভুলে append করতে পারে, চাবি হতে পারে নাtuple বা NamedTuple
colors = ("red")কমা নেই — এটি tuple নয়, string; লুপ চালালে অক্ষর আসে("red",)
print(s[0], s[2]) সর্বত্রকোন index-এ কী, বোঝা যায় না; ঘরের ক্রম বদলালে নিঃশব্দে ভুলunpacking বা s.name, s.gpa
name, age = line.split(",")ডেটায় ৩টি ঘর থাকলে ValueError; কিংবা উল্টো *_ দিয়ে ভুল লুকানোঘরের সংখ্যা যাচাই, বা csv module
৫-৬টি মানের tuple ফেরত দেওয়াডাকার জায়গায় ক্রম গুলিয়ে যায়NamedTuple বা dataclass ফেরত

AI-কে রেকর্ড নিয়ে কাজের কোড লিখতে বলার সময় এই কাঠামোটি কপি করে ব্যবহার করুন:

আমি Python 3.12 বা নতুন version ব্যবহার করি। একটি প্রোগ্রাম লিখে দাও: একটি CSV-ধাঁচের লেখা থেকে (প্রতি লাইনে "নাম,রোল,GPA") শিক্ষার্থীদের রেকর্ড পড়ো, GPA অনুযায়ী বড় থেকে ছোট সাজাও (GPA সমান হলে রোল ছোট আগে), আর সর্বোচ্চ ও সর্বনিম্ন GPA একসাথে ফেরত দেয় এমন একটি ফাংশন লেখো। শর্ত: ১. প্রতিটি শিক্ষার্থীর রেকর্ডের জন্য typing.NamedTuple ব্যবহার করো, list নয়। ২. কোথাও record[0], record[2] ধরনের index দিয়ে পড়বে না — নাম বা unpacking ব্যবহার করো। ৩. কোনো লাইনে ঘরের সংখ্যা ৩ না হলে চুপচাপ বাদ দেবে না; লাইন নম্বরসহ পরিষ্কার বার্তা দেবে। ৪. এক উপাদানের tuple লাগলে শেষের কমা নিশ্চিত করবে। ৫. কোডের নিচে বলো: খালি ইনপুট দিলে কী হবে, আর কোন জায়গায় unpacking-এর ValueError আসতে পারে।

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

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

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

প্র ০১tuple কি আসলে “বদলানো যায় না এমন list”, নাকি একটি “রেকর্ড”? Python সম্প্রদায়ে এই দুই মতের পার্থক্য কী, আর Guido van Rossum কোন দিকে ছিলেন?

Python-এ দুটি ধারণা পাশাপাশি চলে। প্রথম মত বলে, tuple হলো একটি immutable list — সব একই রকম জিনিসের একটি সারি, শুধু বদলানো যায় না। এই মতে (3, 1, 4, 1, 5)-এর মতো সংখ্যার tuple একদম স্বাভাবিক, আর tuple বেছে নেওয়ার কারণ শুধু নিরাপত্তা বা hash. দ্বিতীয় মত বলে, tuple হলো একটি রেকর্ড — C-র struct বা ডেটাবেসের একটি সারির মতো, যেখানে প্রতিটি অবস্থানের নিজস্ব অর্থ আছে, আর ঘরগুলো প্রায়ই ভিন্ন ধরনের।

Guido van Rossum বহুবার দ্বিতীয় মতের পক্ষে বলেছেন। python-dev মেইলিং লিস্টে তাঁর একটি পরিচিত ব্যাখ্যা হলো: list সাধারণত একই রকম (homogeneous) জিনিসের জন্য, যাদের সংখ্যা বদলায়; tuple ভিন্ন ভিন্ন (heterogeneous) জিনিসের জন্য, যারা একসাথে একটি ইউনিট। এই দৃষ্টিভঙ্গির ছাপ ভাষাতেই আছে — দীর্ঘদিন tuple-এর কোনো method ছিল না, কারণ একটি রেকর্ডে “৭ কতবার আছে?” প্রশ্নটি অর্থহীন। count আর index যোগ হয় অনেক পরে, Python 2.6-এ, মূলত সুবিধার জন্য। os.stat()-এর ফলাফল, time.localtime(), ডেটাবেস driver-এর প্রতিটি সারি — standard library-র এসব জায়গায় tuple রেকর্ড হিসেবেই ব্যবহৃত।

আবার বাস্তবে প্রথম মতও টিকে আছে, আর তার পক্ষে যুক্তিও আছে। ফাংশনের *args একটি tuple, যাতে যতগুলো খুশি একই রকম মান আসতে পারে। dict-এর চাবি হিসেবে সংখ্যার একটি ক্রম রাখতে চাইলে tuple-ই একমাত্র পথ। type hint-এ দুই মতের পার্থক্য স্পষ্ট করে লেখার ব্যবস্থাও আছে: tuple[str, int, float] মানে নির্দিষ্ট তিন ঘরের রেকর্ড, আর tuple[int, ...] মানে যেকোনো দৈর্ঘ্যের পূর্ণসংখ্যার সারি।

কোন মত “ঠিক”? ভাষা দুটিকেই অনুমতি দেয়। তবে কোড পড়ার সময় রেকর্ড-ভাবনা বেশি সাহায্য করে: যদি দেখেন একটি tuple-এর দৈর্ঘ্য প্রোগ্রাম চলার সময় বদলায় বা তার উপর লুপ চালিয়ে সব ঘরে একই কাজ হয়, তবে হয়তো সেটি আসলে list হওয়া উচিত ছিল।

মূল উপলব্ধি: type বেছে নেওয়া শুধু কারিগরি সিদ্ধান্ত নয়, পাঠকের কাছে একটি বার্তা। tuple বলে “এটি একটি জিনিসের কয়েকটি দিক”, list বলে “এগুলো একই রকম অনেকগুলো” — সেই বার্তা মেলালে কোড নিজেই নিজের ব্যাখ্যা দেয়।

প্র ০২tuple immutable, তবু (1, [2])-এর কোনো hash নেই। কেন tuple-এর hashability তার উপাদানের উপর নির্ভর করে — Python কি চাইলে যেকোনো tuple-কে hash করতে পারত না?

dict আর set একটি hash table-এর উপর তৈরি। চাবি রাখার সময় Python চাবির hash সংখ্যা হিসাব করে, আর সেই সংখ্যা দেখে ঠিক করে চাবিটি টেবিলের কোন “খোপে” যাবে। পরে খোঁজার সময় আবার hash হিসাব করে একই খোপে যায়। এই ব্যবস্থা তখনই কাজ করে, যখন দুটি শর্ত মানা হয়: চাবির hash জীবনভর বদলাবে না, আর দুটি চাবি সমান (==) হলে তাদের hash-ও সমান হবে।

tuple-এর সমতা নির্ভর করে তার উপাদানের সমতার উপর: (1, [2]) == (1, [2]) সত্য, কারণ ভেতরের list দুটি সমান। তাহলে hash-ও উপাদান থেকেই হিসাব করতে হবে — নইলে দুটি সমান tuple ভিন্ন খোপে চলে যেত। এখন ভাবুন, ভেতরের list-টি পরে বদলে [2, 3] হলো। tuple-এর সমতা বদলে গেল, তাই সঠিক hash-ও বদলে যাওয়ার কথা — অথচ চাবিটি পুরোনো hash-এর খোপে বসে আছে। খোঁজার সময় Python নতুন hash নিয়ে ভিন্ন খোপে যাবে, আর চাবিটি “হারিয়ে” যাবে, যদিও সেটি টেবিলেই আছে। এই ধরনের নিঃশব্দ ভুল এড়াতে Python list-কে unhashable করেছে, আর tuple hash করার সময় প্রতিটি উপাদানের hash চায় — একটিও না পেলে TypeError.

বিকল্প কী ছিল? tuple-এর hash তার ঠিকানা (id) দিয়ে করা যেত, যেমন সাধারণ object-এর ক্ষেত্রে হয়। কিন্তু তাহলে (23.81, 90.41) দিয়ে রাখা একটি চাবি নতুন করে লেখা (23.81, 90.41) দিয়ে আর খুঁজে পাওয়া যেত না — tuple-কে চাবি বানানোর মূল উদ্দেশ্যই ব্যর্থ হতো। আরেকটি লক্ষণীয় বিষয়: error আসে tuple বানানোর সময় নয়, hash চাওয়ার সময় — Python অলসভাবে যাচাই করে, তাই (1, [2]) বানানো পুরোপুরি বৈধ।

এই নিয়ম অন্যান্য ভাষাতেও দেখা যায়। Java-তে একটি mutable object-কে HashMap-এর চাবি বানিয়ে পরে বদলালে ঠিক এই “হারিয়ে যাওয়া” ঘটে, কারণ Java আপনাকে থামায় না। Python থামিয়ে দিয়ে নিরাপদ পথ বেছে নিয়েছে। পর্ব ১৬-এ dict আর পর্ব ১৭-এ set শেখার সময় hash table-এর ভেতরটা আরও বিস্তারিত দেখবেন।

মূল উপলব্ধি: hashability একটি চুক্তি — “আমার সমতা কখনো বদলাবে না”. tuple নিজের ঘরগুলোর জন্য এই চুক্তি দিতে পারে, কিন্তু ঘরের ভেতরের object-এর হয়ে দিতে পারে না। একটি পাত্রের প্রতিশ্রুতি তার সবচেয়ে দুর্বল উপাদানের চেয়ে শক্ত নয়।

প্র ০৩first, *rest = items — এই starred unpacking Python-এ শুরু থেকে ছিল না। কোন PEP এটি এনেছিল, এর আগে মানুষ কী লিখত, আর কেন একটি assignment-এ মাত্র একটি তারা অনুমোদিত?

starred unpacking এসেছে PEP 3132 — “Extended Iterable Unpacking” থেকে, যা প্রস্তাব করেছিলেন Georg Brandl, আর যা চালু হয় ২০০৮ সালে Python 3.0-তে। Python 2-তে unpacking ছিল কঠোর: বাম দিকে ঠিক যতটি নাম, ডান দিকে ঠিক ততটি মান। “প্রথমটি আর বাকিগুলো” চাইলে লিখতে হতো first, rest = items[0], items[1:]. এতে দুটি সমস্যা: প্রথমত, এটি শুধু index আর slice করা যায় এমন sequence-এ চলে — একটি ফাইল বা generator-এর মতো iterable-এ নয়; দ্বিতীয়ত, লেখায় উদ্দেশ্য স্পষ্ট নয়, আর slice-এর সংখ্যায় off-by-one-এর সুযোগ থাকে।

PEP 3132-এর ধারণাটি নতুন ছিল না। Lisp আর ML-পরিবারের ভাষায় “head আর tail”-এ ভাগ করা বহু পুরোনো কৌশল, আর ফাংশনের সংজ্ঞায় Python নিজেই def f(*args) দিয়ে “বাকি সব”-এর ধারণা আগে থেকে ব্যবহার করত। PEP-টি একই চিহ্ন assignment-এর বাম দিকে নিয়ে এল, ফলে ভাষাটি আরও সামঞ্জস্যপূর্ণ হলো।

একটি তারা কেন? কারণ মান ভাগের হিসাব তখনই নিশ্চিত, যখন অজানা অংশ একটিই। a, *b, c-এ বাকি নামগুলো স্থির: বাম থেকে একটি, ডান থেকে একটি, তারা পায় মাঝের সব। কিন্তু *a, *b = 1, 2, 3-এ কোনো একক উত্তর নেই — a পাবে ০টি, ১টি, নাকি ২টি? Python অস্পষ্টতা অনুমান করে না, তাই এটিকে SyntaxError করেছে, যা কোড চালানোর আগেই ধরা পড়ে। একই কারণে তারা-চিহ্নিত নাম সবসময় একটি list পায়, এমনকি ডান দিক tuple বা string হলেও — সংখ্যাটি অজানা, তাই এমন একটি পাত্র দেওয়া হয় যা যেকোনো দৈর্ঘ্য ধরে আর পরে বাড়ানোও যায়।

পরে Python 3.5-এ PEP 448 তারার ব্যবহার আরও বাড়ায়: ফাংশন ডাকায় একাধিক তারা (print(*a, *b)), আর list, tuple, set বানানোর সময় ছড়ানো ([*a, *b]). লক্ষ করুন, ছড়ানোর সময় একাধিক তারা ঠিক আছে, কারণ সেখানে কোনো অস্পষ্টতা নেই — সব মান শুধু পাশাপাশি বসে যায়।

মূল উপলব্ধি: ভালো ভাষা-নকশা একটি চিহ্নকে এক অর্থে সব জায়গায় ব্যবহার করে — * মানে “বাকি সব একসাথে”. আর যেখানে অর্থ অস্পষ্ট হতে পারে, সেখানে অনুমান না করে সরাসরি না বলে দেয়।

প্র ০৪namedtuple কে এবং কখন Python-এ এনেছিলেন, এটি ভেতরে কীভাবে কাজ করত, আর পরে dataclass (PEP 557) কেন এল? দুটির মধ্যে কখন কোনটি বেছে নেবেন?

collections.namedtuple যোগ হয় Python 2.6-এ (২০০৮), Raymond Hettinger-এর হাত ধরে — যিনি Python-এর collections module, itertools আর বহু দ্রুত built-in-এর পেছনের অন্যতম প্রধান মানুষ। লক্ষ্য ছিল সহজ: standard library আর সাধারণ কোডে অসংখ্য জায়গায় tuple রেকর্ড হিসেবে ফেরত আসত, আর result[3] পড়ে কেউ বুঝত না কী। namedtuple সেই tuple-কেই নাম দিল, অথচ পুরোনো কোড যা index ব্যবহার করে, তা ভাঙল না — কারণ namedtuple tuple-এর subclass.

এর প্রথম দিকের বাস্তবায়ন ছিল বেশ অভিনব: namedtuple ঘরের নামগুলো দিয়ে একটি class-এর সোর্স কোড string হিসেবে তৈরি করে exec দিয়ে চালাত। বহু বছর এভাবেই চলেছে; পরে Python 3.7-এর আশেপাশে বাস্তবায়ন বদলে দ্রুত করা হয়, যাতে নতুন namedtuple-type তৈরিতে কম সময় লাগে। ঘরের মান রাখা হয় tuple-এর ঘরেই, তাই প্রতিটি রেকর্ড একটি সাধারণ tuple-এর মতোই হালকা — আলাদা কোনো dictionary লাগে না।

Python 3.6-এ ভ্যারিয়েবলের type annotation (PEP 526) আসার পর typing.NamedTuple-এর class-রূপ সম্ভব হলো, যা পড়তে অনেক পরিষ্কার। তবু namedtuple-এর কিছু সীমা থেকে গেল: এটি immutable (সবসময় চাই না), এটি tuple হওয়ায় Point(1, 2) == (1, 2) সত্য (যা কখনো অপ্রত্যাশিত বাগ আনে), আর unpacking বা index দিয়ে ঘরের ক্রমের উপর নির্ভরতা থেকে যায় — মাঝখানে একটি ঘর যোগ করলে পুরোনো কোড ভাঙে।

এই অভাব পূরণে Eric V. Smith প্রস্তাব করেন PEP 557 — Data Classes, যা Python 3.7-এ (২০১৮) চালু হয়। @dataclass একটি সাধারণ class-এর জন্য __init__, __repr__, __eq__ নিজে লিখে দেয়; মান বদলানো যায় (চাইলে frozen=True দিয়ে স্থির করা যায়), আর এটি tuple নয়, তাই ভুল তুলনা বা index-নির্ভরতা নেই। এর পেছনে তৃতীয় পক্ষের attrs লাইব্রেরির অভিজ্ঞতারও বড় প্রভাব ছিল।

কখন কোনটি? ছোট, স্থির রেকর্ড, যা tuple হিসেবেও ব্যবহার হবে (unpacking, dict-এর চাবি, পুরোনো tuple-নির্ভর API) — NamedTuple. বড় বা বদলানো যায় এমন রেকর্ড, method, যাচাই বা উত্তরাধিকার দরকার — dataclass (পর্ব ৩২)।

মূল উপলব্ধি: ভাষা ধাপে ধাপে বাড়ে — প্রথমে সবচেয়ে কম বদল দিয়ে সমস্যার সমাধান (পুরোনো tuple-কে নাম দেওয়া), তারপর অভিজ্ঞতা থেকে আরও পূর্ণ সমাধান। পুরোনোটি বাদ যায় না, কারণ তার নিজস্ব জায়গা আছে।

প্র ০৫পর্ব ১১-এর match বিবৃতি tuple-এর সাথে কীভাবে কাজ করে? unpacking আর pattern matching কি একই ধারণার দুটি রূপ?

Python 3.10-এ আসা structural pattern matching (PEP 634) আসলে unpacking-এর একটি বড় ভাই। unpacking বলে: “ডান দিকের জিনিসটি এই আকারের — খুলে নামগুলোতে বসাও; না মিললে error.” match বলে: “জিনিসটি কি এই আকারের? মিললে খুলে নামগুলোতে বসাও; না মিললে পরের আকারটি দেখো।” অর্থাৎ match হলো শর্তযুক্ত unpacking.

একটি উদাহরণ ভাবুন — একটি মানচিত্রে বিন্দুর অবস্থান: case (0, 0): মেলে শুধু মূলবিন্দুতে; case (x, 0): মেলে যেকোনো দুই ঘরের sequence-এ যার দ্বিতীয় ঘর 0, আর প্রথম ঘর x নামে বসে; case (x, y): মেলে যেকোনো দুই ঘরে; case (x, y, *rest): তিন বা বেশি ঘরে — এখানে starred unpacking-এর তারাও চলে। এগুলোকে বলে sequence pattern. namedtuple বা dataclass-এর ক্ষেত্রে আরও পড়তে সহজ রূপ আছে — class pattern: case City(name=n, lat=lat):, যা index-এর বদলে ঘরের নাম দিয়ে মেলায়।

কয়েকটি সূক্ষ্ম পার্থক্য আছে। প্রথমত, sequence pattern list আর tuple দুটোকেই মেলায়, কিন্তু ইচ্ছে করে string, bytes মেলায় না — নইলে case (a, b): দুই অক্ষরের string-এও মিলে যেত, যা প্রায় কখনো উদ্দেশ্য নয়। সাধারণ unpacking-এ a, b = "ক১" কিন্তু চলে। দ্বিতীয়ত, pattern-এ একটি সরল নাম সবসময় “ধরে রাখার” নাম (capture), তুলনার মান নয় — case (ZERO, y): লিখলে ZERO নামের ধ্রুবকের সাথে তুলনা হয় না, বরং ZERO নামে নতুন মান বসে। তুলনা চাইলে বিন্দুযুক্ত নাম (যেমন Color.RED) বা literal লাগে।

ধারণাটি নতুন নয়। ML, Haskell, Erlang, Rust, Scala-র মতো ভাষায় pattern matching দশকের পর দশক ধরে কেন্দ্রীয় বৈশিষ্ট্য, আর সেখানে tuple আর রেকর্ড ভাঙার প্রধান উপায় এটিই। Python অনেক বিতর্কের পর (আগের PEP 622 প্রস্তাব থেকে PEP 634, 635, 636-এ ভাগ হয়ে) এটি গ্রহণ করে, আর নকশায় সচেতনভাবে আগের unpacking-এর নিয়মের সাথে মিল রাখে, যাতে Python প্রোগ্রামারদের কাছে এটি পরিচিত লাগে।

মূল উপলব্ধি: unpacking, starred unpacking আর match — তিনটিই একই প্রশ্নের উত্তর: “এই ডেটার আকার কেমন, আর কোন অংশকে কী নাম দেব?” ডেটাকে আকার দিয়ে ভাবতে শিখলে tuple আর রেকর্ড নিয়ে কাজ অনেক পরিষ্কার হয়।

অনুশীলন

এগুলোর বেশিরভাগে নতুন কোড লিখতে হবে না — অনুমান করা, হাতে চালানো আর মিলিয়ে নেওয়াই আসল কাজ।

  1. tuple নাকি নয়? চালানোর আগে লিখুন, নিচের প্রতিটির type কী আর দৈর্ঘ্য কত (যদি থাকে): (ক) a = ("ঢাকা"); (খ) b = ("ঢাকা",); (গ) c = 1, 2, 3; (ঘ) d = tuple("ঢাকা"); (ঙ) e = ((1, 2),). তারপর REPL-এ type() আর len() দিয়ে মিলিয়ে নিন।

    (ক) str, দৈর্ঘ্য ৪ — বন্ধনী কিছু বদলায় না, কমা নেই। (খ) tuple, দৈর্ঘ্য ১। (গ) tuple, দৈর্ঘ্য ৩ — বন্ধনী ছাড়াও কমাই যথেষ্ট। (ঘ) tuple, দৈর্ঘ্য ৪ — “ঢাকা”-র প্রতিটি code point (ঢ, া, ক, া) একটি করে ঘর। (ঙ) tuple, দৈর্ঘ্য ১ — ভেতরে একটিই জিনিস, আর সেটি নিজেই একটি দুই ঘরের tuple.

  2. unpacking অনুমান করুন: প্রতিটির পরে নামগুলোর মান কী, নাকি error? (ক) a, *b = "Py"; (খ) *a, b, c = [1, 2]; (গ) x, (y, z) = 1, [2, 3]; (ঘ) p, q = {"x": 1, "y": 2}; (ঙ) m, n = 1, 2, 3.

    (ক) a = 'P', b = ['y'] — তারা সবসময় list দেয়। (খ) a = [], b = 1, c = 2 — তারা শূন্যটিও নিতে পারে। (গ) x = 1, y = 2, z = 3 — nested unpacking-এ ভেতরের অংশ list হলেও চলে। (ঘ) p = 'x', q = 'y' — dict-এর উপর হাঁটলে চাবিগুলো আসে (ক্রম তৈরির ক্রম মেনে, পর্ব ১৬)। (ঙ) ValueError: too many values to unpack — দুটি নাম, তিনটি মান।

  3. ধাঁধা: t = ([1, 2], 3). এরপর t[0] += [9] চালালে কী হয়? t এখন কী? আর t[0].extend([9]) লিখলে পার্থক্য কী হতো? কেন দ্বিতীয়টি নিরাপদ?

    TypeError: 'tuple' object does not support item assignment আসে, অথচ t হয়ে যায় ([1, 2, 9], 3). কারণ += দুই ধাপে চলে: আগে list নিজের জায়গায় বাড়ে (সফল), তারপর ফল t[0]-এ বসানোর চেষ্টা (ব্যর্থ)। t[0].extend([9]) শুধু প্রথম ধাপটি করে — list-এর method ডাকে, tuple-এর কোনো ঘরে কিছু বসাতে চায় না — তাই কোনো error নেই, আর ফলও একই। শিক্ষা: tuple-এর ভেতরের mutable জিনিস বদলাতে চাইলে তার method ব্যবহার করুন, আর সবচেয়ে ভালো হলো tuple-এর ভেতরে mutable কিছু না রাখা।

  4. রেকর্ড বানান (একমাত্র কোড লেখার কাজ): RGB রং রাখার জন্য একটি typing.NamedTuple লিখুন — Color, তিনটি ঘর red, green, blue (পূর্ণসংখ্যা)। বাংলাদেশের পতাকার সবুজের জন্য Color(0, 106, 78) বানান, তারপর (ক) নাম দিয়ে green ছাপুন, (খ) unpacking দিয়ে তিনটি মান তিন নামে নিন, (গ) _replace দিয়ে লাল অংশ 10 করা নতুন একটি রং বানান, আর দেখান মূলটি বদলায়নি।
    from typing import NamedTuple
    
    class Color(NamedTuple):
        red: int
        green: int
        blue: int
    
    flag_green = Color(0, 106, 78)
    print(flag_green.green)              # 106
    r, g, b = flag_green                 # এটি একটি tuple, তাই unpacking চলে
    lighter = flag_green._replace(red=10)
    print(lighter, flag_green)           # Color(red=10, ...) Color(red=0, ...)

    রং (0, 106, 78) প্রায়ই উল্লেখ করা পতাকার সবুজের একটি RGB মান; এখানে আসল বিষয় হলো রেকর্ডের নকশা। লক্ষ করুন, Color একটি tuple হওয়ায় এটিকে dict-এর চাবিও বানানো যায় — যেমন রঙের নাম খোঁজার একটি টেবিলে।

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

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

ক্রমPython-এ a, *b, c = items চালানোর সময় যে ধাপগুলো ঘটে, তা সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📦 ডান দিকের items থেকে সব মান বের করে গোনা
  2. 🔢 তারা-ছাড়া নাম কয়টি, আর মান যথেষ্ট কি না যাচাই (না হলে ValueError)
  3. 👈 তারার আগের নামগুলোতে বাম থেকে মান বসানো
  4. 👉 তারার পরের নামগুলোতে ডান থেকে মান বসানো
  5. 🧺 মাঝের বাকি মানগুলো একটি list বানিয়ে তারা-চিহ্নিত নামে বসানো

Python-এ নিচের কোনটি এক উপাদানের একটি tuple তৈরি করে?

  • (7)
  • [7]
  • (7,)
  • tuple(7)

t = ("ক", [1, 2]). এরপর t[1].append(3) চালালে কী হয়?

  • TypeError, কারণ tuple বদলানো যায় না
  • কোনো error নেই; t হয়ে যায় ("ক", [1, 2, 3])
  • কোনো error নেই, কিন্তু t অপরিবর্তিত থাকে
  • একটি নতুন tuple তৈরি হয়, পুরোনোটি থাকে

Python-এ first, *rest = (10, 20, 30) চালানোর পর rest-এর মান কী?

  • (20, 30)
  • 30
  • ValueError
  • [20, 30]

নিচের কোনটি Python dictionary-র চাবি হিসেবে ব্যবহার করা যায় না?

  • (1, [2, 3])
  • (23.81, 90.41)
  • "ঢাকা"
  • ((1, 2), (3, 4))

Python-এ sorted([(2, "খ"), (1, "গ"), (2, "ক")])-এর ফল কী?

  • [(2, "খ"), (1, "গ"), (2, "ক")]
  • [(1, "গ"), (2, "ক"), (2, "খ")]
  • [(1, "গ"), (2, "খ"), (2, "ক")]
  • [(2, "ক"), (2, "খ"), (1, "গ")]

lat, lon = (23.81, 90.41, 8) চালালে Python কী করে?

  • lat = 23.81, lon = 90.41; 8 বাদ পড়ে
  • lat = 23.81, lon = (90.41, 8)
  • ValueError দেয়
  • TypeError দেয়

একজন শিক্ষার্থীর (নাম, রোল, GPA) রেকর্ড এমনভাবে রাখতে চান যাতে তৈরির পর বদলানো না যায় এবং কোডে s.gpa লিখে পড়া যায়। কোনটি সবচেয়ে উপযুক্ত?

  • একটি list: ["রহিম", 12, 3.75]
  • একটি সাধারণ tuple: ("রহিম", 12, 3.75)
  • তিনটি আলাদা ভ্যারিয়েবল name, roll, gpa
  • typing.NamedTuple দিয়ে বানানো একটি Student class

শব্দকোষ

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

মনে রাখুন

  1. tuple একটি স্থির রেকর্ড — একটি জিনিসের কয়েকটি দিক (স্থানাঙ্ক, নাম-রোল-GPA); list হলো একই রকম অনেক জিনিসের বদলানো যায় এমন সংগ্রহ।
  2. কমাই tuple বানায়: 1, 2 tuple, (5) শুধু 5, এক উপাদানে (5,), খালি ().
  3. immutability অগভীর: ঘরগুলো স্থির, কিন্তু ঘরের list বদলাতে পারে — তখন tuple আর hashable নয়। সব উপাদান hashable হলে tuple dict-এর চাবি হতে পারে।
  4. unpacking-এ সংখ্যা মিলতে হবে; “বাকি সব” নিতে একটি তারা (first, *rest), যা সবসময় list পায়; অদরকারি মান _; লুপে for name, marks in results.
  5. t[2]-এর বদলে নাম: namedtuple বা typing.NamedTuple; বদলানো যায় এমন বড় রেকর্ডের জন্য পরে dataclass.

পরের পর্বে: এই পর্বে দেখলেন, tuple dict-এর চাবি হতে পারে কারণ সেটি hashable. এবার সেই dictionary-তেই ঢুকবেন — চাবি দিয়ে মুহূর্তে খোঁজা, চাবি-মানের জোড়ায় লুপ (সেখানেও unpacking!), আর hash table কেন এত দ্রুত।

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

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