অধ্যায় 16

সেট — পুনরাবৃত্তিহীন সংগ্রহ

পুনরাবৃত্তি সরানো, দুটি সংগ্রহ মেলানোর চারটি চিহ্ন, কেন {} খালি সেট নয়, আর সেটে ক্রম না থাকার মানে কী।

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

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

একদিনের বিক্রির তালিকা এসেছে — প্রতিটি বিক্রি একটি করে এন্ট্রি:

python
sold = ["pen", "bag", "pen", "ink", "pen"]

প্রশ্ন: কয় রকমের পণ্য বিক্রি হয়েছে? len(sold) বলবে পাঁচ, কিন্তু সেটা বিক্রির সংখ্যা। আলাদা পণ্য তিনটি।

গত অধ্যায়গুলোর হাতিয়ার দিয়ে করা যায় — একটি খালি লিস্ট নিয়ে, প্রতিটি পণ্যে if item not in unique: দেখে যোগ করা। ছয় লাইন, আর কাজটা হয়।

কিন্তু প্রশ্নটা যদি হয় «সোমবার আর মঙ্গলবার দুদিনই কোন পণ্যগুলো বিক্রি হয়েছে?» — তখন দুটো নেস্টেড লুপ লাগে। «শুধু সোমবার?» — আরেকটা। এই প্রশ্নগুলো এত সাধারণ যে এদের নিজস্ব গণিত আছে, আর পাইথনে সেই গণিতটা সরাসরি লেখা যায়।

সেট হলো সেই পাত্র: পুনরাবৃত্তি রাখে না, আর «দুটোতেই আছে», «একটাতে আছে অন্যটায় নেই» ধরনের প্রশ্নের উত্তর এক অক্ষরে দেয়।

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

  • সেট বানাতে, আর একটি লিস্ট থেকে পুনরাবৃত্তি সরাতে
  • |, &, -, ^ দিয়ে দুটি সেট মেলাতে
  • বলতে কেন {} একটি খালি সেট নয়
  • জানতে সেট ক্রম রাখে না, আর সেটা কখন সমস্যা
  • ক্রম রেখেই পুনরাবৃত্তি সরানোর প্যাটার্নটি লিখতে

আগে যা জানা লাগবে: ডিকশনারি — কী আর মানের জোড়া।


সেট — পুনরাবৃত্তিহীন সংগ্রহ

python
sold = ["pen", "bag", "pen", "ink", "pen"]

unique = set(sold)
print(sorted(unique))
print(len(unique))
text
['bag', 'ink', 'pen']
3

set(...) একটি লিস্ট নিয়ে পুনরাবৃত্তি ফেলে দেয়। তিনবারের pen একবারই থাকে।

খেয়াল করুন ছাপার সময় sorted() লেখা হয়েছে, আর সেটা ইচ্ছে করে — একটু পরেই কারণটা আসছে।

সরাসরিও বানানো যায়, কার্লি বন্ধনী দিয়ে:

python
letters = {"a", "b", "a"}
print(sorted(letters))
print(len(letters))
text
['a', 'b']
2

দ্বিতীয় "a" নীরবে হারিয়ে গেছে। কোনো ত্রুটি নেই — সেটে একই জিনিস দুবার থাকার ধারণাটাই নেই।

{} একটি খালি সেট নয়

python
not_a_set = {}
real_set = set()

print(type(not_a_set))
print(type(real_set))
text
<class 'dict'>
<class 'set'>

কার্লি বন্ধনী আগে ডিকশনারির জন্য ব্যবহৃত হতো, তাই খালি {} ডিকশনারিই থেকে গেছে। খালি সেট চাইলে set() লিখতে হয়।

এই ভুলটা নীরব: {} দিয়ে শুরু করে .add() লিখলে AttributeError আসে, আর তখন মনে হয় সমস্যা add-এ।

ক্রম নেই

এটি সেটের সবচেয়ে গুরুত্বপূর্ণ সীমা, আর সবচেয়ে বেশি ভুল বোঝা বৈশিষ্ট্য।

