এই পর্বে যা শিখবেন
- set কী, list আর dict থেকে কোথায় আলাদা, আর কোন প্রশ্নের জন্য set সবচেয়ে ভালো
- set তৈরি, ডুপ্লিকেট দূর করা (ক্রম রেখে আর ক্রম ছাড়া), আর
add,remove,discard,pop - list-এ খোঁজা আর set-এ খোঁজার গতির পার্থক্য কেন এত বড়
- union, intersection, difference, symmetric difference, subset আর superset — operator ও method দুই রূপে
- frozenset, hashable-এর শর্ত, set-এর ক্রমের ফাঁদ, আর AI-এর লেখা set-কোড যাচাই
১ · set কী — অনন্য মানের ক্রমহীন ব্যাগ
আগের তিনটি পর্বে তিন রকম সংগ্রহ দেখেছেন। list হলো ক্রম মেনে সাজানো সারি — কে প্রথম, কে দ্বিতীয়, তা গুরুত্বপূর্ণ, আর একই জিনিস বারবার থাকতে পারে। tuple হলো বদলানো যায় না এমন সারি। dict হলো চাবি থেকে মানে যাওয়ার খাতা। এবার চতুর্থটি — set.
set-এর দুটি মাত্র নিয়ম, আর দুটিই গুরুত্বপূর্ণ:
- প্রতিটি মান একবারই থাকে। একই মান দশবার যোগ করলেও set-এ সেটি একটিই।
- কোনো ক্রম নেই। set-এ “প্রথম উপাদান” বা “৩ নম্বর উপাদান” বলে কিছু নেই। তাই
my_set[0]লেখা যায় না, slicing-ও চলে না।
তাহলে set কোন প্রশ্নের উত্তর দেয়? মূলত দুই ধরনের প্রশ্ন। এক, “এটা কি আগে দেখেছি?” — যেমন একটি ওয়েবসাইটে আজ কতজন আলাদা মানুষ এসেছেন, একই ফোন নম্বর তালিকায় দুবার আছে কি না। দুই, “দুটি দলের মধ্যে সম্পর্ক কী?” — কে দুটো দলেই আছে, কে শুধু একটিতে, একটি দল কি আরেকটির পুরোপুরি ভেতরে। স্কুলের গণিত বইয়ের সেট অধ্যায় আর Venn diagram মনে আছে? Python-এর set হুবহু সেই ধারণা।
একটি স্টিকার অ্যালবামের কথা ভাবুন — বিশ্বকাপের খেলোয়াড়দের স্টিকার। আপনার অ্যালবামে মাশরাফির স্টিকার একবার লাগানো থাকলে দ্বিতীয়টি লাগানোর জায়গা নেই; সেটি “ডুপ্লিকেট”, বন্ধুর সাথে বদলানোর জন্য আলাদা রাখেন। আর বন্ধু যখন জিজ্ঞেস করেন “সাকিব আছে?”, আপনি পুরো অ্যালবাম পাতা পাতা উল্টান না — জানেন সাকিবের স্টিকার কোন পাতার কোন ঘরে বসে, সরাসরি সেখানে তাকান। কোন ক্রমে স্টিকারগুলো পেয়েছিলেন, তা অ্যালবাম মনে রাখে না। set ঠিক এমন: আছে কি নেই, একবারই, আর খোঁজা সরাসরি ঘরে গিয়ে।
| ধরন | ক্রম আছে? | ডুপ্লিকেট? | বদলানো যায়? | সবচেয়ে ভালো কাজ |
|---|---|---|---|---|
list [1, 2, 2] | হ্যাঁ | হ্যাঁ | হ্যাঁ | ক্রম মেনে রাখা, index দিয়ে পাওয়া |
tuple (1, 2, 2) | হ্যাঁ | হ্যাঁ | না | স্থির রেকর্ড, dict-এর চাবি |
dict {"a": 1} | যোগ করার ক্রম | চাবি অনন্য | হ্যাঁ | চাবি দিয়ে মান খোঁজা |
set {1, 2} | না | না | হ্যাঁ | “আছে কি?”, ডুপ্লিকেট দূর, দলের তুলনা |
| frozenset | না | না | না | set যখন চাবি বা অন্য set-এর সদস্য হবে |
set আসলে চাবি আছে কিন্তু মান নেই এমন একটি dict. পর্ব ১৬-এ দেখেছেন, dict চাবিগুলো একটি hash table-এ রাখে, আর তাই চাবি দিয়ে খোঁজা এত দ্রুত। set একই কৌশল ব্যবহার করে — শুধু প্রতিটি চাবির পাশে কোনো মান রাখে না। dict-এর চাবির সব নিয়ম (অনন্য হতে হবে, hashable হতে হবে) তাই set-এর উপাদানের জন্যও খাটে।
২ · set তৈরি — আর {}-এর ফাঁদ
set বানানোর দুটি প্রধান উপায়:
🧺 বন্ধনী দিয়ে সরাসরি
মানগুলো কোঁকড়া বন্ধনীতে, কমা দিয়ে আলাদা। ডুপ্লিকেট দিলেও একটিই থাকবে — {3, 1, 3, 2}-এ তিনটি উপাদান।
🔄 set() দিয়ে রূপান্তর
যেকোনো iterable (list, tuple, string, range) থেকে set. খালি set বানানোর একমাত্র উপায়ও এটি।
empty = set(){} লিখলে খালি set নয়, খালি dict পাবেন। কোঁকড়া বন্ধনী Python-এ প্রথমে এসেছিল dict-এর জন্য, set-এর বন্ধনী-রূপ এসেছে অনেক পরে। তাই type({}) দেয় <class 'dict'>. এই ভুল নতুনদের আর AI-এর কোডে প্রায়ই দেখা যায়: seen = {} লিখে পরে seen.add(x) — ফল AttributeError: 'dict' object has no attribute 'add'. মনে রাখার উপায়: খালি set-এর ছাপা রূপও set(), কারণ {} নামটি dict আগেই নিয়ে নিয়েছে।
set()-এ একটি string দিলে কী হয়? string নিজেও একটি iterable — একটি একটি করে অক্ষর দেয় (পর্ব ০৯)। তাই set("banana") দেয় তিনটি অক্ষর: 'b', 'a', 'n'. অনেকে ভাবেন এতে একটি শব্দের set তৈরি হবে — হয় না। একটি শব্দের set চাইলে লিখুন {"banana"}. বাংলায় set("কিছু") দেবে 'ক', 'ি', 'ছ', 'ু' — কার চিহ্নও আলাদা code point, মনে আছে নিশ্চয়ই।
set-এ কী রাখা যায় — hashable-এর শর্ত
set-এর প্রতিটি উপাদানকে hashable হতে হবে — অর্থাৎ তার একটি hash মান থাকতে হবে, যা object-এর জীবনভর বদলায় না। সংখ্যা, string, tuple (ভেতরে শুধু hashable জিনিস থাকলে), None, True/False — সব hashable. কিন্তু list, dict আর সাধারণ set নিজে mutable, তাই hashable নয়। কেন এই নিয়ম, পর্ব ১৬-এ dict-এর চাবির আলোচনায় দেখেছেন: object বদলালে তার hash বদলাবে, আর সে ভুল ঘরে বসে থাকবে — খুঁজে আর পাওয়া যাবে না। অংশ ৭-এ এর সমাধান দেখবেন।
একটি ছোট চমক: {1, 1.0, True}-এ কয়টি উপাদান? উত্তর — একটি! Python-এ 1 == 1.0 == True, আর সমান সংখ্যার hash-ও সমান রাখা হয়েছে। set-এর কাছে এরা একই মান, তাই প্রথমটি (1) থেকে যায়। বাস্তবে এ নিয়ে খুব কম ঝামেলা হয়, কিন্তু পরীক্ষার “আউটপুট কী?” প্রশ্নে এটি জনপ্রিয়।
৩ · ডুপ্লিকেট দূর করা — set আর dict.fromkeys
set-এর সবচেয়ে পরিচিত ব্যবহার: একটি তালিকা থেকে বারবার আসা মান সরিয়ে ফেলা, যাকে বলে deduplication. ধরুন একটি অনলাইন দোকানের অর্ডার থেকে গ্রাহকদের জেলাগুলো পেয়েছেন — ঢাকা বারবার, খুলনা দুবার। কোন কোন জেলা থেকে অর্ডার এসেছে? set(districts) — এক লাইনেই উত্তর।
কিন্তু একটি দাম আছে: ক্রম হারিয়ে যায়। set-এর কোনো ক্রম নেই, তাই কোন জেলা প্রথমে এসেছিল তা আর জানা যায় না। কখনো সেটি সমস্যা নয় (শুধু গুনতে চাইলে len(set(districts))). কিন্তু যদি দরকার হয় “প্রথম যে ক্রমে এসেছিল, সেই ক্রমে আলাদা জেলাগুলো” — তখন একটি কৌশল আছে: list(dict.fromkeys(districts)). Python 3.7 থেকে dict যোগ করার ক্রম মনে রাখে বলে নিশ্চিত, আর dict-এর চাবিও অনন্য। ধাপে ধাপে দুটি পথ পাশাপাশি দেখুন:
1districts = ["ঢাকা", "খুলনা", "ঢাকা", "সিলেট", "খুলনা"]2unique = set(districts)3print(sorted(unique))4ordered = dict.fromkeys(districts)5print(list(ordered))
| districts | |
| unique (set) | |
| ordered (dict) |
- একটি list, ডুপ্লিকেটসহ। পাঁচটি অর্ডারের জেলা — “ঢাকা” দুবার, “খুলনা” দুবার। ক্রমটিও তথ্য: ঢাকা থেকে প্রথম অর্ডার এসেছিল, সিলেট থেকে সবার শেষে।
set(districts)— ডুপ্লিকেট উধাও, ক্রমও উধাও। set প্রতিটি জেলার hash হিসাব করে নিজের ঘরে বসায়; দ্বিতীয় “ঢাকা” এসে দেখে ঘরে আগেই “ঢাকা” আছে, তাই কিছু হয় না। ফল তিনটি জেলা। কিন্তু কোনটি কোন ঘরে, তা নির্ভর করে hash-এর ওপর — আর string-এর hash প্রতিবার প্রোগ্রাম চালালে বদলাতে পারে (অংশ ৮)।- মানুষকে দেখানোর আগে
sorted(). set সরাসরি ছাপলে আজ একরকম, কাল আরেকরকম ক্রম আসতে পারে।sorted()একটি নতুন list দেয়, Unicode ক্রমে সাজানো — খ, ঢ, স. এই অভ্যাস পুরো পর্বে মেনে চলব। dict.fromkeys— একটি একটি করে চাবি। এটি list-এর প্রতিটি মানকে চাবি বানিয়ে একটি dict তৈরি করে, সব মানNone. “ঢাকা” যোগ হলো, “খুলনা” যোগ হলো, আবার “ঢাকা” এল — চাবি আগেই আছে, তাই নতুন কিছু যোগ হয় না, আর পুরোনো অবস্থানও বদলায় না।- সব দেখা শেষ। “সিলেট” নতুন, যোগ হলো; শেষ “খুলনা” আগেই আছে। dict-এ তিনটি চাবি, ঠিক যে ক্রমে প্রথমবার দেখা গিয়েছিল। এই ক্রম Python 3.7 থেকে ভাষার নিয়মে নিশ্চিত — কোনো ভাগ্যের ব্যাপার নয়।
list(ordered)— ক্রম রেখে অনন্য। dict-এর ওপরlist()চাবিগুলো দেয়। ফল['ঢাকা', 'খুলনা', 'সিলেট']— প্রথম আসার ক্রমে। দুটি আউটপুট মিলিয়ে দেখুন: একই তিনটি জেলা, কিন্তু প্রথমটি বর্ণানুক্রমে (আমরা সাজিয়েছি বলে), দ্বিতীয়টি আসার ক্রমে।
| লেখা | ফল | ক্রম | কখন ব্যবহার করবেন |
|---|---|---|---|
set(xs) | set | হারায় | শুধু আলাদা মানগুলো বা তাদের সংখ্যা দরকার |
sorted(set(xs)) | list | নতুন, সাজানো | আলাদা মানগুলো ছাপতে বা রিপোর্টে দিতে |
list(dict.fromkeys(xs)) | list | প্রথম আসার ক্রম | ক্রমটি নিজেই তথ্য (যেমন প্রথম অর্ডারের সময়) |
৪ · set বদলানো — add, remove, discard, pop
set নিজে mutable — তৈরির পরে উপাদান যোগ করা আর সরানো যায়। কিন্তু index নেই বলে list-এর মতো “৩ নম্বর জায়গায় বসাও” বলা যায় না; শুধু “এটা যোগ করো” বা “এটা সরাও”. প্রধান method-গুলো:
| লেখা | কী করে | মান না থাকলে / আগেই থাকলে |
|---|---|---|
s.add(x) | একটি উপাদান যোগ | আগেই থাকলে কিছুই হয় না, কোনো error নেই |
s.update(xs) | একটি iterable-এর সব উপাদান যোগ | ডুপ্লিকেট চুপচাপ বাদ |
s.remove(x) | একটি উপাদান সরায় | না থাকলে KeyError |
s.discard(x) | একটি উপাদান সরায় | না থাকলে কিছুই হয় না |
s.pop() | যেকোনো একটি উপাদান সরিয়ে ফেরত দেয় | কোনটি — বলা যায় না; set খালি হলে KeyError |
s.clear() | সব সরায়, set খালি | — |
add আর update গুলিয়ে ফেলবেন না। tags.add("python") একটি শব্দ যোগ করে। কিন্তু tags.update("python") string-কে iterable হিসেবে ধরে ছয়টি আলাদা অক্ষর যোগ করে — 'p', 'y', 't'… একটি শব্দ যোগ করতে update ব্যবহার করতে হলে লিখুন tags.update(["python"]). এই ভুল কোনো error দেয় না, শুধু set-কে অক্ষরে ভরে দেয়।
remove না discard?
দুটিই উপাদান সরায়; পার্থক্য শুধু মান না থাকলে। remove একটি KeyError দেয় — যেন বলছে “যা সরাতে বলা হলো, তা তো নেই!” discard চুপ থাকে। কোনটি বেছে নেবেন, তা নির্ভর করে “না থাকা” আপনার প্রোগ্রামে ভুল কি না তার ওপর:
- একটি পরীক্ষার হলে খাতা জমা দেওয়ার পর রোল নম্বরটি “বাকি” set থেকে সরাচ্ছেন। যে রোল তালিকাতেই নেই, তার খাতা আসা মানে কোথাও গোলমাল — সেখানে
removeভালো; ভুলটি সঙ্গে সঙ্গে চোখে পড়বে। - একটি ওয়েবসাইটে লগআউট করলে ব্যবহারকারীকে “অনলাইন” set থেকে সরাচ্ছেন। কেউ দুবার লগআউট চাপলে দ্বিতীয়বার তিনি আর set-এ নেই — এটা স্বাভাবিক, ভুল নয়। সেখানে
discard.
pop() নিয়ে একটি সতর্কতা: list-এর pop() শেষ উপাদান দেয়, কিন্তু set-এর কোনো “শেষ” নেই। তাই set-এর pop() একটি যেকোনো উপাদান দেয় — কোনটি দেবে তা নির্ভর করে ভেতরের ঘরের অবস্থার ওপর। “লটারি” বা “এলোমেলো বাছাই” হিসেবে একে ব্যবহার করবেন না; সেটি এলোমেলোও নয়, নির্ভরযোগ্যও নয়। এলোমেলো বাছাইয়ের জন্য random module আছে (পর্ব ২৭)। pop()-এর আসল কাজ — “কাজের ঝুড়ি” থেকে একটি একটি করে কাজ তুলে নেওয়া, যখন ক্রমে কিছু আসে যায় না।
৫ · in দিয়ে খোঁজা — list বনাম set
পর্ব ০৮-এ membership operator in দেখেছেন: "ঢাকা" in districts — আছে কি নেই। লেখায় list আর set-এ একই রকম দেখায়, কিন্তু ভেতরে কাজ সম্পূর্ণ আলাদা। ধরুন একটি বিশ্ববিদ্যালয়ের ভর্তি পরীক্ষায় ১০,০০০ জনের রোল নম্বর আছে, আর জানতে চান 10001 রোলধারী তালিকায় আছেন কি না:
1rolls_list = list(range(1, 10001))2rolls_set = set(rolls_list)3print(10001 in rolls_list)4print(10001 in rolls_set)5# ১০,০০০ বার খুঁজলে?
| এখন যা দেখা হচ্ছে | |
| তুলনার সংখ্যা |
- একই ১০,০০০টি রোল, দুই রূপে। একটি list, একটি set. set বানাতে একবার একটু খরচ হয় — প্রতিটি রোলের hash হিসাব করে ঘরে বসাতে হয়। এই খরচ শুধু একবার।
- list-এ খোঁজা শুরু। list জানে না 10001 কোথায় থাকতে পারে। তাই ঘর 0 থেকে শুরু: “এটা কি 10001?” — না। এটা পর্ব ০২-এ দেখা linear search.
- একটি একটি করে… ঘর 1, ঘর 2, ঘর 3… প্রতিটির সাথে তুলনা। list বড় হলে কাজ ঠিক ততগুণ বাড়ে।
- শেষ পর্যন্ত — ১০,০০০ তুলনা, উত্তর False. যা নেই তা খুঁজতে list-কে পুরোটা দেখতে হয়, কারণ শেষ ঘরেও থাকতে পারত। একে বলে O(n) — Big-O ভাষায় “উপাদানের সংখ্যার সমানুপাতিক কাজ”.
- set-এ খোঁজা — আগে hash. set প্রথমে
hash(10001)হিসাব করে, তারপর সেই সংখ্যা থেকে বের করে “এই মান থাকলে কোন ঘরে থাকত”. কোনো তুলনা এখনো হয়নি। - শুধু সেই ঘরে তাকানো। ঘরটি ফাঁকা (বা অন্য কোনো মান) — তাহলে 10001 নেই। এক বা খুব কম কয়েকটি তুলনায় উত্তর। set-এ ১০ লাখ উপাদান থাকলেও গড়ে কাজ প্রায় একই — একে বলে গড় O(1).
- আসল পার্থক্য দেখা যায় লুপে। ধরুন ১০,০০০ জনের পরীক্ষার খাতার রোল মিলিয়ে দেখবেন প্রত্যেকে নিবন্ধিত কি না — লুপের ভেতরে
in. list হলে প্রতিবার ১০,০০০ পর্যন্ত তুলনা, মোট প্রায় ১০ কোটি (O(n²)); set হলে মোট মোটামুটি ১০,০০০. এক মুহূর্ত বনাম লক্ষণীয় অপেক্ষা — আর ডেটা দশগুণ হলে list-এর কাজ বাড়ে একশো গুণ।
সংখ্যাগুলো দেখলে পার্থক্যটি পরিষ্কার হয়। নিচের টেবিলে আছে “যা নেই তা খুঁজতে” সবচেয়ে খারাপ ক্ষেত্রে কতগুলো তুলনা লাগে:
| উপাদানের সংখ্যা | list-এ একবার খোঁজা | set-এ একবার খোঁজা (গড়) | লুপে n বার খুঁজলে (list) |
|---|---|---|---|
| ১০০ | ১০০ তুলনা | প্রায় ১ | ১০,০০০ |
| ১০,০০০ | ১০,০০০ তুলনা | প্রায় ১ | ১০ কোটি |
| ১০ লাখ | ১০ লাখ তুলনা | প্রায় ১ | ১ লাখ কোটি — কার্যত অসম্ভব |
কীভাবে set “সরাসরি ঘরে” যায়, তার ভেতরের গল্প — hash function, bucket, দুটি চাবি একই ঘরে পড়লে কী হয় — পর্ব ১৬-এ dict-এর সাথে দেখেছেন; set-এর ভেতরেও হুবহু একই ব্যবস্থা। এখানে শুধু ফলটি মনে রাখুন: set-এ “আছে কি?” প্রশ্নের দাম গড়ে set-এর আকারের ওপর নির্ভর করে না।
“গড়” শব্দটি কেন? কারণ খুব খারাপ ভাগ্যে (বা কেউ ইচ্ছে করে একই ঘরে পড়ে এমন মান পাঠালে) অনেক মান একই ঘরের আশেপাশে জট পাকাতে পারে, তখন খোঁজা ধীর হয়। বাস্তবে Python-এর hash এমনভাবে নকশা করা যে সাধারণ ডেটায় এটি প্রায় ঘটে না। আর ছোট তালিকায় (দশ-বিশটি জিনিস) list আর set-এর পার্থক্য চোখে পড়ার মতো নয় — তখন যেটি পড়তে সহজ, সেটিই নিন।
in? আগে set বানিয়ে নিনallowed = set(allowed_list) — একবার খরচ, তারপর প্রতিবার সস্তা৬ · সেট-গণিত — Venn diagram কোডে
এবার set-এর দ্বিতীয় শক্তি — দুটি দলের তুলনা। একটি কলেজে Physics প্র্যাকটিক্যালে নাম লিখিয়েছেন রহিম, সুমি, করিম আর নাদিয়া; Chemistry-তে সুমি, নাদিয়া, তানভীর আর মিতু। স্কুলের খাতায় আপনি দুটি বৃত্ত আঁকতেন, মাঝখানের মিলিত অংশে সুমি আর নাদিয়া। Python-এ চারটি প্রশ্ন, চারটি operator — ধাপে ধাপে দেখুন কোন অংশ বেছে নেওয়া হচ্ছে:
physics = {"রহিম", "সুমি", "করিম", "নাদিয়া"}
chemistry = {"সুমি", "নাদিয়া", "তানভীর", "মিতু"}
sorted(physics | chemistry)
['করিম', 'তানভীর', 'নাদিয়া', 'মিতু', 'রহিম', 'সুমি']
sorted(physics & chemistry)
['নাদিয়া', 'সুমি']
sorted(physics - chemistry) ['করিম', 'রহিম'] sorted(chemistry - physics) # উল্টো দিক — ভিন্ন উত্তর ['তানভীর', 'মিতু']
sorted(physics ^ chemistry)
['করিম', 'তানভীর', 'মিতু', 'রহিম']
- তিনটি অঞ্চল। দুটি বৃত্ত মিলে তিনটি অংশ: শুধু Physics (নীল), দুটোতেই (বেগুনি), শুধু Chemistry (সবুজ)। পরের প্রতিটি operator এই তিনটির কোনগুলো নেয়, তা উজ্জ্বল হয়ে দেখাবে। লক্ষ করুন, ছাপার আগে প্রতিবার
sorted(). |— union: “যেকোনো একটিতে”. তিন অংশই। কতজন অন্তত একটি প্র্যাকটিক্যাল করছেন — ছয়জন। সুমি আর নাদিয়া দুই তালিকায় থাকলেও ফলাফলে একবারই, কারণ ফল নিজেও একটি set. ল্যাবের জন্য মোট কতগুলো অ্যাপ্রন লাগবে, এই প্রশ্নের উত্তর।&— intersection: “দুটোতেই”. শুধু মাঝের অংশ — সুমি আর নাদিয়া। দুটি প্র্যাকটিক্যালের সময় একই দিনে পড়লে এই দুজনের সমস্যা হবে, তাই রুটিন বানানোর আগে এই প্রশ্নটিই জরুরি। ফেসবুকের “mutual friends”-ও একটি intersection.-— difference: “এটিতে আছে, ওটিতে নেই”.physics - chemistryদেয় শুধু নীল অংশ: রহিম আর করিম। সাবধান — বিয়োগের মতোই এর দিক আছে:chemistry - physicsদেয় সবুজ অংশ, সম্পূর্ণ ভিন্ন উত্তর। “নিবন্ধন করেছেন কিন্তু আসেননি” =registered - attended, উল্টো নয়।^— symmetric difference: “ঠিক একটিতে”. দুই পাশের অংশ, মাঝেরটি বাদ — রহিম, করিম, তানভীর, মিতু। মনে রাখার উপায়:(a | b) - (a & b). দুটি তালিকার “অমিল” খুঁজতে কাজে লাগে — যেমন গতকালের আর আজকের স্টক তালিকায় কোন পণ্য নতুন এল বা বাদ গেল।
operator আর method — একই কাজ, দুই রূপ
প্রতিটি operator-এর একটি নামযুক্ত method-ও আছে। একটি গুরুত্বপূর্ণ পার্থক্য: operator দুই পাশেই set চায়, method যেকোনো iterable নেয়। physics | ["মিতু"] লিখলে TypeError, কিন্তু physics.union(["মিতু"]) ঠিকঠাক চলে। তালিকা হাতে থাকলে method সুবিধাজনক; দুটিই set হলে operator পড়তে সংক্ষিপ্ত।
| প্রশ্ন | operator | method | নিজেকেই বদলাতে |
|---|---|---|---|
| যেকোনো একটিতে | a | b | a.union(b) | a |= b বা a.update(b) |
| দুটোতেই | a & b | a.intersection(b) | a &= b |
| a-তে আছে, b-তে নেই | a - b | a.difference(b) | a -= b |
| ঠিক একটিতে | a ^ b | a.symmetric_difference(b) | a ^= b |
| a কি b-এর ভেতরে? | a <= b | a.issubset(b) | — |
| a কি b-কে ধারণ করে? | a >= b | a.issuperset(b) | — |
| কোনো মিল নেই? | — | a.isdisjoint(b) | — |
subset, superset আর disjoint
শেষ তিনটি প্রশ্ন উত্তর দেয় True বা False. a <= b মানে a-র প্রতিটি উপাদান b-তেও আছে — a হলো b-এর subset. একটি চাকরির আবেদনে চাওয়া কাগজপত্র যদি required হয় আর জমা দেওয়া কাগজ submitted, তাহলে “সব জমা হয়েছে?” প্রশ্নটি হুবহু required <= submitted. উল্টো দিক থেকে submitted >= required — submitted হলো superset. আর a < b (শুধু ছোট-চিহ্ন) মানে proper subset — a, b-এর ভেতরে, কিন্তু b-তে অন্তত একটি বাড়তি জিনিস আছে।
a.isdisjoint(b) জিজ্ঞেস করে “দুটির মধ্যে কি একটিও মিল নেই?” — দুটি disjoint set. যেমন সকাল আর বিকেলের শিফটে একই কর্মীর নাম কি দুবার পড়েছে? morning.isdisjoint(evening) হলে True — কোনো সংঘাত নেই। এটি not (a & b)-এর সমান, কিন্তু প্রথম মিল পেলেই থেমে যায় আর নতুন set বানায় না।
এবার সব একসাথে চালিয়ে দেখুন — দুই প্র্যাকটিক্যালের তালিকা, একটি সেমিনারের নিবন্ধন বনাম উপস্থিতি, আর খাতা জমা না দেওয়া রোল নম্বর খোঁজা:
physics = {"রহিম", "সুমি", "করিম", "নাদিয়া"}
chemistry = {"সুমি", "নাদিয়া", "তানভীর", "মিতু"}
print("দুটোতেই:", sorted(physics & chemistry))
print("যেকোনো একটিতে:", len(physics | chemistry), "জন")
print("শুধু Physics:", sorted(physics - chemistry))
print("ঠিক একটিতে:", sorted(physics ^ chemistry))
registered = {101, 102, 103, 104, 105, 106}
attended = {101, 103, 104, 106, 107}
print("নিবন্ধন করেও আসেননি:", sorted(registered - attended))
print("নিবন্ধন ছাড়াই এসেছেন:", sorted(attended - registered))
print("সবাই নিবন্ধিত?", attended <= registered)
all_rolls = set(range(1, 11))
submitted = [1, 2, 4, 5, 7, 8, 10, 2] # 2 দুবার জমা পড়েছে
print("খাতা জমা দেননি:", sorted(all_rolls - set(submitted)))প্রথম চার লাইন Venn diagram-এর চারটি প্রশ্ন। & দেয় নাদিয়া আর সুমি (Unicode ক্রমে ন আগে, স পরে); |-এর সদস্য সংখ্যা ৬ জন; - দেয় করিম আর রহিম; ^ দেয় চারজন। তারপর সেমিনারের হিসাব: 102 আর 105 নিবন্ধন করেও আসেননি, আর 107 নিবন্ধন ছাড়াই এসেছেন — তাই শেষ প্রশ্ন “সব উপস্থিত কি নিবন্ধিত?” (attended <= registered) এর উত্তর False. শেষ লাইন একটি পরিচিত সমস্যা: ১ থেকে ১০ রোলের মধ্যে কারা খাতা জমা দেননি — উত্তর [3, 6, 9]. জমার তালিকায় 2 দুবার থাকলেও কিছু যায় আসে না, কারণ set(submitted) ডুপ্লিকেট আগেই ফেলে দিয়েছে। লুপ বা if কোথাও লাগেনি। প্রতিটি ফল ছাপার আগে sorted() করা হয়েছে, তাই আউটপুট প্রতিবার একই।
৭ · frozenset আর hashable-এর শর্ত
অংশ ২-এ দেখেছেন, set-এর উপাদান hashable হতে হবে। তাহলে list-এর set বানাতে চাইলে কী হয়? {["ঢাকা", "খুলনা"], ["সিলেট"]} লিখলে Python সঙ্গে সঙ্গে TypeError দেয়, বার্তায় থাকে unhashable type: 'list'. একই কারণে set-এর ভেতরে আরেকটি সাধারণ set রাখা যায় না, আর set-কে dict-এর চাবিও করা যায় না।
সমাধান দুটি, ডেটার ধরন অনুযায়ী:
- ক্রম গুরুত্বপূর্ণ হলে tuple.
("ঢাকা", "চট্টগ্রাম")একটি hashable জোড়া — immutable, তাই তার hash কখনো বদলায় না। কিন্তু tuple-এ ক্রম আছে:("ঢাকা", "চট্টগ্রাম")আর("চট্টগ্রাম", "ঢাকা")আলাদা। - ক্রম গুরুত্বহীন হলে frozenset. এটি set-এর বরফ-জমা রূপ — একবার তৈরি হলে আর বদলানো যায় না (
add,removeনেই), কিন্তু union, intersection,in— পড়ার সব কাজ চলে। বদলায় না বলে এটি hashable, তাই dict-এর চাবি বা set-এর সদস্য হতে পারে।
একটি বাস কোম্পানি ঢাকা–চট্টগ্রাম রুটের ভাড়া রাখছে। যাত্রী ঢাকা থেকে চট্টগ্রাম যান বা চট্টগ্রাম থেকে ঢাকা — ভাড়া একই। tuple চাবি দিলে দুটি দিকের জন্য দুটি আলাদা এন্ট্রি লাগত, আর একটিতে ভাড়া বদলে অন্যটিতে ভুলে যাওয়ার সুযোগ থাকত। frozenset({"ঢাকা", "চট্টগ্রাম"}) চাবি দিলে দুটি দিকই একই চাবি — কারণ set-এ ক্রম নেই। এখানে set-এর “ক্রমহীনতা” দুর্বলতা নয়, এটিই আসল সুবিধা।
নিচের কোডে বাস্তব কয়েকটি কাজ — একটি কন্টাক্ট লিস্টে ডুপ্লিকেট ফোন নম্বর খোঁজা, চাকরির আবেদনে কাগজপত্র যাচাই, list-এর set বানানোর চেষ্টা, আর frozenset চাবি:
phones = ["01711000001", "01811000002", "01711000001",
"01911000003", "01811000002", "01711000001"]
print("আলাদা নম্বর:", len(set(phones)))
seen, dupes = set(), set()
for p in phones:
if p in seen:
dupes.add(p)
seen.add(p)
print("ডুপ্লিকেট:", sorted(dupes))
required = {"ছবি", "NID", "সনদ", "CV"}
submitted = {"CV", "ছবি", "NID"}
print("সব জমা?", required <= submitted, "| বাকি:", sorted(required - submitted))
try:
routes = {["ঢাকা", "চট্টগ্রাম"]}
except TypeError as e:
print("TypeError:", e)
fare = {frozenset({"ঢাকা", "চট্টগ্রাম"}): 850}
print(fare[frozenset({"চট্টগ্রাম", "ঢাকা"})])ছয়টি নম্বরের মধ্যে আলাদা নম্বর ৩টি। ডুপ্লিকেট খোঁজার লুপটি set-এর ক্লাসিক “আগে দেখেছি?” প্যাটার্ন: seen set-এ যা দেখা হয়েছে, আর আবার দেখা গেলে সেটি dupes-এ যায়। 01711000001 তিনবার এলেও dupes-এ একবারই, কারণ সেটিও set. দুটি নম্বর ডুপ্লিকেট — সাজিয়ে ছাপা হলো। seen list হলে প্রতিটি in পুরো list দেখত; হাজার হাজার কন্টাক্টে সেটি টের পাওয়া যেত। কাগজপত্রের হিসাবে “সনদ” জমা পড়েনি, তাই required <= submitted হলো False. এরপর list-এর set বানানোর চেষ্টা TypeError দিল; try/except দিয়ে ধরে বার্তাটি ছাপা হয়েছে (exception ধরা পর্ব ২৪-এ শিখবেন) — বার্তায় unhashable type: 'list' অংশটি খেয়াল করুন। শেষে frozenset চাবি: ভাড়া রাখা হয়েছিল “ঢাকা, চট্টগ্রাম” ক্রমে, খোঁজা হলো উল্টো ক্রমে — তবু 850 পাওয়া গেল।
৮ · set-এর ক্রমের ফাঁদ — কেন সবসময় sorted()
REPL-এ {"রহিম", "সুমি", "করিম"} লিখুন। একটি ক্রমে ছাপা হবে। এবার Python বন্ধ করে আবার খুলে একই লাইন লিখুন — সম্ভবত অন্য ক্রমে ছাপা হবে। এটা bug নয়; ইচ্ছাকৃত।
কারণ দুটি স্তরে। প্রথমত, set উপাদানগুলো রাখে hash থেকে হিসাব করা ঘরে, যোগ করার ক্রমে নয় — তাই যে ক্রমে ঘরগুলো সাজানো, সেই ক্রমেই ছাপা হয়। দ্বিতীয়ত, Python 3.3 থেকে string আর bytes-এর hash প্রতিবার প্রোগ্রাম শুরু হলে একটি গোপন এলোমেলো বীজ (seed) মিশিয়ে হিসাব হয় — একে বলে hash randomization. ফলে একই string-এর hash আজ এক, কাল আরেক; আর তাই ঘর, আর ছাপার ক্রমও। এর পেছনে একটি নিরাপত্তার গল্প আছে (ভাবনার প্রশ্ন ৩)।
ছোট int-এর set দেখে বিভ্রান্ত হবেন না। {3, 1, 2} ছাপলে প্রায়ই {1, 2, 3} আসে — মনে হয় set সাজিয়ে রাখে। আসলে CPython-এ ছোট int-এর hash তার নিজের মানই, তাই তারা ক্রমিক ঘরে বসে। {100, 5, 37} বা বড় সংখ্যায় এই “ক্রম” ভেঙে যায়, আর ভাষার নিয়মে এর কোনো নিশ্চয়তা কখনোই ছিল না। কোড এমন কিছুর ওপর দাঁড় করাবেন না, যা শুধু কাকতালীয়ভাবে ঠিক দেখায়।
বাস্তবে এই ফাঁদ কোথায় কামড়ায়?
- টেস্ট: প্রত্যাশিত আউটপুট
"{'ঢাকা', 'খুলনা'}"লিখে রাখলেন। আপনার কম্পিউটারে পাস করল, সার্ভারে ফেল — কারণ সেখানে ক্রম উল্টো। set-এর তুলনা set দিয়েই করুন (result == {"ঢাকা", "খুলনা"}— ক্রম ছাড়াই সমান), অথবাsorted()দিয়ে। - রিপোর্ট আর ফাইল: প্রতিবার ভিন্ন ক্রমের রিপোর্ট দেখে ব্যবহারকারী ভাবেন ডেটা বদলেছে। git-এ রাখা ফাইলে প্রতিবার অকারণ পরিবর্তন দেখায়।
- “প্রথম উপাদান” নেওয়া:
list(s)[0]বাnext(iter(s))দিয়ে “প্রথমটি” নিলেন — সেটি প্রতিবার ভিন্ন হতে পারে।
set দিয়ে হিসাব করুন, list দিয়ে দেখান। সদস্যতা, ডুপ্লিকেট, সেট-গণিত — এসব set-এর কাজ। কিন্তু মানুষ, ফাইল বা টেস্টের জন্য আউটপুট বানানোর শেষ ধাপে sorted(s) (বা নির্দিষ্ট কোনো ক্রম) — যাতে একই ইনপুটে সবসময় একই আউটপুট আসে।
একটু আগাম ঝলক — set comprehension
পরের পর্বে comprehension শিখবেন — এক লাইনে list, dict বা set বানানো। set-এর রূপটি দেখতে এমন: {addr.split(",")[-1].strip() for addr in addresses} — প্রতিটি ঠিকানার শেষ অংশ (জেলা) নিয়ে একটি set, অর্থাৎ ঠিকানার তালিকা থেকে আলাদা জেলাগুলো। কোঁকড়া বন্ধনী, ভেতরে for — এটি একটি set comprehension. বিস্তারিত পর্ব ১৮-এ।
৯ · কখন set — বাস্তব কাজের তালিকা
একটি সহজ পরীক্ষা: আপনার প্রশ্নে কি “আছে কি?”, “আলাদা কতগুলো?”, “দুটোতেই কে?”, “কোনটি বাকি?” — এমন শব্দ আছে? থাকলে set-এর কথা ভাবুন।
👥 অনন্য ভিজিটর
একটি ওয়েবসাইটের লগে একই মানুষ দিনে বিশবার আসতে পারেন। len(set(user_ids)) — আজ কতজন আলাদা মানুষ।
🏷️ ট্যাগ
একটি ব্লগ পোস্টের ট্যাগ — একই ট্যাগ দুবার রাখার মানে নেই। দুটি পোস্টের মিল কতটা? tags_a & tags_b.
🤝 মিউচুয়াল ফ্রেন্ড
দুজনের বন্ধু-তালিকার intersection. “আপনি হয়তো চেনেন” পরামর্শের একটি সরল রূপ: বন্ধুর বন্ধু, যিনি এখনো আপনার বন্ধু নন — একটি difference.
friends_a & friends_b📋 বাকি কাজ
যে রোল নম্বরের খাতা আসেনি, যে কাগজ জমা পড়েনি, যে পণ্য স্টকে নেই — সবই “পুরো তালিকা − যা আছে”.
required - submittedআর কখন set নয়? যখন ক্রম দরকার (কে আগে লাইনে দাঁড়িয়েছেন), যখন ডুপ্লিকেট নিজেই তথ্য (একজন গ্রাহক কতবার কিনেছেন — তার জন্য গোনা দরকার, পর্ব ১৬-এ দেখা Counter ভালো), বা যখন প্রতিটি জিনিসের সাথে আরও তথ্য রাখতে হবে (তখন dict).
C-তে কোনো built-in set নেই — অনন্য মান রাখতে আর দ্রুত খুঁজতে নিজেকেই একটি hash table বানাতে হয়, যা C কোর্সের হ্যাশ টেবিল পর্বে ধাপে ধাপে দেখানো হয়েছে। Python-এর set আসলে C-তে লেখা ঠিক সেই রকম একটি hash table. C++-এ দুটি আছে: std::set (সাজানো, গাছ দিয়ে তৈরি, খোঁজা O(log n)) আর std::unordered_set (hash table, গড় O(1)) — Python-এর set দ্বিতীয়টির মতো, তাই সাজানো ক্রম পেতে sorted() লাগে।
type({})কী? —dict; খালি set বানাতেset()লিখতে হয়।removeআরdiscard-এর পার্থক্য? — উপাদান না থাকলেremoveKeyErrorদেয়,discardকিছুই করে না।- list-এর বদলে set-এ
inকেন দ্রুত? — set hash table ব্যবহার করে, তাই খোঁজা গড়ে O(1); list-কে একটি একটি করে দেখতে হয়, O(n). - set-এ list রাখা যায় না কেন? — list mutable, তাই unhashable; tuple বা frozenset রাখা যায়।
- ক্রম ঠিক রেখে list থেকে ডুপ্লিকেট দূর করবেন কীভাবে? —
list(dict.fromkeys(xs));list(set(xs))ক্রম হারায়।
১০ · AI-এর যুগে — AI-এর লেখা set-কোড যাচাই
set নিয়ে কোড লিখতে বললে AI সাধারণত কাজ চলে এমন কোড দেয়। কিন্তু কয়েকটি ভুল এতই প্রচলিত যে একটু চোখ রাখলেই ধরা যায়। এর বেশিরভাগ “চলে, কিন্তু ভুল” ধরনের — কোনো error দেয় না, তাই আরও বিপজ্জনক।
| AI প্রায়ই যা লেখে | সমস্যা | ঠিক রূপ |
|---|---|---|
print(unique_names) | প্রতিবার ভিন্ন ক্রম; টেস্ট কখনো পাস, কখনো ফেল | print(sorted(unique_names)) |
লুপে if x not in seen_list: | প্রতিটি খোঁজা O(n), পুরো লুপ O(n²) | seen = set() |
seen = {} তারপর seen.add(x) | {} একটি dict — AttributeError | seen = set() |
list(set(xs)) “ক্রম ঠিক রেখে” | ক্রম হারায়, অথচ দাবি করে উল্টো | list(dict.fromkeys(xs)) |
s.remove(x) যেখানে x না-ও থাকতে পারে | হঠাৎ KeyError | s.discard(x), বা আগে যাচাই |
s.pop() দিয়ে “এলোমেলো” বাছাই | এলোমেলোও নয়, নির্দিষ্টও নয় | random.choice(sorted(s)) |
AI-কে set-সংক্রান্ত কোড লিখতে বা রিভিউ করতে বলার সময় নিয়মগুলো আগেই বলে দিন। নিচের কাঠামোটি কপি করে নিজের কাজ বসিয়ে ব্যবহার করুন:
AI-এর লেখা set-কোড পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলো আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।
প্র ০১Python-এর set আসলে প্রায় দেড়শো বছর পুরোনো একটি গাণিতিক ধারণার প্রতিরূপ। Georg Cantor-এর set theory কী ছিল, আর গণিতের সেই ধারণা প্রোগ্রামিংয়ে এসে কোথায় বদলে গেল?
উনিশ শতকের শেষ ভাগে জার্মান গণিতবিদ Georg Cantor একটি সরল প্রশ্ন নিয়ে কাজ শুরু করেন: দুটি অসীম সংগ্রহের “আকার” কি তুলনা করা যায়? ১৮৭৪ সালে প্রকাশিত তাঁর একটি প্রবন্ধে তিনি দেখান যে সব বাস্তব সংখ্যার সংগ্রহকে স্বাভাবিক সংখ্যা (১, ২, ৩…) দিয়ে গুনে শেষ করা যায় না — অর্থাৎ অসীমেরও স্তর আছে। এই কাজ করতে গিয়ে তিনি “সেট” (জার্মানে Menge) ধারণাটিকে গণিতের কেন্দ্রে নিয়ে আসেন: কিছু স্বতন্ত্র জিনিসের একটি সংগ্রহ, যাকে একটি একক বস্তু হিসেবে ভাবা যায়। তাঁর ধারণা নিয়ে সেই সময়ের অনেক নামী গণিতবিদ তীব্র আপত্তি করেছিলেন, কিন্তু বিশ শতকের শুরুতে set theory প্রায় পুরো আধুনিক গণিতের ভিত্তি হয়ে দাঁড়ায়। ১৮৮০ সালে ব্রিটিশ যুক্তিবিদ John Venn যে বৃত্তের ছবি জনপ্রিয় করেন, স্কুলে আমরা সেটিকেই Venn diagram নামে চিনি।
Python-এর set এই গাণিতিক সংজ্ঞার দুটি মূল কথা হুবহু মানে: সদস্যতাই একমাত্র প্রশ্ন (কোনো জিনিস আছে বা নেই — দুবার থাকার মানে নেই), আর ক্রম অর্থহীন ({১, ২} আর {২, ১} একই set). union, intersection, difference, subset — নাম আর প্রতীকও গণিত থেকে নেওয়া; শুধু ∪ আর ∩ কীবোর্ডে নেই বলে | আর &, যা আবার bitwise OR আর AND-এর প্রতীক — একটি bitmask আসলে ছোট একটি set, তাই মিলটা কাকতালীয় নয়।
কিন্তু প্রোগ্রামিংয়ে এসে কয়েকটি জায়গায় ধারণাটি বদলাতে হয়েছে। প্রথমত, গণিতের set অসীম হতে পারে (সব জোড় সংখ্যা), কম্পিউটারের set সসীম — সেটি মেমরিতে রাখতে হয়। দ্বিতীয়ত, গণিতে একটি set নিজের সদস্য হতে পারে কি না, এই প্রশ্ন থেকেই বার্ট্রান্ড রাসেলের বিখ্যাত প্যারাডক্স জন্মেছিল, যা সামলাতে set theory-কে কড়া স্বতঃসিদ্ধ দিয়ে নতুন করে সাজাতে হয়। Python-এ প্রশ্নটি অন্যভাবে আটকে যায়: সাধারণ set mutable, তাই unhashable, তাই কোনো set-এর সদস্যই হতে পারে না — নিজের তো নয়ই। তৃতীয়ত, গণিতে “আছে কি?” প্রশ্নের উত্তর ধারণাগত; কম্পিউটারে এর একটি দাম আছে, আর hash table সেই দামকে গড়ে স্থির রাখার প্রকৌশল।
তবু এই বদলগুলো সত্ত্বেও গাণিতিক শিকড়টি বড় সুবিধা দেয়: স্কুলে শেখা নিয়মগুলো (De Morgan-এর সূত্র, বিনিময় ও সংযোগ নিয়ম) Python-এর set-এও খাটে। a | b == b | a, কিন্তু a - b সাধারণত b - a নয় — খাতায় যেমন, কোডেও তেমন।
মূল উপলব্ধি: ভালো প্রোগ্রামিং ধারণা প্রায়ই নতুন কিছু নয় — গণিতের পুরোনো, পরীক্ষিত একটি ধারণাকে যন্ত্রের সীমার সাথে মানিয়ে নেওয়া। গণিত জানলে কোডের নিয়ম আন্দাজ করা যায়, আর কোড লিখলে গণিত জীবন্ত হয়ে ওঠে।
প্র ০২Python-এ set প্রথম দিন থেকে ছিল না। কীভাবে এটি একটি আলাদা module থেকে built-in type হলো, আর তার আগে প্রোগ্রামাররা কী দিয়ে কাজ চালাতেন?
Python-এর প্রথম দিকের version-গুলোতে list, tuple আর dict ছিল, কিন্তু set ছিল না। অনন্য মান রাখতে বা দ্রুত “আছে কি?” যাচাই করতে প্রোগ্রামাররা একটি কৌশল ব্যবহার করতেন: dict-এর চাবিতে জিনিসগুলো রাখো, মান হিসেবে যেকোনো কিছু (সাধারণত 1 বা None) দাও। seen[x] = 1, তারপর if x in seen. কাজ চলত, কারণ dict-এর চাবি অনন্য আর hash table-এ রাখা — ঠিক যা set-এর দরকার। কিন্তু union বা intersection করতে প্রতিবার নিজে লুপ লিখতে হতো, আর কোড পড়ে বোঝা যেত না যে মানগুলো অর্থহীন।
এই অভাব মেটাতে Greg Wilson ২০০০ সালে PEP 218 প্রস্তাব করেন — “Adding a Built-In Set Object Type”. প্রস্তাবটি ধাপে ধাপে এগোয়, যা Python-এর একটি পরিচিত কৌশল: আগে একটি সাধারণ module-এ জিনিসটি দাও, মানুষ ব্যবহার করুক, নকশার ভুল ধরা পড়ুক, তারপর ভাষার মূল অংশে নাও। ২০০৩ সালে Python 2.3-এ এল sets module — Set আর ImmutableSet class, Python কোডে লেখা, dict-এর ওপর বানানো। এক বছর পর, ২০০৪ সালে Python 2.4-এ set আর frozenset হলো built-in type, এবার C-তে লেখা এবং অনেক দ্রুত। এই কাজে Raymond Hettinger-এর বড় ভূমিকা ছিল, যিনি পরে বহু বছর ধরে set-এর ভেতরের বাস্তবায়ন উন্নত করেছেন। পুরোনো sets module পরে বাতিল হয়, আর Python 3-এ পুরোপুরি সরিয়ে দেওয়া হয়।
আরও দুটি ধাপ পরে এসেছে। {1, 2, 3} ধাঁচের set literal আর set comprehension এসেছে Python 3.0-তে (পরে 2.7-এও যোগ করা হয়)। তার আগে set বানাতে সবসময় set([1, 2, 3]) লিখতে হতো। কিন্তু ততদিনে {} বহু বছর ধরে খালি dict বোঝাচ্ছে — লাখো লাইনের কোড সেটির ওপর নির্ভর করে। তাই খালি set-এর জন্য কোনো বন্ধনী-রূপ রাখা যায়নি, আর আজকের {}-এর ফাঁদের জন্ম সেখান থেকেই। একটি ভাষার ইতিহাস তার অদ্ভুত নিয়মগুলোর ব্যাখ্যা দেয়।
একটি মজার দিক: built-in set আসার পরও dict আর set-এর ভেতরের বাস্তবায়ন আলাদা পথে উন্নত হয়েছে। Python 3.6-এ dict-এর ভেতরের নকশা বদলে সেটি যোগ করার ক্রম মনে রাখতে শুরু করে, আর 3.7-এ সেটি ভাষার নিয়ম হয়। set-কে ইচ্ছে করেই সেই পথে নেওয়া হয়নি — set-এর কাজ সদস্যতা আর সেট-গণিত, সেখানে ক্রম মনে রাখা বাড়তি খরচ।
মূল উপলব্ধি: নতুন বৈশিষ্ট্য সরাসরি ভাষায় না ঢুকিয়ে আগে library-তে পরীক্ষা করা Python-এর একটি সুচিন্তিত অভ্যাস। আর একবার কোনো সিনট্যাক্স (যেমন {}) একটি অর্থ পেয়ে গেলে, পরে আর তা বদলানো যায় না — পেছনের কোড ভাঙার দাম অনেক বেশি।
প্র ০৩একই প্রোগ্রাম দুবার চালালে string-এর set কেন ভিন্ন ক্রমে ছাপে? এই “এলোমেলোতা” Python কেন ইচ্ছে করে যোগ করল — এটা কি ঝামেলা বাড়ানো নয়?
শুরুতে Python-এ string-এর hash ছিল একটি নির্দিষ্ট সূত্র: একই string, একই কম্পিউটার — প্রতিবার একই hash. তাই set-এর ক্রমও প্রতিবার একই থাকত, আর অনেকে না জেনেই সেই ক্রমের ওপর ভরসা করতেন। সমস্যা দেখা দিল নিরাপত্তায়।
২০১১ সালের শেষ দিকে নিরাপত্তা গবেষকেরা একটি জনসমক্ষ উপস্থাপনায় দেখান যে Python, PHP, Java, Ruby-সহ বহু ভাষার ওয়েব ফ্রেমওয়ার্ক একটি সাধারণ আক্রমণে কাবু হতে পারে, যার নাম পরে hash flooding. ওয়েব ফর্মের ফিল্ডগুলো সাধারণত একটি hash table-এ (Python-এ dict) রাখা হয়। hash সূত্র নির্দিষ্ট আর সবার জানা হলে আক্রমণকারী আগে থেকে হিসাব করে এমন হাজার হাজার ফিল্ডের নাম বানাতে পারেন, যেগুলো সব একই ঘরে পড়ে। তখন hash table-এর গড় O(1) খোঁজা নেমে আসে list-এর মতো একটি একটি করে দেখায় — আর একটিমাত্র অনুরোধ সার্ভারের CPU-কে দীর্ঘ সময় আটকে রাখতে পারে। খুব কম খরচে একটি সার্ভার অচল করার উপায়।
সমাধান হলো hash-এ একটি গোপন এলোমেলো বীজ মেশানো, যা প্রতিবার প্রোগ্রাম শুরু হলে নতুন করে বাছা হয়। আক্রমণকারী বীজ জানেন না, তাই কোন string কোন ঘরে পড়বে আগে থেকে হিসাব করতে পারেন না। Python প্রথমে পুরোনো version-গুলোতে (2.6.8, 2.7.3, 3.2.3) এটি ঐচ্ছিক হিসেবে যোগ করে, আর Python 3.3 থেকে এটি ডিফল্টভাবে চালু. পরে Python 3.4-এ (PEP 456) string-এর hash সূত্র বদলে SipHash নামের একটি keyed hash নেওয়া হয়, যা এই ধরনের আক্রমণ ঠেকাতেই নকশা করা। পরীক্ষা বা ডিবাগিংয়ের জন্য PYTHONHASHSEED environment variable দিয়ে বীজ স্থির করা যায়।
তাহলে কি এটা ঝামেলা বাড়ানো? একভাবে হ্যাঁ — যে কোড set-এর ক্রমের ওপর নির্ভর করত, সেগুলোর টেস্ট হঠাৎ মাঝে মাঝে ফেল করতে শুরু করে। কিন্তু সেই কোড আগে থেকেই ভুল ছিল; set কখনোই ক্রমের প্রতিশ্রুতি দেয়নি। এলোমেলোতা সেই লুকানো ভুলগুলোকে সামনে নিয়ে এসেছে। এখানে নিরাপত্তা আর সুবিধার একটি পরিষ্কার লেনদেন: “প্রতিবার একই ক্রম” নামের একটি অলিখিত সুবিধা ছেড়ে দিয়ে পুরো ইন্টারনেটের সার্ভারকে একটি সহজ আক্রমণ থেকে বাঁচানো।
মূল উপলব্ধি: যে আচরণের নিশ্চয়তা ভাষা দেয়নি, তার ওপর নির্ভর করা মানে ভবিষ্যতের একটি বাগ পুষে রাখা। আর পারফরম্যান্সের “গড়” হিসাবও নিরাপত্তার প্রশ্ন — শত্রু সবসময় সবচেয়ে খারাপ ক্ষেত্রটিই খোঁজে।
প্র ০৪কোটি কোটি URL-এর একটি set মেমরিতে রাখা সম্ভব না হলে কী করবেন? Bloom filter নামের “সম্ভাব্য set” কীভাবে কাজ করে, আর কেন সেটি কখনো কখনো ভুল “হ্যাঁ” বলে কিন্তু কখনো ভুল “না” বলে না?
Python-এর set নির্ভুল: “আছে” বললে আছে, “নেই” বললে নেই। কিন্তু এই নির্ভুলতার দাম মেমরি — প্রতিটি উপাদান নিজে মেমরিতে থাকে, সাথে hash table-এর ফাঁকা ঘর। ধরুন একটি ব্রাউজার জানতে চায় একটি ওয়েবসাইট ক্ষতিকর তালিকায় আছে কি না, আর তালিকায় কোটি কোটি URL. পুরো তালিকা প্রতিটি ফোনে রাখা অসম্ভব। আবার প্রতিটি পেজ খোলার আগে সার্ভারে জিজ্ঞেস করাও ধীর।
১৯৭০ সালে Burton H. Bloom Communications of the ACM-এ প্রকাশিত একটি প্রবন্ধে এর একটি চমৎকার সমাধান দেন, যা আজ Bloom filter নামে পরিচিত। ধারণাটি এমন: একটি লম্বা bit-এর সারি নিন, সব 0. একটি জিনিস যোগ করতে কয়েকটি আলাদা hash function দিয়ে কয়েকটি ঘরের নম্বর হিসাব করুন, আর সেই ঘরগুলো 1 করে দিন। জিনিসটি নিজে কোথাও রাখা হয় না — শুধু কয়েকটি bit. খোঁজার সময় একই hash function দিয়ে ঘরগুলো দেখুন: একটিও 0 হলে জিনিসটি নিশ্চিতভাবে নেই, কারণ থাকলে ঘরটি 1 হতো। সব 1 হলে জিনিসটি সম্ভবত আছে — কারণ অন্য কয়েকটি জিনিস মিলে কাকতালীয়ভাবে ঠিক সেই ঘরগুলো 1 করে দিয়ে থাকতে পারে।
তাই Bloom filter-এর ভুল একদিকে: কখনো কখনো ভুল “হ্যাঁ” (false positive), কিন্তু কখনো ভুল “না” নয়। আর এই ভুলের হার নিয়ন্ত্রণযোগ্য — bit-এর সারি যত লম্বা আর hash function-এর সংখ্যা যত উপযুক্ত, ভুল তত কম। প্রতিটি জিনিসের জন্য মাত্র দশটির মতো bit খরচ করে ভুলের হার প্রায় এক শতাংশে নামানো যায়, যেখানে আসল set-এ প্রতিটি URL নিজেই কয়েক ডজন byte.
ব্যবহার বিশাল। ডেটাবেস (যেমন বড় key-value store) ডিস্কে খোঁজার আগে একটি Bloom filter দেখে নেয় — “নেই” বললে ধীর ডিস্ক পড়া বাঁচে। ওয়েব ক্যাশ আর নেটওয়ার্ক যন্ত্রেও একই কৌশল। নকশাটি এমন: দ্রুত ও সস্তা প্রথম ছাঁকনি, আর “সম্ভবত আছে” বললে তবেই ধীর কিন্তু নির্ভুল যাচাই। Python-এর standard library-তে Bloom filter নেই, তবে পর্ব ১৬-এর hash ধারণা আর int-এর bitwise operator জানলে একটি সরল রূপ নিজেই বানাতে পারবেন।
মূল উপলব্ধি: সব প্রশ্নের নির্ভুল উত্তর লাগে না। “নিশ্চিতভাবে নেই” বা “সম্ভবত আছে” — এই একমুখী অনিশ্চয়তা মেনে নিলে মেমরি কয়েকশো গুণ কমানো যায়। প্রকৌশল মানে প্রায়ই সঠিক জায়গায় সঠিক পরিমাণ ভুল মেনে নেওয়া।
প্র ০৫ব্যাংক, মোবাইল অপারেটর, ভর্তি পরীক্ষা — বড় সব সিস্টেমের ডেটাবেসের ভেতরেও সেট-গণিত চলে। Edgar Codd-এর relational model আর SQL-এর UNION, INTERSECT, EXCEPT কীভাবে Python-এর set-এর আত্মীয়?
১৯৭০ সালে IBM-এর গবেষক Edgar F. Codd “A Relational Model of Data for Large Shared Data Banks” নামে একটি প্রবন্ধ প্রকাশ করেন Communications of the ACM-এ। সেই সময় ডেটাবেসে ডেটা খুঁজতে প্রোগ্রামারকে জানতে হতো ডেটা ডিস্কে কীভাবে সাজানো, কোন রেকর্ড থেকে কোন রেকর্ডে পয়েন্টার ধরে যেতে হবে। Codd-এর মূল কথা ছিল: ডেটাকে ভাবুন টেবিল হিসেবে, আর প্রতিটি টেবিল আসলে একটি সেট — সারিগুলোর সেট, যার কোনো নির্দিষ্ট ক্রম নেই আর একই সারি দুবার নেই। প্রশ্ন করা হবে গাণিতিক অপারেশন দিয়ে, যার নাম relational algebra: union, intersection, difference, আর তার সাথে নির্বাচন (select), প্রক্ষেপণ (project), যোগ (join). ডেটা কোথায় কীভাবে রাখা, সে চিন্তা ডেটাবেস নিজে করবে। এই কাজের জন্য Codd ১৯৮১ সালে Turing Award পান।
Codd-এর ধারণা থেকেই এসেছে SQL, আর তাতে সেট-গণিতের সরাসরি প্রতিরূপ: UNION (Python-এর |), INTERSECT (&), আর EXCEPT (-; Oracle-এ এর নাম MINUS). “যে গ্রাহক গত মাসে কিনেছেন কিন্তু এ মাসে কেনেননি” — SQL-এ দুটি query-র মাঝে EXCEPT, Python-এ last_month - this_month. একই চিন্তা, একই উত্তর।
তবে বাস্তব SQL পুরোপুরি “সেট” নয় — এটা একটা মজার আপস। SQL-এর টেবিলে একই সারি দুবার থাকতে পারে (গণিতের ভাষায় এটি একটি multiset বা bag), কারণ প্রতিবার ডুপ্লিকেট সরাতে সাজানো বা hash করার খরচ আছে। তাই UNION ডুপ্লিকেট সরায়, কিন্তু UNION ALL সরায় না এবং দ্রুত; SELECT DISTINCT লিখলে তবেই ডুপ্লিকেট বাদ যায়। আর টেবিলের সারির কোনো ক্রম নেই — ORDER BY না লিখলে ফলাফলের ক্রমের কোনো নিশ্চয়তা নেই। শুনতে পরিচিত লাগছে? Python-এর set-এর sorted() নিয়মটিই, ভিন্ন পোশাকে। ডেটাবেসের ভেতরে INTERSECT বা EXCEPT চালাতে ইঞ্জিন প্রায়ই একটি hash table বানায় — অর্থাৎ ঠিক আপনার Python set-এর কৌশল।
Python-এর ডেটা বিশ্লেষণেও এটি চোখে পড়বে: pandas-এ (পর্ব ৪৬) দুটি টেবিল মেলানো আসলে relational join; আর দুটি কলামের মানের তুলনায় এখনো সবচেয়ে সহজ হাতিয়ার set(df1["id"]) - set(df2["id"]).
মূল উপলব্ধি: set শুধু একটি Python type নয় — এটি তথ্য নিয়ে চিন্তা করার একটি ভাষা। যিনি “দুটোতেই”, “এটিতে কিন্তু ওটিতে না” ভাবতে শেখেন, তিনি Python-এ, SQL-এ, স্প্রেডশিটে — সবখানে একই প্রশ্ন এক লাইনে করতে পারেন।
অনুশীলন
এগুলোর বেশিরভাগে নতুন কোড লিখতে হবে না — অনুমান করা, হাতে চালানো আর মিলিয়ে নেওয়াই আসল কাজ।
-
হাতে হিসাব করুন:
a = {1, 2, 3, 4},b = {3, 4, 5}. চালানোর আগে খাতায় লিখুন: (ক)sorted(a | b); (খ)sorted(a & b); (গ)sorted(b - a); (ঘ)sorted(a ^ b); (ঙ){3, 4} <= b; (চ)len({1, 1.0, True, "1"}). তারপর REPL-এ মিলিয়ে নিন।(ক)
[1, 2, 3, 4, 5]. (খ)[3, 4]. (গ)[5]— b-তে আছে কিন্তু a-তে নেই;a - bহলে[1, 2]আসত। (ঘ)[1, 2, 5]— ঠিক একটিতে আছে এমনগুলো। (ঙ)True— 3 আর 4 দুটোই b-তে আছে। (চ)2—1,1.0,TruePython-এ সমান আর সমান hash-এর, তাই একটিই; কিন্তু string"1"সংখ্যা নয়, আলাদা উপাদান। -
ভুল খুঁজুন: একজন লিখেছেন
visited = {}, তারপর একটি লুপেvisited.add(page). আরেকজন লিখেছেনtags = set(), তারপরtags.update("python"), আর অবাক হয়েছেন যে"python" in tagsFalse. দুটি ক্ষেত্রে কী হয়েছে, আর কীভাবে ঠিক করবেন?প্রথমটিতে
{}একটি খালি dict, আর dict-এরaddmethod নেই —AttributeError. ঠিক রূপ:visited = set().দ্বিতীয়টিতে
updateতার argument-কে iterable হিসেবে ধরে প্রতিটি উপাদান যোগ করে; string-এর উপাদান হলো অক্ষর। তাই set-এ ঢুকেছে'p','y','t','h','o','n'— পুরো শব্দটি নয়। ঠিক রূপ:tags.add("python"), অথবাtags.update(["python"]). দ্বিতীয় ভুলটি বেশি বিপজ্জনক, কারণ কোনো error আসে না। -
কাজের হিসাব: একটি কোম্পানির ৫০,০০০ জন গ্রাহকের ফোন নম্বর একটি list-এ আছে। নতুন ২০,০০০টি নম্বরের প্রতিটি পুরোনো তালিকায় আছে কি না যাচাই করতে হবে। (ক) পুরোনো তালিকা list রেখে লুপে
inকরলে সবচেয়ে খারাপ ক্ষেত্রে মোটামুটি কতগুলো তুলনা? (খ) আগে set বানালে? (গ) লুপ ছাড়া এক লাইনে কীভাবে “নতুন নম্বরগুলোর মধ্যে যেগুলো আগেই আছে” বের করবেন?(ক) প্রতিটি খোঁজায় সর্বোচ্চ ৫০,০০০ তুলনা, ২০,০০০ বার — মোট প্রায় ১০০ কোটি তুলনা। (খ) set বানাতে একবার ৫০,০০০টি hash, তারপর প্রতিটি খোঁজা গড়ে প্রায় একটি ধাপ — মোট মোটামুটি ৭০,০০০ ধাপ। পার্থক্য দশ হাজার গুণেরও বেশি। (গ)
sorted(set(new_numbers) & set(old_numbers))— intersection; আর একেবারে নতুনগুলো চাইলেset(new_numbers) - set(old_numbers). শেষেsorted()যাতে রিপোর্টের ক্রম প্রতিবার একই থাকে। -
ছোট কোড লিখুন: একটি ঠিকানার list আছে, প্রতিটির শেষ অংশে জেলা —
"মিরপুর ১০, ঢাকা","খালিশপুর, খুলনা","ধানমন্ডি, ঢাকা","জিন্দাবাজার, সিলেট". আলাদা জেলাগুলো বর্ণানুক্রমে ছাপুন, আর সাথে কোন জেলা থেকে প্রথম ঠিকানা এসেছিল সেই ক্রমেও। set comprehension না জানলে সাধারণ লুপ ব্যবহার করুন।একটি উপায়:
addresses = ["মিরপুর ১০, ঢাকা", "খালিশপুর, খুলনা", "ধানমন্ডি, ঢাকা", "জিন্দাবাজার, সিলেট"] districts = [a.split(",")[-1].strip() for a in addresses] print(sorted(set(districts))) # ['খুলনা', 'ঢাকা', 'সিলেট'] print(list(dict.fromkeys(districts))) # ['ঢাকা', 'খুলনা', 'সিলেট']split(",")[-1]কমার পরের শেষ অংশ নেয়,strip()সামনের ফাঁকা জায়গা সরায় — এটি না দিলে" ঢাকা"আর"ঢাকা"আলাদা উপাদান হয়ে যেত। প্রথম লাইনের বন্ধনী-লুপটি একটি list comprehension (পর্ব ১৮)। সাধারণ লুপে লিখলেdistricts = [], তারপর প্রতিটি ঠিকানার জন্যappend. লক্ষ করুন, set কখনো সরাসরি ছাপা হয়নি।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমPython-এ x in my_set চালালে ভেতরে যে ধাপে কাজ হয়, তা সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- 🔢
hash(x)হিসাব করা - 📍 hash থেকে টেবিলের কোন ঘরে দেখতে হবে তা বের করা
- 👀 সেই ঘরে কোনো উপাদান আছে কি না দেখা
- ⚖️ উপাদান থাকলে
==দিয়ে x-এর সাথে মিলিয়ে দেখা - ✅ মিললে
True; ফাঁকা ঘর পেলেFalse
Python-এ type({}) কী ফেরত দেয়?
- <class 'set'>
- <class 'frozenset'>
- <class 'dict'>
- SyntaxError
a = {1, 2, 3} আর b = {2, 3, 4} হলে sorted(a - b) কী?
- [4]
- [1]
- [1, 4]
- [2, 3]
একটি set-এ নেই এমন একটি উপাদান s.discard(x) আর s.remove(x) দিয়ে সরাতে চাইলে কী ঘটে?
- দুটিই KeyError দেয়
- discard দেয় KeyError, remove কিছুই করে না
- দুটিই কিছু না করে চুপচাপ চলে যায়
- remove দেয় KeyError, discard কিছুই করে না
একটি বড় list-এর বদলে set-এ x in collection সাধারণত অনেক দ্রুত কেন?
- set hash থেকে সরাসরি সম্ভাব্য ঘরটি বের করে, সব উপাদান দেখে না
- set উপাদানগুলো সাজিয়ে রাখে আর binary search করে
- set উপাদানের কপি না রেখে শুধু ঠিকানা রাখে
- set-এর কোড C-তে লেখা, list-এর কোড Python-এ
Python-এ নিচের কোনটি একটি set-এর উপাদান হতে পারে?
- ["ঢাকা", "খুলনা"]
- {"ঢাকা", "খুলনা"}
- frozenset({"ঢাকা", "খুলনা"})
- {"ঢাকা": 1}
একটি list xs থেকে ডুপ্লিকেট সরিয়ে প্রথম আসার ক্রম ঠিক রাখতে কোনটি নির্ভরযোগ্য?
- list(set(xs))
- list(dict.fromkeys(xs))
- sorted(set(xs))
- xs.sort()
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- set = অনন্য মানের ক্রমহীন ব্যাগ। ডুপ্লিকেট থাকে না, index নেই। খালি set মানে
set();{}হলো খালি dict. - ডুপ্লিকেট দূর:
set(xs)(ক্রম হারায়) বাlist(dict.fromkeys(xs))(প্রথম আসার ক্রম থাকে)। না থাকলে error চাইলেremove, না চাইলেdiscard. x in sগড়ে O(1),x in listO(n). লুপের ভেতরে বারবার খুঁজলে আগে set বানান।|যেকোনোটিতে,&দুটোতেই,-এটিতে কিন্তু ওটিতে নয় (দিক আছে!),^ঠিক একটিতে,<=subset. method রূপগুলো যেকোনো iterable নেয়।- উপাদান hashable হতে হবে — list নয়, tuple বা frozenset হ্যাঁ। set-এর ক্রমে কখনো ভরসা নয়: দেখানোর আগে
sorted().
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ১৮ · comprehension — এক লাইনে list/dict/setপরবর্তী পর্বলুপ দিয়ে সংগ্রহ বানানোর কাজ এক লাইনে — set comprehension-সহ।
- Python প্রোগ্রামিং · কোর্স হোম৫৫টি পর্বপুরো কোর্সের রোডম্যাপ — ৮টি মডিউল, কোন পর্ব কখন।
- C প্রোগ্রামিং · হ্যাশ টেবিলভেতরে দেখুনset আর dict-এর ভেতরের hash table নিজের হাতে বানিয়ে দেখুন — bucket, collision, rehashing.