একটি সেটের জিনিসগুলোর কোনো ক্রম নেই। সূচক নেই, স্লাইস নেই:

python
tags = {"new", "sale"}
print(tags[0])
text
TypeError: 'set' object is not subscriptable

আর ছাপালে যে ক্রমে দেখা যায়, সেটি নির্ভরযোগ্য নয় — একই প্রোগ্রাম দুবার চালালে ক্রম আলাদা হতে পারে। তাই এই অধ্যায়ের প্রতিটি উদাহরণে ছাপার আগে sorted() লেখা হয়েছে: একটি সেট সরাসরি ছাপলে আপনি যা দেখছেন তা পরের বার একই থাকার নিশ্চয়তা নেই।

লিস্ট থেকে পুনরাবৃত্তি সরানোর সবচেয়ে ছোট উপায় list(set(sold)), কিন্তু সেটি মূল ক্রমটা নষ্ট করে দেয়। ক্রম দরকার হলে সেট আর লিস্ট একসাথে কাজে লাগান:

python
sold = ["pen", "bag", "pen", "ink", "bag"]
seen = set()
unique = []

for item in sold:
    if item not in seen:
        seen.add(item)
        unique.append(item)

print(unique)
text
['pen', 'bag', 'ink']

লিস্টটি ক্রম রাখছে, আর সেটটি «আগে দেখেছি কি না» মনে রাখছে। এই জোড়াটা খুব সাধারণ একটি প্যাটার্ন।

যোগ আর বাদ

python
tags = {"new"}

tags.add("sale")
tags.add("new")
print(sorted(tags))

tags.discard("gone")
tags.remove("sale")
print(sorted(tags))
text
['new', 'sale']
['new']

add একটি জিনিস যোগ করে — আর সেটি আগে থেকে থাকলে কিছুই হয় না, কোনো অভিযোগও নেই। লিস্টের append দ্বিতীয় কপি বানাত; সেট বানায় না।

বাদ দেওয়ার দুটি রূপ, আর পার্থক্যটা কাজের:

python
tags = {"new"}
tags.remove("gone")
text
KeyError: 'gone'

remove না পেলে থেমে যায়; discard চুপচাপ ছেড়ে দেয়। জিনিসটা থাকারই কথা হলে remove লিখুন, তাহলে না থাকলে সেটা ধরা পড়বে।

দুটি সেট মেলানো

এখানেই সেট সত্যিকারের কাজে আসে:

python
monday = {"rafi", "ahmed", "bilal"}
tuesday = {"ahmed", "dia"}

print(sorted(monday | tuesday))
print(sorted(monday & tuesday))
print(sorted(monday - tuesday))
print(sorted(monday ^ tuesday))
text
['ahmed', 'bilal', 'dia', 'rafi']
['ahmed']
['bilal', 'rafi']
['bilal', 'dia', 'rafi']

চারটি চিহ্ন, চারটি প্রশ্ন:

  • | — যেকোনোটিতে আছে (union)। দুদিনে মোট কারা এসেছে।
  • & — দুটিতেই আছে (intersection)। দুদিনই কারা এসেছে।
  • - — প্রথমটিতে আছে, দ্বিতীয়টিতে নেই (difference)। শুধু সোমবার কারা।
  • ^ — ঠিক একটিতে আছে (symmetric difference)। যারা একদিন এসেছে, দুদিন নয়।

খেয়াল করুন - ক্রম মানে: monday - tuesday আর tuesday - monday আলাদা উত্তর দেয়। বাকি তিনটি দুই দিকেই একই।

লুপ দিয়ে এই চারটি লিখতে গেলে প্রতিটির জন্য কয়েক লাইন লাগত, আর অন্তত একটিতে ভুল হওয়ার ভালো সম্ভাবনা থাকত।

কী রাখা যায়

ডিকশনারির কী-এর মতোই নিয়ম: যা রাখবেন তা অপরিবর্তনীয় হতে হবে।

python
s = set()
s.add([1, 2])
text
TypeError: unhashable type: 'list'

হুবহু সেই একই বার্তা, একই কারণে — সেটও ভেতরে হ্যাশ ব্যবহার করে। লেখা, সংখ্যা, টাপল চলে; লিস্ট আর ডিকশনারি চলে না।


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

sales.py:

python
# Which products sold on both days, and which only on one
monday = ["pen", "bag", "pen", "ink"]
tuesday = ["ink", "bottle", "pen", "ink"]

mon = set(monday)
tue = set(tuesday)

print("Monday sold    :", sorted(mon), f"({len(monday)} sales, {len(mon)} products)")
print("Tuesday sold   :", sorted(tue), f"({len(tuesday)} sales, {len(tue)} products)")
print()
print("Either day     :", sorted(mon | tue))
print("Both days      :", sorted(mon & tue))
print("Monday only    :", sorted(mon - tue))
print("Exactly one day:", sorted(mon ^ tue))
print()
print("Was ink sold on Monday?", "ink" in mon)
text
Monday sold    : ['bag', 'ink', 'pen'] (4 sales, 3 products)
Tuesday sold   : ['bottle', 'ink', 'pen'] (4 sales, 3 products)

Either day     : ['bag', 'bottle', 'ink', 'pen']
Both days      : ['ink', 'pen']
Monday only    : ['bag']
Exactly one day: ['bag', 'bottle']

Was ink sold on Monday? True

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

মূল লিস্টগুলো রাখা হয়েছে। len(monday) বলছে চারটি বিক্রি, len(mon) বলছে তিন রকম পণ্য। সেটে বদলানোর পর বিক্রির সংখ্যাটা আর জানা যেত না — তাই সেটটি মূল ডেটার বিকল্প নয়, তার উপর একটি প্রশ্ন করার হাতিয়ার।

প্রতিটি ছাপার আগে sorted()। না দিলে ক্রম এলোমেলো দেখাত, আর চালানোর মাঝে বদলেও যেতে পারত।

"ink" in mon লেখা হয়েছে, "ink" in monday নয়। দুটোই True দিত, কিন্তু সেটে খোঁজা লিস্টে খোঁজার চেয়ে দ্রুত — লিস্টে পাইথন একটি একটি করে মেলায়, সেটে সে হ্যাশ দিয়ে সরাসরি জায়গাটায় যায়। চারটি জিনিসে এর কোনো মানে নেই; চল্লিশ হাজারে আছে।


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

AttributeError: 'dict' object has no attribute 'add' s = {} লিখে সেট ভাবা হয়েছে, কিন্তু ওটা একটি খালি ডিকশনারি। s = set() লিখুন।

TypeError: 'set' object is not subscriptable s[0] লেখা হয়েছে। সেটে ক্রম নেই, তাই সূচকও নেই। একটি নির্দিষ্ট জিনিস দরকার হলে সম্ভবত লিস্ট বা ডিকশনারি দরকার ছিল; নয়তো sorted(s) দিয়ে একটি লিস্ট বানিয়ে নিন।

KeyError: 'gone' remove এমন কিছু বাদ দিতে বলা হয়েছে যা সেটে নেই। থাকা-না-থাকা দুটোই স্বাভাবিক হলে discard ব্যবহার করুন।

TypeError: unhashable type: 'list' একটি লিস্ট সেটে রাখার চেষ্টা। tuple(...) দিয়ে বদলে নিন।

list(set(...)) করার পর ক্রম এলোমেলো এটাই স্বাভাবিক, সেটে ক্রম নেই। ক্রম দরকার হলে উপরের seen প্যাটার্নটি ব্যবহার করুন, অথবা sorted()।

দুটি সেট == দিয়ে মেলাচ্ছি, ক্রম আলাদা হলেও True আসছে ঠিকই আসছে। দুটি সেট সমান যদি তাদের ভেতরের জিনিসগুলো এক হয় — ক্রম কোনো ভূমিকা রাখে না। লিস্টে [1, 2] == [2, 1] হলো False, সেটে {1, 2} == {2, 1} হলো True।