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

context manager — with-এর ভেতরের কথা

Context Managers: the with Statement, __enter__/__exit__, contextlib & ExitStack

Taught in Bangla, this lesson explains what the with statement guarantees: the __enter__ and __exit__ protocol as a structured try/finally, writing context managers as classes and with @contextmanager, contextlib tools like suppress, redirect_stdout, ExitStack and nullcontext, and why Python relies on with rather than __del__ for cleanup.

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

  • কেন কিছু সম্পদ “যা-ই ঘটুক” ফেরত দিতেই হয়, আর with ঠিক কোন try/finally-এর সমান
  • Context manager protocol: __enter__, __exit__-এর তিনটি argument, আর True ফেরত দেওয়ার মানে
  • class দিয়ে নিজের context manager: সময় মাপার Timer, ফোল্ডার বদলানো, rollback-সহ transaction
  • @contextmanager আর contextlib-এর হাতিয়ার — suppress, closing, redirect_stdout, ExitStack, nullcontext
  • চেনা context manager-এর (open, Lock, decimal.localcontext, sqlite3, TemporaryDirectory) আসল আচরণ, C++-এর RAII-র সাথে তুলনা, আর AI-এর লেখা কোড কীভাবে যাচাই করবেন

১ · মূল ভাবনা — যা নিয়েছেন, তা ফেরত দিতেই হবে

প্রোগ্রাম অনেক সময় এমন জিনিস ধার নেয়, যা সীমিত বা অন্যদের সাথে ভাগ করে ব্যবহার করতে হয়। এগুলোকে বলে resource বা সম্পদ। কয়েকটি উদাহরণ:

  • ফাইল: অপারেটিং সিস্টেম একটি প্রোগ্রামকে একসাথে সীমিত সংখ্যক ফাইল খুলতে দেয়। খোলা ফাইল বন্ধ না করলে লেখা ডেটা ডিস্কে পৌঁছাতে দেরি হয়, আর Windows-এ খোলা ফাইল মোছা বা সরানোও যায় না।
  • Lock: কয়েকটি thread একই ডেটা বদলালে একটি lock দিয়ে পালা ঠিক করা হয়। Lock নিয়ে ছেড়ে না দিলে বাকি সব thread চিরকাল অপেক্ষায় থাকে — প্রোগ্রাম আটকে যায়।
  • ডেটাবেস transaction: কয়েকটি বদল একসাথে হয় সবগুলো, নয়তো একটিও নয়। শেষে হয় commit (স্থায়ী করা), নয়তো rollback (সব বাতিল করে আগের অবস্থায় ফেরা)। মাঝপথে ফেলে রাখা চলে না।
  • সাময়িক বদল: কাজের জন্য অন্য ফোল্ডারে যাওয়া, দশমিকের নির্ভুলতা কিছুক্ষণের জন্য বদলানো, print-এর আউটপুট অন্য জায়গায় পাঠানো — কাজ শেষে আগের অবস্থা ফিরিয়ে আনতেই হবে।

প্রতিটির একই ছাঁচ: আগে (নেওয়া/খোলা/বদলানো) → মাঝে আসল কাজ → পরে (ফেরত/বন্ধ/আগের অবস্থা)। আর আসল বিপদ মাঝের অংশে। কাজের মাঝে একটি exception উঠলে (পর্ব ২৪) Python সাথে সাথে ফাংশন ছেড়ে বেরিয়ে যায় — নিচের “পরে” লাইনটি আর চলে না। return, break বা continue দিয়েও “পরে” অংশ এড়িয়ে যাওয়া সহজ। ফলে তৈরি হয় resource leak: সম্পদ নেওয়া হলো, ফেরত দেওয়া হলো না।

একটি আধুনিক লাইব্রেরির কথা ভাবুন, যেখানে বইয়ে RFID ট্যাগ লাগানো। আপনি বই নিয়ে পড়ার ঘরে বসলেন (আগে)। পড়লেন, নোট নিলেন (মাঝে)। এখন বেরোনোর সময় যা-ই ঘটুক — পড়া শেষ হোক, হঠাৎ ফোন এসে দৌড়ে বেরোতে হোক, বা লোডশেডিংয়ে ঘর অন্ধকার হয়ে যাক — গেটের যন্ত্র বইটি ধরে রেখে স্বয়ংক্রিয়ভাবে ফেরত নথিভুক্ত করে দেয় (পরে)। আপনাকে মনে রাখতে হয় না। with হলো সেই গেট: ব্লক থেকে যেভাবেই বেরোন, “ফেরত” অংশটি চলবেই। আর যে object জানে ঢোকার সময় কী করতে হবে আর বেরোনোর সময় কী, তাকে বলে context manager.

পুরোনো সমাধান: try/finally

পর্ব ২৪-এ দেখেছেন, finally ব্লক সবসময় চলে — সফল হলেও, exception হলেও, return হলেও। তাই with-এর আগে Python-এ সঠিক কোড ছিল এমন:

f = open("sales.csv", encoding="utf-8")   # আগে
try:
    data = f.read()                       # মাঝে — এখানে error হতে পারে
finally:
    f.close()                             # পরে — সবসময় চলবে

কোডটি ঠিক, কিন্তু তিনটি দুর্বলতা আছে। এক, প্রতিবার একই চার লাইনের কাঠামো লিখতে হয়, আর ভুলে যাওয়া সহজ। দুই, open() লাইনটি try-এর বাইরে রাখতে হয় (খোলাই ব্যর্থ হলে বন্ধ করার কিছু নেই) — এই সূক্ষ্মতা অনেকে ভুল করেন। তিন, “কীভাবে বন্ধ করতে হয়” জ্ঞানটি ব্যবহারকারীর কোডে ছড়িয়ে থাকে: ফাইলের জন্য close(), lock-এর জন্য release(), ডেটাবেসের জন্য commit()/rollback(). প্রতিটি ব্যবহারকারীকে প্রতিটির নিয়ম মনে রাখতে হয়।

with statement তৃতীয় সমস্যাটির মূলে আঘাত করে: পরিষ্কারের নিয়মটি থাকে সম্পদের নিজের ভেতরে, ব্যবহারকারীর কোডে নয়। ব্যবহারকারী শুধু বলেন “এই ব্লকের জন্য এটি নাও”, বাকিটা সম্পদ নিজে জানে।

মূল ধারণা

with একটি নিশ্চয়তা: ব্লকে ঢুকতে পারলে বেরোনোর কাজটি চলবেই — স্বাভাবিকভাবে শেষ হোক, return/break হোক, বা exception উঠুক। কোন কাজটি চলবে, তা ঠিক করে context manager নিজে।

২ · with আসলে কী করে — protocol আর try/finally পাশাপাশি

পর্ব ৩০-এ দেখেছেন, Python-এর অনেক সিনট্যাক্স আসলে কিছু dunder method ডাকে — len(x) ডাকে x.__len__(), for ডাকে __iter__. with-ও তাই। যে object-এ দুটি method আছে, সে-ই context manager. এই দুই method-এর চুক্তিকে বলে context manager protocol:

🚪 __enter__(self)

ব্লকে ঢোকার ঠিক আগে চলে। প্রস্তুতির কাজ করে (খোলা, lock নেওয়া, অবস্থা মনে রাখা)। এর ফেরত মান as-এর পরের নামে বসে। প্রায়ই self ফেরত দেয়, কিন্তু যা খুশি দিতে পারে।

__enter__

🔒 __exit__(self, exc_type, exc, tb)

ব্লক থেকে যেভাবেই বেরোনো হোক, চলে। সফল হলে তিনটি argument-ই None. exception হলে পায় তার type, exception object নিজে আর traceback. True ফেরত দিলে exception চাপা পড়ে; নইলে ওপরে যায়।

__exit__

এবার মূল প্রশ্ন: with ঠিক কোন কোডের সমান? PEP 343 নিজেই এর উত্তর দিয়েছে। একটু সরল করে, বাঁ দিকের দুই লাইন আর ডান দিকের কোড একই কাজ করে:

with Manager() as m:
    কাজ(m)

দুই লাইন। কী খুলতে হবে, কীভাবে বন্ধ করতে হবে — কিছুই লেখা নেই, কারণ সে জ্ঞান Manager-এর ভেতরে।

mgr = Manager()                  # ১. expression হিসাব
m = mgr.__enter__()              # ২. ঢোকা; ফেরত মান → m
ok = True
try:
    কাজ(m)                        # ৩. body
except BaseException as e:
    ok = False
    if not mgr.__exit__(type(e), e, e.__traceback__):
        raise                    # ৪ক. False হলে exception ওপরে
finally:
    if ok:
        mgr.__exit__(None, None, None)   # ৪খ. সফল হলে

লক্ষ করুন: __enter__ চলে try-এর বাইরে — ঢোকাই ব্যর্থ হলে __exit__ চলে না। আর __exit__ ঠিক একবারই চলে, দুই পথের যেকোনো একটিতে। (আসল নিয়মে Python method দুটি object-এর class থেকে খোঁজে, instance থেকে নয় — অন্য dunder method-এর মতোই।)

এই বিস্তারিত রূপটি থেকে চারটি নিয়ম বেরিয়ে আসে, যেগুলো মুখস্থ নয় — বুঝে রাখার মতো:

  1. as পায় __enter__-এর ফেরত মান, object নিজেকে নয়। with open(...) as f-এ f হলো file object, কারণ file-এর __enter__ self ফেরত দেয়। কিন্তু with lock as x-এ x হবে True — lock-এর __enter__ acquire()-এর ফল ফেরত দেয়।
  2. ঢোকা ব্যর্থ হলে বেরোনো নেই। ফাইলটিই না থাকলে open() নিজেই FileNotFoundError দেয়; বন্ধ করার কিছু তৈরিই হয়নি।
  3. বেরোনোর কাজ একবার, নিশ্চিতভাবে। return, break, continue — সবই finally-এর মতো __exit__ পার হয়ে যায়।
  4. exception কী হবে, তা ঠিক করে __exit__-এর ফেরত মান। সত্য-মান (True) হলে চাপা; মিথ্যা-মান (False, None, কিছুই না লেখা) হলে আবার ওঠে।

এবার একটি ছোট context manager-এর পুরো জীবন ধাপে ধাপে দেখুন — প্রথমে সফল পথে, তারপর exception-এর পথে। “পরে” চাপুন:

  1. Manager()expressionobject তৈরি
  2. __enter__()as mপ্রস্তুতি
  3. bodyকাজ(m)আসল কাজ
  4. __exit__(…)সবসময়পরিষ্কার
ধাপ ১with-এর পরের expression হিসাব
mgr = Manager()        # __init__ চলে
                       # এখনো কিছু খোলা হয়নি
ধাপ ২ঢোকা — ফেরত মান as-এ
m = mgr.__enter__()    # প্রস্তুতি: খোলা, lock নেওয়া
                       # এখান থেকে __exit__ নিশ্চিত
ধাপ ৩body সফলভাবে শেষ
কাজ(m)                 # কোনো exception নেই
# শেষ লাইন পর্যন্ত চলল
ধাপ ৪সফল পথে বেরোনো
mgr.__exit__(None, None, None)
# ফেরত মান এখানে অর্থহীন
# with-এর পরের লাইনে যায়
ধাপ ৫আবার, এবার body-তে exception
কাজ(m)
  x = 100 / 0          # ZeroDivisionError!
  # body-র বাকি লাইন বাদ
ধাপ ৬exception-এর তথ্যসহ __exit__
mgr.__exit__(ZeroDivisionError,
             ZeroDivisionError('division by zero'),
             <traceback>)
ধাপ ৭ফেরত মানই ঠিক করে পরিণতি
False / None → exception আবার ওঠে (সাধারণ)
True         → চাপা পড়ে; with-এর পরের লাইন চলে
  1. expression হিসাব। with-এর পরে যা লেখা, Python আগে সেটি হিসাব করে — এখানে Manager(), তাই __init__ চলে একটি object তৈরি হয়। বেশিরভাগ ভালো context manager এখানে কিছু খোলে না, শুধু সেটিং মনে রাখে।
  2. ঢোকা। Python ডাকে mgr.__enter__(). এটি প্রস্তুতির কাজ করে, আর যা ফেরত দেয় তা m নামে বসে। __enter__ সফলভাবে শেষ হওয়ার মুহূর্ত থেকে Python প্রতিশ্রুতি দেয়: __exit__ চলবেই।
  3. body — সফল পথ। ব্লকের ভেতরের লাইনগুলো চলে। এখানে কোনো ভুল হয়নি; শেষ লাইন পর্যন্ত চলল।
  4. সফল পথে বেরোনো। Python ডাকে __exit__(None, None, None) — তিনটি None মানে “কোনো exception নেই”. context manager পরিষ্কারের কাজ করে (বন্ধ, commit, lock ছাড়া)। তারপর প্রোগ্রাম with-এর পরের লাইনে যায়।
  5. এবার exception-এর পথ। একই ব্লক আবার, কিন্তু body-র মাঝখানে 100 / 0. Python সাথে সাথে body ছেড়ে দেয় — নিচের লাইনগুলো আর চলে না। সাধারণ কোড হলে সরাসরি ফাংশনের বাইরে চলে যেত, “পরে” অংশ বাদ পড়ত।
  6. exception-এর তথ্যসহ __exit__. কিন্তু with থাকায় বেরোনোর আগে Python ডাকে __exit__, আর এবার তিনটি argument-এ দেয় exception-এর type, exception object আর traceback. context manager চাইলে দেখে নিতে পারে কী ভুল হলো — যেমন transaction হলে rollback করবে, commit নয়।
  7. পরিণতি — ফেরত মান অনুযায়ী। __exit__ মিথ্যা-মান (False, None) ফেরত দিলে exception আবার ওঠে, ওপরের কোনো except সেটি ধরে বা প্রোগ্রাম traceback দেখায় — এটাই সাধারণ ও নিরাপদ আচরণ। True ফেরত দিলে exception মুছে যায়, যেন কিছুই হয়নি, আর with-এর পরের লাইন চলে।

True ফেরত দেওয়া কেন বিপজ্জনক: __exit__ সব ধরনের exception পায় — আপনার প্রত্যাশিত ভুল তো বটেই, টাইপের ভুল (TypeError), নামের ভুল (NameError), এমনকি ব্যবহারকারীর Ctrl+C (KeyboardInterrupt). অন্ধভাবে return True লিখলে এসবই নিঃশব্দে গিলে ফেলা হয় — পর্ব ২৪-এর swallowing exceptions, এবার আরও লুকানো জায়গায়। চাপা দেওয়া ন্যায্য শুধু তখন, যখন context manager-এর কাজই একটি নির্দিষ্ট exception সামলানো: contextlib.suppress(FileNotFoundError) (অংশ ৫), বা টেস্টে pytest.raises(ValueError) — যে context manager প্রত্যাশিত exception-টি ধরে চাপা দেয়, আর না এলে টেস্ট ব্যর্থ করে (পর্ব ৩৯)। সেখানেও পরীক্ষা করা হয় exc_type ঠিক সেই ধরনের কি না।

৩ · নিজের context manager — class দিয়ে

Protocol মাত্র দুটি method, তাই যেকোনো class-কে context manager বানানো যায়। তিনটি বাস্তব উদাহরণ দেখি, ক্রমশ জটিল।

Timer — সময় মেপে attribute-এ রাখা

একটি কোড-অংশ কত সময় নেয়, মাপতে চান। পর্ব ৩৫-এ @timer decorator পুরো ফাংশন মেপেছিল; context manager দিয়ে ফাংশনের ভেতরের যেকোনো অংশ মাপা যায়:

import time

class Timer:
    def __enter__(self):
        self.start = time.perf_counter()
        return self                        # as t → এই object
    def __exit__(self, exc_type, exc, tb):
        self.elapsed = time.perf_counter() - self.start
        return False                       # exception হলে ওপরে যাক

with Timer() as t:
    report = build_monthly_report()
# এখানে t.elapsed পড়ে লগে লিখুন বা তুলনা করুন

তিনটি নকশা-সিদ্ধান্ত লক্ষ করুন। এক, __enter__ self ফেরত দেয়, তাই as t দিয়ে ব্লকের পরেও object-টি হাতে থাকে। দুই, Timer নিজে কিছু ছাপে না; ফল রাখে t.elapsed attribute-এ — কে কীভাবে ব্যবহার করবেন (লগ, টেস্ট, গ্রাফ), সেটা ব্যবহারকারীর সিদ্ধান্ত। তিন, ব্লকে exception হলেও সময় মাপা হয়, আর exception চাপা পড়ে না। পর্ব ২৭-এর time.perf_counter() সময় মাপার সঠিক ঘড়ি।

ChangeDir — কিছুক্ষণের জন্য অন্য ফোল্ডারে

কিছু টুল (পুরোনো script, build tool) ধরে নেয় যে প্রোগ্রাম একটি নির্দিষ্ট ফোল্ডারে চলছে। কাজের জন্য সেখানে যাওয়া আর কাজ শেষে — error হলেও — আগের জায়গায় ফেরা:

import os

class ChangeDir:
    def __init__(self, target):
        self.target = target               # এখানে শুধু মনে রাখা
    def __enter__(self):
        self.old = os.getcwd()             # কোথায় ছিলাম
        os.chdir(self.target)
        return self.target
    def __exit__(self, exc_type, exc, tb):
        os.chdir(self.old)                 # যা-ই ঘটুক, ফিরে যাও

এখানে __exit__ কিছুই ফেরত দেয়নি, মানে None — মিথ্যা-মান, তাই exception ওপরে যাবে। এটাই স্বাভাবিক ধরন। মজার তথ্য: এই কাজটি এত প্রচলিত যে Python 3.11 থেকে স্ট্যান্ডার্ড লাইব্রেরিতে তৈরি contextlib.chdir আছে। তবে মনে রাখবেন, বর্তমান ফোল্ডার পুরো process-এর একটি global state — কয়েকটি thread একসাথে এটি বদলালে একে অপরের ফোল্ডার নষ্ট করবে। তাই নতুন কোডে pathlib দিয়ে পূর্ণ path ব্যবহার করাই ভালো (পর্ব ২৫); chdir রাখুন বাধ্য হলে।

Transaction — ব্যর্থ হলে সব আগের মতো

এবার সবচেয়ে গুরুত্বপূর্ণ ব্যবহার। মোবাইল wallet-এ রহিম করিমকে ৩০০ টাকা পাঠাবেন। কাজ দুই ধাপের: (১) রহিমের হিসাব থেকে কাটা, (২) করিমের হিসাবে জমা। ধাপ ১-এর পরে ধাপ ২ ব্যর্থ হলে কী হবে? টাকা রহিমের কাছ থেকে কাটা গেল, কিন্তু কারও কাছে পৌঁছাল না — হাওয়ায় মিলিয়ে গেল! ঠিক আচরণ হলো ধাপ ১-ও বাতিল করা। এই “সব, নয়তো কিছুই নয়” নিশ্চয়তাকে বলে atomicity.

আসল ডেটাবেসের বদলে একটি dict দিয়ে নকল “ডেটাবেস” বানিয়ে ব্যাপারটি দেখা যাক। __enter__ শুরুর অবস্থার একটি কপি রাখে; __exit__ exception দেখলে সেই কপি ফিরিয়ে আনে। দুটি লেনদেন চলবে — একটি সফল, একটিতে প্রাপকের নামই ভুল। চালানোর আগে শেষ লাইনের আউটপুট অনুমান করুন:

class Transaction:
    def __init__(self, db):
        self.db = db
    def __enter__(self):
        self.snapshot = dict(self.db)       # আগের অবস্থার কপি
        return self.db
    def __exit__(self, exc_type, exc, tb):
        if exc_type is not None:            # body-তে exception হয়েছে
            self.db.clear()
            self.db.update(self.snapshot)   # rollback
            print("rollback:", exc_type.__name__, exc)
        return False                        # exception চাপা নয়

accounts = {"রহিম": 1000, "করিম": 200}
for dst in ["করিম", "সুমি"]:
    try:
        with Transaction(accounts) as db:
            db["রহিম"] -= 300                # ধাপ ১: কাটা
            db[dst] += 300                  # ধাপ ২: জমা
    except KeyError:
        print("ব্যর্থ:", dst, "নামে কোনো অ্যাকাউন্ট নেই")
    print(accounts)

আউটপুট চার লাইনের। প্রথম লেনদেন সফল: {'রহিম': 700, 'করিম': 500} — __exit__(None, None, None) চলেছে, কিছুই বদলায়নি। দ্বিতীয় লেনদেনে ধাপ ১ চলে রহিম 400-তে নেমেছিল, কিন্তু ধাপ ২-এ db["সুমি"] নেই, তাই KeyError. তখন __exit__ পেল exc_type = KeyError, কপি থেকে সব ফিরিয়ে দিল আর ছাপল rollback: KeyError 'সুমি'. তারপর False ফেরত দেওয়ায় exception ওপরে গেল, বাইরের except ধরে ছাপল ব্যর্থ: সুমি নামে কোনো অ্যাকাউন্ট নেই. শেষ লাইন আবার {'রহিম': 700, 'করিম': 500} — রহিমের ৩০০ টাকা হারায়নি। দুটি কাজ আলাদা: context manager পরিষ্কার করে, except সিদ্ধান্ত নেয়. (clear() আর update() দিয়ে একই dict-এর ভেতরটা বদলানো হয়েছে, নতুন dict নয় — পর্ব ১৯-এর mutation — তাই বাইরের accounts নামটি rollback দেখতে পায়।)

এই নকল transaction আসল ডেটাবেসের তুলনায় খুব সরল: পুরো dict কপি করা বড় ডেটায় চলে না, আর কয়েকজন একসাথে লিখলে কোনো সুরক্ষা নেই। আসল ডেটাবেস (SQLite, PostgreSQL) journal বা log রেখে এই কাজ দক্ষতার সাথে করে। কিন্তু Python-এর দিক থেকে চেহারা একই — অংশ ৬-এ দেখবেন sqlite3 connection ঠিক এভাবেই with-এ commit বা rollback করে।

৪ · @contextmanager — generator দিয়ে সহজ পথ

প্রতিটি context manager-এর জন্য একটি পুরো class — __init__, __enter__, __exit__, মাঝের অবস্থা self-এ রাখা — একটু ভারী লাগে। বিশেষ করে যখন “আগে” আর “পরে” মাত্র এক-দুই লাইন। স্ট্যান্ডার্ড লাইব্রেরির contextlib module এর একটি চমৎকার সমাধান দেয়: @contextmanager decorator (পর্ব ৩৫-এর তালিকায় এর নাম দেখেছিলেন)।

ভাবনাটি পর্ব ৩৪-এর generator থেকে আসে। generator ফাংশন yield-এ থেমে যায় আর পরে ঠিক সেখান থেকে আবার চলে। context manager-এর দরকারও ঠিক এমন একটি “থামা”: আগের কাজ → থামো, ব্লক চলুক → পরের কাজ। তাই নিয়ম সরল:

  • yield-এর আগের কোড = __enter__-এর কাজ।
  • যা yield করা হয় = as-এর নামে বসে।
  • yield-এর পরের কোড = __exit__-এর কাজ — আর একে অবশ্যই finally-এর ভেতরে রাখতে হবে।
  • generator ঠিক একবার yield করবে — বেশি বা কম হলে RuntimeError.
from contextlib import contextmanager

@contextmanager
def opened(name):
    print("খোলা", name)          # __enter__-এর কাজ
    try:
        yield name               # as f ← এই মান; এখানে থেমে ব্লক চলে
    finally:
        print("বন্ধ", name)       # __exit__-এর কাজ — error হলেও

with opened("a.csv") as f:
    print("কাজ", f)
print("পরে")

finally কেন বাধ্যতামূলক? কারণ ব্লকে exception হলে @contextmanager সেটি generator-এর ভেতরে, ঠিক yield লাইনে “ছুড়ে” দেয় (generator-এর throw() method দিয়ে)। generator-এর দৃষ্টিতে মনে হয় yield লাইনটিই exception দিয়েছে। finally না থাকলে নিচের পরিষ্কারের লাইন আর চলবে না — পুরো ব্যবস্থার উদ্দেশ্যই নষ্ট। ধাপে ধাপে দেখুন:

1@contextmanager2def opened(name):3    print("খোলা", name)4    try:5        yield name6    finally:7        print("বন্ধ", name)89with opened("a.csv") as f:10    print("কাজ", f)11print("পরে")
generator-এর অবস্থা
generator
f
কে ডাকল
  1. ডাক দিলে শুধু generator তৈরি। opened("a.csv") ডাকলে body-র একটি লাইনও চলে না (পর্ব ৩৪) — তৈরি হয় একটি generator object, আর @contextmanager সেটিকে একটি context manager object-এ মুড়ে দেয়।
  2. __enter__ = প্রথম next(). with ডাকে মোড়কের __enter__, যে ভেতরে next(gen) ডাকে। generator চলতে শুরু করে: লাইন ৩ ছাপে খোলা a.csv, লাইন ৫-এ yield name-এ থামে। yield করা মান 'a.csv' হয়ে যায় f.
  3. body চলে, generator অপেক্ষায়। লাইন ১০ ছাপে কাজ a.csv. এই পুরো সময় generator yield লাইনে জমে থাকে — তার local ভ্যারিয়েবল (name) অটুট।
  4. __exit__ = দ্বিতীয় next(). body স্বাভাবিকভাবে শেষ, তাই মোড়ক আবার next(gen) ডাকে। generator yield-এর পর থেকে চলে, finally-তে ঢুকে ছাপে বন্ধ a.csv, তারপর ফাংশন শেষ — StopIteration. মোড়ক এটাই আশা করে (দ্বিতীয়বার yield করলে সে RuntimeError: generator didn't stop দিত)।
  5. with-এর পরে। লাইন ১১ ছাপে পরে. সফল পথে ক্রম: খোলা → কাজ → বন্ধ → পরে।
  6. যদি body-তে exception হতো। ধরুন লাইন ১০-এর জায়গায় raise ValueError. তখন মোড়ক gen.throw(exc) ডাকে — exception-টি generator-এর ভেতরে yield লাইনে উদয় হয়। finally ছাপে বন্ধ a.csv, তারপর exception generator থেকে বেরিয়ে যায়, মোড়ক বোঝে এটি চাপা দেওয়ার নয়, তাই ওপরে পাঠায় — traceback. generator চাইলে yield-এর চারপাশে except ValueError লিখে চাপা দিতে পারত; সেটা __exit__-এর True ফেরত দেওয়ার সমান — একই সাবধানতা লাগবে।

বাস্তব উদাহরণ: টাকার হিসাবে সাময়িক নির্ভুলতা

পর্ব ০৭-এ দেখেছেন, টাকার হিসাবে float নয়, Decimal. Decimal-এর হিসাব একটি “context” মেনে চলে — কত অঙ্ক পর্যন্ত নির্ভুলতা (prec, default ২৮), কীভাবে round করা হবে। একটি নির্দিষ্ট হিসাবের জন্য এটি বদলাতে চাইলে global সেটিং বদলানো বিপজ্জনক — অন্য সব হিসাবও বদলে যাবে, আর বদল ফেরাতে ভুলে গেলে বাগ ছড়িয়ে পড়বে। decimal.localcontext() একটি context manager, যা ব্লকের জন্য একটি কপি দেয়, আর বেরোনোর সময় আগেরটি ফিরিয়ে আনে:

from decimal import Decimal, localcontext, ROUND_HALF_UP

price = Decimal("1000") / Decimal("3")       # 333.3333… (২৮ অঙ্ক)

with localcontext(prec=6, rounding=ROUND_HALF_UP):   # Python 3.11+
    per_head = Decimal("1000") / Decimal("3")       # 333.333
# এখানে আবার ২৮ অঙ্কের সাধারণ context

একটি সূক্ষ্মতা: এই context প্রতিটি thread-এর (এবং asyncio-তে প্রতিটি task-এর) জন্য আলাদা, তাই এক জায়গার বদল অন্য thread-এ ঢোকে না। কিছু টাকা আর সেন্ট বা পয়সায় ঠিকঠাক round করতে আসলে quantize() (পর্ব ০৭) বেশি উপযুক্ত; localcontext দরকার হয় যখন পুরো একটি হিসাব-ধারা অন্য নিয়মে চালাতে চান। @contextmanager দিয়ে আপনি নিজেও এমন “সাময়িক বদল, তারপর ফেরত” manager লিখতে পারেন — একটি global সেটিং বদলানো আর finally-তে আগের মান ফেরানো।

class নাকি @contextmanager? সরল “আগে-পরে” কাজে @contextmanager ছোট আর পরিষ্কার। class ভালো যখন object-টির নিজস্ব অবস্থা বা method দরকার (যেমন Timer-এর elapsed, বা একটি connection যার আরও method আছে), যখন একই object বারবার with-এ ব্যবহার করা হবে (generator-ভিত্তিক manager একবারের জন্য), বা যখন exception-এর ধরন দেখে জটিল সিদ্ধান্ত নিতে হয়। আরেকটি সুবিধা: @contextmanager দিয়ে বানানো manager decorator হিসেবেও ব্যবহার করা যায় — @opened("x") লিখলে পুরো ফাংশন ব্লকের ভেতরে চলে।

৫ · contextlib-এর তৈরি হাতিয়ার

নিজে লেখার আগে দেখে নিন, contextlib-এ হয়তো তৈরিই আছে। সবচেয়ে কাজেরগুলো:

হাতিয়ারকী করেকোথায় কাজে লাগে
suppress(...)নির্দিষ্ট exception চাপা দেয় (3.4+)ফাইল মোছা, না থাকলেও চলবে
closing(obj)বেরোনোর সময় obj.close() ডাকেযে object-এর close() আছে কিন্তু with-সমর্থন নেই
redirect_stdout(f)print-এর আউটপুট সাময়িকভাবে f-এ পাঠায় (3.4+)টেস্টে আউটপুট ধরা
ExitStack()চলার সময় যত খুশি manager যোগ; উল্টো ক্রমে বন্ধ (3.3+)সংখ্যা আগে জানা নেই এমন কয়েকটি ফাইল
nullcontext(x)কিছুই করে না; as-এ x দেয় (3.7+)“কখনো lock লাগবে, কখনো লাগবে না”
chdir(path)অংশ ৩-এর ChangeDir, তৈরি (3.11+)বাধ্য হলে সাময়িক ফোল্ডার বদল

suppress — এবং কেন সাবধান থাকবেন

import os
from contextlib import suppress

with suppress(FileNotFoundError):     # ফাইল না থাকলেও সমস্যা নেই
    os.remove("old_report.tmp")

# সমান: try: os.remove(...)  except FileNotFoundError: pass

এখানে suppress ঠিক ব্যবহার: একটি নির্দিষ্ট, প্রত্যাশিত exception, আর ব্লকে মাত্র একটি লাইন। দুটি ভুল ব্যবহার থেকে সাবধান। এক, suppress(Exception) — এটি প্রায় সবকিছু গিলে ফেলে, বানান ভুল থেকে শুরু করে অনুমতির সমস্যা পর্যন্ত। দুই, ব্লকে অনেক লাইন রাখা — exception হলে ব্লকের বাকি সব লাইন বাদ পড়ে, আর কোনো খবর থাকে না কোন লাইন পর্যন্ত চলেছিল। suppress-এর ব্লক হোক এক লাইনের, আর exception হোক যতটা সম্ভব নির্দিষ্ট।

closing আর nullcontext

কিছু পুরোনো বা তৃতীয় পক্ষের object-এ close() আছে কিন্তু __enter__/__exit__ নেই। with closing(obj) as o: তাদের জন্য বেরোনোর সময় close() ডাকে। আর nullcontext একটি “ফাঁকা” manager — কোডের একই কাঠামো রাখতে, যখন কোনো শর্তে সত্যিকারের manager দরকার নেই:

from contextlib import nullcontext

def save(data, lock=None):
    with lock if lock is not None else nullcontext():
        write_to_disk(data)          # lock থাকলে পাহারায়, না থাকলে সরাসরি

redirect_stdout — print ধরা, টেস্টের জন্য দারুণ

ধরুন একটি পুরোনো ফাংশন রসিদ print করে, আর আপনি টেস্ট লিখতে চান যে রসিদে ঠিক কী লেখা হলো। stdout-কে কিছুক্ষণের জন্য একটি io.StringIO-তে পাঠানো যায় — একটি নকল ফাইল, যা লেখাগুলো মেমরিতে জমায়। ব্লক শেষে print আবার আগের মতো স্ক্রিনে যায়। (pytest-এর capsys একই কাজ আরও সুবিধাজনকভাবে করে — পর্ব ৩৯।)

ExitStack — কয়টি resource, আগে জানা নেই

with open("a.csv") as a, open("b.csv") as b: চলে যখন ফাইলের সংখ্যা কোড লেখার সময়ই জানা। কিন্তু ব্যবহারকারী যদি ৩টি বা ৩০টি জেলার CSV দেন? লুপের ভেতরে with দিলে প্রতিটি ফাইল পরের ফাইল খোলার আগেই বন্ধ হয়ে যাবে — অথচ সবগুলো একসাথে খোলা দরকার (যেমন পাশাপাশি মিলিয়ে পড়তে)। ExitStack হলো একটি “context manager-এর স্তূপ”: stack.enter_context(cm) প্রতিটি manager-এ ঢোকে আর তার __exit__ স্তূপে তুলে রাখে। ExitStack-এর ব্লক শেষ হলে স্তূপের ওপর থেকে — অর্থাৎ খোলার উল্টো ক্রমে — সবগুলো বন্ধ হয়।

1with ExitStack() as stack:2    files = [stack.enter_context(open(p))3             for p in paths]4    merge(files)5# ব্লক শেষ — স্তূপ খালি হচ্ছে
স্তূপ (নিচ → ওপর)
স্তূপ
খোলা ফাইল
এইমাত্র
  1. খালি স্তূপ। ExitStack() নিজেই একটি context manager. ঢোকার সময় সে শুধু একটি খালি তালিকা তৈরি করে — এখনো কিছু খোলা হয়নি।
  2. প্রথম ফাইল। stack.enter_context(open("dhaka.csv")) ফাইলটির __enter__ ডাকে, ফেরত মান (file object) list-এ যায়, আর ফাইলটির __exit__ স্তূপে ওঠে।
  3. দ্বিতীয় ফাইল। একই কাজ khulna.csv-এর জন্য। স্তূপে এখন দুটি। ধরুন এই মুহূর্তে খোলা ব্যর্থ হতো (ফাইল নেই) — তাহলে ExitStack সঙ্গে সঙ্গে স্তূপে যা আছে (dhaka) বন্ধ করে exception ওপরে পাঠাত। আধা-খোলা অবস্থা থাকে না।
  4. তৃতীয় ফাইল। sylhet.csv. paths-এ ৩০টি থাকলেও কোড একই — এটাই ExitStack-এর শক্তি।
  5. আসল কাজ। তিনটি ফাইল একসাথে খোলা, merge(files) পাশাপাশি পড়ছে। এখানে exception হলেও পরের ধাপগুলো একইভাবে ঘটবে।
  6. শেষ — ওপর থেকে বন্ধ শুরু। ব্লক শেষ, তাই ExitStack.__exit__ স্তূপের সবচেয়ে ওপরেরটি আগে নেয়: sylhet বন্ধ। কেন উল্টো ক্রম? কারণ পরে খোলা জিনিস প্রায়ই আগে খোলা জিনিসের ওপর নির্ভর করে (যেমন connection-এর ওপর cursor) — নির্ভরশীলটি আগে বন্ধ হওয়া উচিত।
  7. তারপর khulna. একটির __exit__ নিজেই exception দিলেও ExitStack বাকিগুলো বন্ধ করা থামায় না — ঠিক যেন নেস্ট করা with.
  8. সবশেষে dhaka — স্তূপ খালি। প্রথমে খোলা ফাইল সবশেষে বন্ধ হলো। পুরো আচরণ হুবহু তিনটি নেস্ট করা with-এর মতো, শুধু সংখ্যাটি চলার সময় ঠিক হয়েছে।

এবার দ্বিতীয় runnable প্রোগ্রাম, যেখানে দুটি হাতিয়ার একসাথে: redirect_stdout দিয়ে একটি ফাংশনের print ধরা, আর ExitStack দিয়ে নিজের লেখা @contextmanager manager তিনবার খোলা। আসল ফাইলের বদলে manager-টি শুধু একটি তালিকায় লিখে রাখে কখন খোলা আর কখন বন্ধ হলো — যাতে ক্রমটি চোখে দেখা যায়:

import io
from contextlib import contextmanager, redirect_stdout, ExitStack
@contextmanager
def opened(name, log):
    log.append(f"খোলা {name}")           # __enter__-এর কাজ
    try:
        yield name.upper()
    finally:
        log.append(f"বন্ধ {name}")       # __exit__-এর কাজ

def receipt(total):
    print("মোট:", total, "টাকা")
buf, log = io.StringIO(), []
with redirect_stdout(buf):
    receipt(550)                         # স্ক্রিনে কিছু আসবে না
with ExitStack() as stack:
    files = [stack.enter_context(opened(n, log)) for n in ("a.csv", "b.csv", "c.csv")]
    log.append(f"কাজ {files}")
print(*log, sep="\n")
print("ধরা লেখা:", repr(buf.getvalue()))

আউটপুট আট লাইনের: খোলা a.csv, খোলা b.csv, খোলা c.csv, তারপর কাজ ['A.CSV', 'B.CSV', 'C.CSV'], তারপর বন্ধ c.csv, বন্ধ b.csv, বন্ধ a.csv — খোলার উল্টো ক্রমে বন্ধ। files-এ আছে প্রতিটি generator-এর yield করা মান (বড় হাতের নাম), কারণ enter_context ফেরত দেয় __enter__-এর ফল। শেষ লাইন ধরা লেখা: 'মোট: 550 টাকা\n' — receipt(550)-এর print স্ক্রিনে যায়নি, জমা হয়েছে buf-এ। টেস্টে এখন লেখা যায় assert buf.getvalue() == "মোট: 550 টাকা\n". ব্লক শেষে redirect_stdout stdout ফিরিয়ে দিয়েছে বলেই শেষ দুটি print আবার স্ক্রিনে এসেছে।

redirect_stdout process-জুড়ে বদল: এটি আসলে sys.stdout নামের একটি global বদলায়। তাই একই সময়ে অন্য thread যা print করে, সেটিও আপনার buf-এ চলে যায়। লাইব্রেরি কোডে বা thread-ওয়ালা প্রোগ্রামে এটি ব্যবহার করবেন না; ভালো নকশা হলো ফাংশনকে একটি file argument দেওয়া (print(..., file=out)). টেস্ট আর ছোট script-এ এটি নিরাপদ ও দারুণ কাজের।

৬ · একসাথে কয়েকটি manager, আর যেগুলো আপনি আগেই ব্যবহার করছেন

এক with-এ কয়েকটি

একটি ফাইল পড়ে আরেকটিতে লিখতে নেস্ট করা দুটি with লাগে না — কমা দিয়ে এক লাইনে লেখা যায়, আর এটি হুবহু নেস্ট করার সমান: বাঁ থেকে ডানে ঢোকা, ডান থেকে বাঁয়ে বেরোনো। লম্বা হলে Python 3.10 থেকে বন্ধনী দিয়ে কয়েক লাইনে ভাঙা যায়:

with open("in.csv", encoding="utf-8") as src, open("out.csv", "w", encoding="utf-8") as dst:
    dst.write(src.read())

with (                                          # Python 3.10+
    open("in.csv", encoding="utf-8") as src,
    open("out.csv", "w", encoding="utf-8") as dst,
):
    dst.write(src.read())

দ্বিতীয় open ব্যর্থ হলে (যেমন লেখার অনুমতি নেই) প্রথম ফাইলটি ঠিকই বন্ধ হয় — কারণ প্রথমটির __enter__ ইতিমধ্যে সফল হয়েছিল।

চেনা context manager — ভেতরে কী হয়

Managerঢোকার সময়বেরোনোর সময়
open(...)ফাইল খোলা (খোলা হয় আসলে open() ডাকার সময়ই); as পায় file objectclose() — buffer ডিস্কে পাঠিয়ে বন্ধ (পর্ব ২৫)
threading.Lock()acquire() — অন্য thread ধরে রাখলে অপেক্ষাrelease() — exception হলেও (পর্ব ৪১)
decimal.localcontext()context-এর কপি চালুআগের context ফেরত
sqlite3 connectionকিছু না; as পায় connectionসফল হলে commit(), exception হলে rollback() — বন্ধ করে না!
tempfile.TemporaryDirectory()নতুন অস্থায়ী ফোল্ডার; as পায় তার path (string)ফোল্ডার আর ভেতরের সবকিছু মুছে ফেলা
unittest.mock.patch(...)একটি নাম সাময়িকভাবে নকল object-এ বদলানোআসল ফেরত (পর্ব ৩৯)

sqlite3-এর বিখ্যাত চমক — commit করে, বন্ধ করে না

অনেকেই ধরে নেন with sqlite3.connect("bank.db") as conn: শেষে connection বন্ধ হয়, যেমন ফাইলে হয়। হয় না। SQLite connection-এর __exit__ শুধু transaction শেষ করে: ব্লক সফল হলে commit, exception হলে rollback. connection খোলাই থাকে — পরের with conn:-এ আবার ব্যবহার করা যায়। এটি নকশা অনুযায়ীই; Python-এর ডকুমেন্টেশনে স্পষ্ট লেখা আছে। কিন্তু প্রত্যাশার উল্টো বলে এটি একটি পরিচিত ফাঁদ: লুপে বারবার sqlite3.connect() ডেকে with-এ রাখলে একগাদা খোলা connection জমতে পারে।

import sqlite3
from contextlib import closing

with closing(sqlite3.connect("bank.db")) as conn:   # বাইরের: বন্ধ করা
    with conn:                                        # ভেতরের: transaction
        conn.execute("UPDATE acc SET bal = bal - 300 WHERE name = ?", ("রহিম",))
        conn.execute("UPDATE acc SET bal = bal + 300 WHERE name = ?", ("করিম",))
    # এখানে commit হয়ে গেছে (বা exception হলে rollback)
# এখানে connection বন্ধ

দুই স্তরের এই কাঠামোটি মনে রাখুন: বাইরের closing সম্পদের জীবন সামলায়, ভেতরের with conn একেকটি transaction. অংশ ৩-এর নকল Transaction যা করেছিল, এখানে আসল ডেটাবেস তা-ই করছে — দ্বিতীয় UPDATE ব্যর্থ হলে প্রথমটিও বাতিল। (? চিহ্ন হলো পর্ব ২৭-এর parameterized query, SQL injection থেকে বাঁচতে। Python 3.12-এ connection-এ নতুন autocommit সেটিং এসেছে, যা transaction-এর আচরণ আরও স্পষ্টভাবে ঠিক করতে দেয়।)

TemporaryDirectory আর async with-এর ঝলক

টেস্ট বা একবারের প্রক্রিয়াকরণের জন্য অস্থায়ী ফোল্ডার দরকার হলে with tempfile.TemporaryDirectory() as tmp: — ব্লক শেষে ফোল্ডারটি ভেতরের সব ফাইলসহ মুছে যায়, কোনো আবর্জনা পড়ে থাকে না। আর পর্ব ৪২-এ asyncio শিখলে দেখবেন async with — একই ধারণা, কিন্তু ঢোকা আর বেরোনোর কাজ নিজেই অপেক্ষা করতে পারে (যেমন নেটওয়ার্ক connection বন্ধ করতে সময় লাগে)। সেখানে method দুটির নাম __aenter__ আর __aexit__, আর contextlib-এ আছে @asynccontextmanager ও AsyncExitStack.

৭ · C++-এর RAII বনাম Python-এর with — কেন __del__-এ ভরসা নয়

C++-এ এই সমস্যার সমাধান আলাদা, আর খুব মার্জিত। একটি object scope থেকে বেরোলে — স্বাভাবিকভাবে বা exception-এ — তার destructor নিশ্চিতভাবে, ঠিক সেই মুহূর্তে চলে। তাই সম্পদকে একটি object-এর জীবনের সাথে বেঁধে দেওয়া হয়: constructor-এ নেওয়া, destructor-এ ফেরত। এই নকশার নাম RAII (Resource Acquisition Is Initialization). এর ফলে C++-এ “বন্ধ করো” লিখতেই হয় না; ফাইল-object scope ছাড়লেই বন্ধ।

Python-এ কি একই কাজ __del__ দিয়ে করা যায়? __del__ হলো সেই method, যা object মেমরি থেকে মোছার আগে চলে। শুনতে destructor-এর মতো, কিন্তু একটি মৌলিক পার্থক্য আছে: Python কখনো প্রতিশ্রুতি দেয় না object ঠিক কখন মুছবে. কারণগুলো:

  • CPython-এ reference counting প্রায় সঙ্গে সঙ্গে মোছে — শেষ নামটি চলে গেলে। কিন্তু এটি CPython-এর বাস্তবায়নের খুঁটিনাটি, ভাষার নিয়ম নয়। PyPy-র মতো অন্য Python-এ garbage collector চলে নিজের সময়ে — হয়তো অনেক পরে।
  • Reference cycle: দুটি object একে অপরকে দেখালে count শূন্য হয় না; cycle-খোঁজা collector কখন চলবে, তা অনিশ্চিত।
  • একটি লুকানো নাম যথেষ্ট: traceback object, একটি cache, বা ডিবাগারের একটি রেফারেন্স object-টিকে জীবিত রাখতে পারে — আপনি টেরও পাবেন না।
  • Interpreter বন্ধের সময়: প্রোগ্রাম শেষের সময় module-গুলো ভাঙা হতে থাকে; __del__ হয়তো চলবেই না, বা চললেও যে module ব্যবহার করতে চায় সেটি আর নেই।
  • __del__-এর exception হারিয়ে যায়: সেখানে error হলে Python শুধু stderr-এ “Exception ignored in…” লেখে; ডাকার কেউ নেই যে ধরবে।

তাই Python-এর পথ হলো deterministic cleanup স্পষ্টভাবে চাওয়া: with. RAII-তে পরিষ্কার নিশ্চিত করে object-এর জীবনকাল; Python-এ নিশ্চিত করে একটি ব্লক. পার্থক্যটি দৃশ্যমানতায়ও: C++-এ ধ্বংস হয় নিঃশব্দে, scope-এর শেষ বন্ধনীতে; Python-এ with লাইনটি চোখের সামনে বলে দেয় “এখানে একটি সম্পদ নেওয়া হলো, ব্লক শেষে ফেরত যাবে”. খোলা ফাইল with ছাড়া হারিয়ে গেলে Python একটি ResourceWarning দিতে পারে (default-এ লুকানো; python -W default বা development mode-এ দেখা যায়) — এটি ভুলের সংকেত, সমাধান নয়। আর একান্তই object-এর সাথে কোনো শেষ-পরিষ্কার বাঁধতে হলে __del__-এর চেয়ে weakref.finalize নিরাপদ বিকল্প।

C/C++ জানলে

C-তে কোনো স্বয়ংক্রিয় ব্যবস্থা নেই — প্রতিটি fopen-এর পরে প্রতিটি পথে fclose নিজে লিখতে হয়, তাই C প্রোগ্রামে goto cleanup ধাঁচ এত প্রচলিত। C++-এ RAII আর smart pointer (std::unique_ptr, std::lock_guard) destructor দিয়ে নিশ্চিত পরিষ্কার করে, কোনো বিশেষ সিনট্যাক্স ছাড়াই (C++-এর RAII ও smart pointer). Python-এর with সেই নিশ্চয়তাই দেয়, কিন্তু scope নয়, একটি স্পষ্ট ব্লক দিয়ে — std::lock_guard-এর Python রূপ হলো with lock:. পার্থক্য: C++ object ব্লকের পরে আর থাকে না, Python-এ with-এর পরেও নামটি থাকে (বন্ধ ফাইল হিসেবে)।

পরীক্ষা ও ইন্টারভিউতে
  • Context manager কী? — যে object-এ __enter__ আর __exit__ আছে; with ঢোকার সময় প্রথমটি, বেরোনোর সময় (exception হলেও) দ্বিতীয়টি ডাকে।
  • __exit__-এর তিনটি argument কী, আর True ফেরত দিলে কী হয়? — exception-এর type, object, traceback (সফল হলে তিনটিই None); True হলে exception চাপা পড়ে।
  • with কোন কোডের সমান? — __enter__ ডেকে, body-কে try-এ রেখে, __exit__-কে finally-ধাঁচে নিশ্চিতভাবে ডাকা।
  • @contextmanager কীভাবে কাজ করে? — generator-এর yield-এর আগের অংশ enter, পরের অংশ exit; body-র exception yield-এ ছুড়ে দেওয়া হয়, তাই পরিষ্কার রাখতে হয় finally-তে।
  • with sqlite3.connect(...) কি connection বন্ধ করে? — না; commit বা rollback করে। বন্ধ করতে closing() বা conn.close().

৮ · AI-এর যুগে — AI-এর লেখা পরিষ্কারের কোড যাচাই করা

সম্পদ-সংক্রান্ত বাগের বৈশিষ্ট্য হলো, ছোট পরীক্ষায় এরা দেখা দেয় না। একটি ফাইল বন্ধ না করলে আপনার ল্যাপটপে কিছুই হয় না; সার্ভারে দশ হাজার অনুরোধের পরে আসে “Too many open files”. একটি lock ছাড়তে ভুলে গেলে প্রোগ্রাম আটকে যায় শুধু তখন, যখন দুটি thread একসাথে আসে — হয়তো মাসে একবার। AI-এর লেখা কোড প্রথম পরীক্ষায় চলে, তাই এই ভুলগুলো চোখ এড়িয়ে যায়। বারবার দেখা যায় এমন ভুল:

AI প্রায়ই যা করেসমস্যাকী চাইবেন
f = open(...), পরে f.close(), মাঝে with নেইমাঝে exception বা return হলে ফাইল খোলা থাকেপ্রতিটি ফাইলে with
lock.acquire() … lock.release()মাঝে exception হলে lock চিরকাল আটকে — বাকি thread-গুলো অনন্ত অপেক্ষায়with lock:
__exit__-এ সবসময় return Trueসব exception, এমনকি বাগ আর Ctrl+C, নিঃশব্দে গায়েবFalse/None; চাপা দিতে হলে নির্দিষ্ট type পরীক্ষা করে
@contextmanager-এ yield-এর চারপাশে try/finally নেইbody-তে error হলে পরিষ্কারের কোড চলে নাtry: yield ... finally: পরিষ্কার
পরিষ্কারের কাজ __del__-একখন চলবে অনিশ্চিত; interpreter বন্ধের সময় হয়তো চলবেই নাwith-সমর্থন (__enter__/__exit__), দরকারে close() method
with suppress(Exception): আর ভেতরে অনেক লাইনযেকোনো ভুলে বাকি লাইন নিঃশব্দে বাদনির্দিষ্ট exception, এক লাইনের ব্লক
with sqlite3.connect(...) ধরে নেয় বন্ধ হবেconnection জমতে থাকেclosing() দিয়ে বাইরের স্তর

AI-কে সম্পদ-সংক্রান্ত কোড লিখতে বলার সময় শর্তগুলো আগেই দিয়ে দিন। নিচের কাঠামোটি কপি করে নিজের কাজ বসান:

আমি Python 3.12+ ব্যবহার করি। একটি ছোট মোবাইল wallet সার্ভিসের জন্য কোড লিখে দাও: (ক) একটি Transaction context manager (class দিয়ে), যা একটি sqlite3 connection নেবে — ব্লক সফল হলে commit, যেকোনো exception হলে rollback, আর exception অবশ্যই ওপরে পাঠাবে; (খ) একটি transfer(conn, src, dst, amount) ফাংশন, যা দুই ধাপের লেনদেন এই Transaction-এর ভেতরে করবে; (গ) কয়েকটি জেলার CSV ফাইল একসাথে খুলে মেলানোর একটি ফাংশন, ExitStack দিয়ে। শর্ত: ১. প্রতিটি ফাইল, lock আর connection with দিয়ে সামলাবে; কোথাও শুধু হাতে close()/release() নয়। ২. __exit__ কখনো অন্ধভাবে True ফেরত দেবে না। কোথাও exception চাপা দিলে কোন type আর কেন, comment-এ লেখো। ৩. @contextmanager ব্যবহার করলে yield-এর চারপাশে try/finally থাকবে। ৪. __del__ দিয়ে কোনো পরিষ্কারের কাজ নয়। ৫. sqlite3 connection শেষে বন্ধ হবে (with conn শুধু transaction শেষ করে — এটা মাথায় রাখো)। ৬. একটি টেস্ট দেখাও, যেখানে দ্বিতীয় ধাপ ব্যর্থ হলে দুই অ্যাকাউন্টের ব্যালান্স আগের মতোই থাকে।

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

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

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

প্র ০১with কীভাবে Python-এ এল? PEP 343-এর আগে PEP 310 আর PEP 340-এ কী প্রস্তাব ছিল, আর কেন সেগুলো বাতিল হলো?

২০০০-এর দশকের শুরুতে Python-এ সম্পদ সামলানোর একমাত্র নির্ভরযোগ্য উপায় ছিল try/finally. এর সাথে আরেকটি বিরক্তিকর সীমা ছিল: Python 2.5-এর আগে একই try-তে except আর finally একসাথে লেখা যেত না — দুটি নেস্ট করা try লাগত। প্রতিটি lock আর ফাইলের জন্য এই কাঠামো লিখতে লিখতে মানুষ ক্লান্ত।

প্রথম গুরুত্বপূর্ণ প্রস্তাব PEP 310 — “Reliable Acquisition/Release Pairs”, লিখেছিলেন Michael Hudson আর Paul Moore. এতে একটি with ব্লক প্রস্তাব করা হয়, যা ঢোকার সময় object-এর __enter__ আর বেরোনোর সময় __exit__ ডাকবে। ধারণাটি ভালো ছিল, কিন্তু সীমিত: __exit__ জানতে পারত না ব্লকে exception হয়েছিল কি না, তাই transaction-এর মতো কাজ (সফলে commit, ব্যর্থতায় rollback) করা যেত না।

তারপর ২০০৫ সালে Guido van Rossum নিজে লেখেন একটি অনেক উচ্চাভিলাষী প্রস্তাব — PEP 340 — “Anonymous Block Statements”. এর ভাবনা ছিল generator দিয়ে একটি সাধারণ “block” কাঠামো বানানো, যা Ruby-র block-এর মতো লুপও হতে পারত, সম্পদ-পরিচালনাও করতে পারত। কিন্তু python-dev আলোচনায় দেখা গেল, একই কাঠামো লুপ আর সম্পদ — দুই কাজ করলে অদ্ভুত প্রশ্ন ওঠে: ব্লকের ভেতরের break কি সম্পদ-ব্লক থেকে বেরোবে, নাকি বাইরের লুপ থেকে? কোড পড়ে বোঝা কঠিন হয়ে যেত কোনটি লুপ, কোনটি নয়। Guido শেষ পর্যন্ত প্রস্তাবটি প্রত্যাহার করেন।

এই আলোচনা থেকে জন্ম নেয় PEP 343 — “The 'with' Statement”, Guido van Rossum আর Nick Coghlan-এর লেখা। এটি PEP 310-এর সরলতা আর PEP 340-এর শক্তির মাঝামাঝি: লুপ নয়, শুধু একবার চলা ব্লক; কিন্তু __exit__ পায় exception-এর তিনটি তথ্য, আর চাইলে চাপা দিতে পারে। সঙ্গে generator থেকে context manager বানানোর ব্যবস্থা — যা পরে contextlib.contextmanager হলো। Nick Coghlan-এর আরেকটি প্রস্তাব (PEP 346) থেকেও অনেক ভাবনা এতে মিশেছিল। একটি কৌতূহলী তথ্য: প্রথম খসড়ায় একটি বাড়তি __context__() method ছিল, যা প্রকাশের আগেই সরল করতে বাদ দেওয়া হয়।

with প্রকাশিত হয় Python 2.5-এ (২০০৬)। নতুন কিওয়ার্ড বলে প্রথমে from __future__ import with_statement লিখে চালু করতে হতো; Python 2.6-এ এটি স্থায়ী কিওয়ার্ড হয়। একই রিলিজে PEP 341 try/except/finally একসাথে লেখার সুযোগ দেয়, আর PEP 342 generator-এ send(), throw(), close() যোগ করে — যা ছাড়া @contextmanager সম্ভব হতো না।

মূল উপলব্ধি: with একটি দীর্ঘ নকশা-বিতর্কের ফল — প্রথম প্রস্তাব কম পারত, দ্বিতীয়টি অতিরিক্ত পারত। শেষ নকশা একটি কাজ (সম্পদের আগে-পরে) পরিষ্কারভাবে করে, আর ঠিক যতটুকু শক্তি দরকার — exception দেখার ক্ষমতা — ততটুকুই দেয়।

প্র ০২C++-এর RAII আর Python-এর with — একই সমস্যার দুটি সমাধান। কোনটির শক্তি কোথায়, আর অন্য ভাষাগুলো কোন পথ নিয়েছে?

RAII ধারণাটি দাঁড় করান C++-এর স্রষ্টা Bjarne Stroustrup, ১৯৮০-এর দশকে C++-এ exception আর destructor-এর নকশার সময়। নামটি — “Resource Acquisition Is Initialization” — নিজেই স্বীকৃতভাবে অদ্ভুত; Stroustrup পরে বলেছেন নামটি খুব ভালো হয়নি। কিন্তু ভাবনাটি গভীর: সম্পদের মালিক একটি object; object তৈরি হওয়া মানে সম্পদ নেওয়া, object ধ্বংস হওয়া মানে সম্পদ ফেরত। C++-এ local object-এর ধ্বংস নিশ্চিত আর নির্দিষ্ট মুহূর্তে — scope-এর শেষে, exception-এ stack unwinding-এর সময়ও। তাই std::lock_guard, std::unique_ptr, std::fstream — কোনোটির জন্যই “বন্ধ করো” লিখতে হয় না।

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

Python এই পথ নিতে পারে না, কারণ Python-এ object-এর জীবনকাল নিশ্চিত নয় — garbage collector কখন মুছবে, ভাষা তা বলে না (প্রশ্ন ৩)। তাই Python নিশ্চয়তা নিয়েছে একটি স্পষ্ট ব্লক-এ বেঁধে। দুর্বলতা: with লিখতে ভুলে যাওয়া সম্ভব — linter ধরতে পারে, ভাষা নয়। শক্তি: কোড পড়েই দেখা যায় সম্পদ কোথায় নেওয়া আর কোথায় ফেরত হলো; আর একই object-কে বারবার with-এ ঢোকানো যায় (যেমন একটি lock বা sqlite connection), যা object-এর জীবনকালের সাথে বাঁধা RAII-তে আলাদা নকশা চায়।

অন্য garbage-collected ভাষাগুলোও প্রায় একই সিদ্ধান্তে পৌঁছেছে। C#-এ প্রথম থেকেই using ব্লক আর IDisposable interface — Python-এর with-এর খুব কাছের আত্মীয়। Java দীর্ঘদিন শুধু try/finally রেখেছিল, তারপর Java 7 (২০১১)-এ এল try-with-resources আর AutoCloseable. Go নিয়েছে আরেক পথ: defer f.Close() — ফাংশন শেষে চলবে এমন একটি ডাক নথিভুক্ত করা, অনেকটা ExitStack-এর callback-এর মতো। Rust আবার RAII-র পথে — তার ownership ব্যবস্থা compile-এর সময়ই নিশ্চিত করে কখন একটি মান মুছবে, তাই Drop trait নির্ভরযোগ্য destructor.

মজার ব্যাপার, Python-এর with-এ এমন একটি ক্ষমতা আছে যা সাধারণ destructor-এ নেই: __exit__ জানে exception হয়েছিল কি না। তাই transaction-এর “সফলে commit, ব্যর্থতায় rollback” সরাসরি লেখা যায়। C++-এ destructor-এ একই সিদ্ধান্ত নিতে std::uncaught_exceptions()-এর মতো কৌশল লাগে।

মূল উপলব্ধি: নিশ্চিত পরিষ্কারের জন্য একটি নিশ্চিত মুহূর্ত দরকার। C++ ও Rust সেই মুহূর্ত পায় object-এর জীবনকাল থেকে; Python, C#, Java পায় একটি স্পষ্ট ব্লক থেকে। কৌশল আলাদা, লক্ষ্য একই — সম্পদের মালিকানা যেন কখনো অস্পষ্ট না থাকে।

প্র ০৩__del__ দেখতে destructor-এর মতো, তবু কেন পরিষ্কারের কাজে একে অবিশ্বস্ত বলা হয়? reference cycle, PEP 442 আর interpreter বন্ধের সময়ের সমস্যা কী?

CPython-এ প্রতিটি object জানে কয়টি নাম বা রেফারেন্স তাকে দেখাচ্ছে (পর্ব ০৫-এর reference counting). সংখ্যা শূন্য হলে object সঙ্গে সঙ্গে মোছা হয়, আর মোছার আগে __del__ চলে। তাই CPython-এ প্রায়ই মনে হয় __del__ ঠিক সময়ে চলছে — f = open(...) লিখে নামটি সরালে ফাইল বন্ধ হয়ে যায়। কিন্তু এই আচরণ CPython-এর বাস্তবায়ন, ভাষার প্রতিশ্রুতি নয়। Python language reference স্পষ্টভাবে বলে, object কখন মুছবে বা আদৌ মুছবে কি না, তা নিশ্চিত নয়।

প্রথম সমস্যা অন্য বাস্তবায়ন. PyPy reference counting ব্যবহার করে না; তার garbage collector চলে মেমরির চাপ অনুযায়ী। CPython-এ ঠিকঠাক চলা কোড PyPy-তে হাজার হাজার খোলা ফাইল জমিয়ে ফেলতে পারে।

দ্বিতীয় সমস্যা reference cycle. একটি Account object তার Transaction-কে দেখায়, Transaction আবার Account-কে — দুজনের count কখনো শূন্য হয় না। এদের খোঁজে CPython-এর আলাদা cycle collector (gc module), যা চলে নিজের হিসাবে। আরও খারাপ ছিল পুরোনো দিনে: Python 3.4-এর আগে cycle-এর কোনো object-এ __del__ থাকলে collector তাকে মুছতেই পারত না — কোনটির __del__ আগে চালাবে, নিরাপদে ঠিক করা যেত না — তাই সেগুলো gc.garbage তালিকায় চিরকাল পড়ে থাকত। Antoine Pitrou-র PEP 442 — “Safe object finalization” Python 3.4-এ এই সমস্যা অনেকটা সমাধান করে। তবু কখন চলবে, সেই অনিশ্চয়তা থেকেই যায়।

তৃতীয় সমস্যা লুকানো রেফারেন্স. exception-এর traceback প্রতিটি frame-এর local ভ্যারিয়েবল ধরে রাখে; তাই error-এর পরে একটি object আপনার ধারণার চেয়ে অনেক বেশি সময় বেঁচে থাকতে পারে। ডিবাগার, profiler, cache — যেকোনোটি একটি রেফারেন্স রাখলেই __del__ পিছিয়ে যায়।

চতুর্থ সমস্যা interpreter বন্ধের সময়. প্রোগ্রাম শেষে Python module-গুলো ভাঙতে থাকে। আগের version-এ এ সময় module-এর global নামগুলো None করে দেওয়া হতো, তাই __del__-এর ভেতরে os.remove ডাকলে 'NoneType' object has no attribute ধরনের অদ্ভুত error আসত। PEP 442-এর পরে এটি অনেক ভালো হয়েছে, কিন্তু ডকুমেন্টেশন এখনো সতর্ক করে যে বন্ধের সময় বেঁচে থাকা object-এর __del__ চলবে, এমন নিশ্চয়তা নেই।

পঞ্চম, error-এর কোনো ঠিকানা নেই. __del__-এ exception হলে ডাকার কেউ নেই; Python শুধু stderr-এ “Exception ignored in …” লিখে এগিয়ে যায়। ডেটাবেসে শেষ লেখা ব্যর্থ হলো, অথচ প্রোগ্রাম জানল না।

মূল উপলব্ধি: __del__ হলো “যখন সুবিধা হয়, তখন” চলা একটি hook — নিরাপত্তা-জাল, মূল ব্যবস্থা নয়। নিশ্চিত পরিষ্কার চাইলে নিশ্চিত মুহূর্ত চাই: with, বা একটি স্পষ্ট close(). শেষ ভরসা হিসেবে কিছু রাখতে হলে weakref.finalize, যা বন্ধের সময়ও চলার চেষ্টা করে।

প্র ০৪contextlib.contextmanager একটি generator-কে কীভাবে context manager বানায়? পর্ব ৩৪-এর generator-এর কোন ক্ষমতাগুলো এখানে কাজে লাগে?

পর্ব ৩৪-এ generator দেখেছেন মূলত মান তৈরির যন্ত্র হিসেবে — for লুপ বারবার next() ডাকে, generator একেকটি মান yield করে। কিন্তু generator-এর আরেকটি বৈশিষ্ট্য আরও গভীর: সে মাঝপথে থেমে নিজের পুরো অবস্থা (local ভ্যারিয়েবল, কোন লাইনে আছে, কোন try-এর ভেতরে) ধরে রাখতে পারে, আর পরে ঠিক সেখান থেকে চলতে পারে। context manager-এর ঠিক এটাই দরকার — “আগের কাজ করে থামো, ব্লক চলুক, তারপর বাকিটা করো”.

Python 2.5-এর PEP 342 generator-এ তিনটি method যোগ করে, যা এই ব্যবস্থা সম্ভব করেছে: send(value) (থেমে থাকা generator-এ একটি মান পাঠানো), throw(exc) (থেমে থাকা জায়গায় একটি exception তোলা) আর close() (generator-কে থামতে বলা)। @contextmanager যে মোড়ক বানায় (ভেতরের নাম _GeneratorContextManager), তার কাজ মোটামুটি এমন:

  • __enter__: next(gen) ডাকে। generator প্রথম yield পর্যন্ত চলে; yield করা মান ফেরত যায় as-এ। generator কিছুই yield না করে শেষ হলে: RuntimeError("generator didn't yield").
  • __exit__, সফল পথে: আবার next(gen). generator এবার শেষ হওয়ার কথা (StopIteration). আবার yield করলে: RuntimeError("generator didn't stop").
  • __exit__, exception-এর পথে: gen.throw(exc). exception yield লাইনে ওঠে। generator সেটি আবার তুলে দিলে মোড়ক False ফেরত দেয় (exception ওপরে যায়)। generator সেটি ধরে চুপচাপ শেষ হলে মোড়ক True ফেরত দেয় — অর্থাৎ চাপা।

শেষ নিয়মটি থেকে একটি সূক্ষ্ম ফাঁদ বোঝা যায়: generator-এর ভেতরে try: yield except Exception: pass লিখলে আপনি অজান্তেই ব্লকের সব exception চাপা দিচ্ছেন। এ ছাড়া মোড়কটি কিছু প্রান্তিক ব্যাপারও সামলায়, যেমন ব্লকের ভেতর থেকে ওঠা StopIteration-কে generator-এর স্বাভাবিক শেষের সাথে গুলিয়ে না ফেলা — PEP 479 (Python 3.7 থেকে পূর্ণভাবে) generator-এর ভেতরে StopIteration-কে RuntimeError-এ বদলে দিয়ে এই বিভ্রান্তি দূর করেছে।

আরও একটি সুন্দর সংযোগ: Python 3.2 থেকে @contextmanager-এর মোড়ক ContextDecorator-এর উত্তরসূরি, তাই একই manager decorator হিসেবেও চলে — পর্ব ৩৫ আর ৩৬ এখানে মিলে গেল। async জগতে একই ছাঁচে Python 3.7-এ এসেছে @asynccontextmanager.

মূল উপলব্ধি: generator শুধু “মানের স্রোত” নয় — সে থেমে থাকা একটি গণনা, যাকে বাইরে থেকে চালানো, মান পাঠানো বা exception ছোড়া যায়। @contextmanager এই ক্ষমতা দিয়ে বিশ লাইনের class-কে ছয় লাইনের ফাংশনে নামিয়ে আনে। আর পরে asyncio-র coroutine-ও (পর্ব ৪২) জন্ম নিয়েছে এই একই “থামা আর আবার চলা” ভাবনা থেকে।

প্র ০৫async with কেন আলাদা কিওয়ার্ড? PEP 492 কোন সমস্যার সমাধান করেছিল, আর আজ async context manager কোথায় কোথায় দেখা যায়?

সাধারণ __enter__ আর __exit__ সাধারণ ফাংশন — তারা শুরু থেকে শেষ পর্যন্ত একটানা চলে। কিন্তু asynchronous প্রোগ্রামে (পর্ব ৪২) অনেক “আগে-পরে” কাজেই অপেক্ষা থাকে: একটি ডেটাবেস connection pool থেকে connection নিতে অপেক্ষা, নেটওয়ার্ক connection বন্ধের সময় সার্ভারকে বিদায়-বার্তা পাঠানো, অন্য coroutine-এর ধরে রাখা lock পেতে অপেক্ষা। সাধারণ __exit__-এর ভেতরে অপেক্ষা করলে পুরো event loop আটকে যায় — একজনের অপেক্ষায় হাজার জনের কাজ থেমে থাকে।

এই সমস্যার সমাধান আসে PEP 492 — “Coroutines with async and await syntax”-এ, যা লিখেছিলেন Yury Selivanov, আর যা Python 3.5 (২০১৫)-এ প্রকাশিত হয়। এই PEP শুধু async def আর await-ই আনেনি; সঙ্গে এনেছিল async with আর async for. নতুন protocol-এ দুটি method: __aenter__ আর __aexit__, যারা coroutine — অর্থাৎ ভেতরে await করতে পারে। async with এদের ফল await করে, তাই অপেক্ষার সময় event loop অন্য কাজ চালাতে পারে।

আলাদা কিওয়ার্ড কেন, সাধারণ with কেন নিজে থেকে বুঝে নেয় না? কারণ Python-এর দর্শন: অপেক্ষা যেখানে হতে পারে, সেখানে চোখে দেখা যাক। await, async for, async with — প্রতিটি একটি দৃশ্যমান চিহ্ন যে এখানে এই coroutine থামতে পারে, আর এই ফাঁকে অন্য coroutine ডেটা বদলে ফেলতে পারে। লুকানো থামা concurrency-বাগের বড় উৎস।

পরের বছরগুলোতে চারপাশের ব্যবস্থা পূর্ণ হয়: Python 3.7-এ contextlib.asynccontextmanager (async generator দিয়ে manager) আর AsyncExitStack. Python 3.10-এ aclosing আর nullcontext-এর async সমর্থন। আর Python 3.11-এ asyncio নিজেই দুটি গুরুত্বপূর্ণ async context manager পেয়েছে: asyncio.TaskGroup (একদল task চালানো, একটি ব্যর্থ হলে বাকিগুলো বাতিল করে সবার শেষ পর্যন্ত অপেক্ষা) আর asyncio.timeout() (একটি ব্লকের জন্য সময়সীমা)। লাইব্রেরিতেও সর্বত্র: aiohttp-এর async with session.get(url), httpx-এর AsyncClient, async ডেটাবেস driver-এর connection ও transaction.

TaskGroup বিশেষভাবে আকর্ষণীয়, কারণ এটি “structured concurrency” নামের একটি ভাবনার Python রূপ — task-গুলোর জীবনকাল একটি ব্লকে বাঁধা, ব্লক থেকে বেরোনোর আগে সবাই শেষ। অর্থাৎ with-এর মূল নীতি — “ব্লকে যা শুরু, ব্লকেই তার শেষ” — এখন সম্পদ ছাড়িয়ে চলমান কাজের ক্ষেত্রেও প্রয়োগ হচ্ছে।

মূল উপলব্ধি: async with নতুন কোনো ধারণা নয় — একই আগে-পরে নিশ্চয়তা, শুধু আগে-পরের কাজ এখন অপেক্ষা করতে পারে, আর সেই অপেক্ষা কোডে দৃশ্যমান। এই পর্বে protocol ভালোভাবে বুঝলে পর্ব ৪২-এ শুধু নামের আগে একটি a যোগ হবে।

অনুশীলন

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

  1. ক্রম অনুমান করুন: একটি class Tag-এর __enter__ ছাপে "ঢুকলাম" আর self ফেরত দেয়; __exit__ ছাপে "বেরোলাম" আর exception-এর type-এর নাম (না থাকলে None), তারপর False ফেরত দেয়। একটি ফাংশন def f():-এর ভেতরে with Tag():, তার ভেতরে return "ফল". বাইরে print(f()). পুরো আউটপুট ক্রমসহ লিখুন।

    তিন লাইন: ঢুকলাম, বেরোলাম None, ফল. return ব্লকের ভেতর থেকে হলেও __exit__ চলে — finally-এর মতো — আর এটি exception নয়, তাই তিনটি argument None. ফেরত মান "ফল" আগে ঠিক হয়ে থাকে, __exit__ শেষ হওয়ার পরে ফাংশন সেটি ফেরত দেয়, তারপর print ছাপে। অর্থাৎ return দিয়ে with এড়ানো যায় না — এটাই আসল নিশ্চয়তা।

  2. বাগ খুঁজুন: AI লিখেছে — @contextmanager def locked(lock):, body-তে lock.acquire(), yield, lock.release() (কোনো try নেই)। ব্যবহার: with locked(bank_lock): transfer(...), আর transfer কখনো কখনো ValueError দেয়। কী ঘটবে, আর ঠিক করবেন কীভাবে?

    সফল পথে সব ঠিক। কিন্তু transfer ValueError দিলে @contextmanager সেটি yield লাইনে ছুড়ে দেয়; generator-এ কোনো try নেই, তাই exception সরাসরি বেরিয়ে যায় আর lock.release() লাইনটি কখনো চলে না। lock চিরকাল ধরা থাকে — পরের যে thread bank_lock চাইবে, সে অনন্তকাল অপেক্ষা করবে (deadlock-এর মতো আটকে যাওয়া)। ঠিক রূপ: lock.acquire(), তারপর try: yield finally: lock.release(). আরও ভালো: এই manager লেখারই দরকার নেই — threading.Lock নিজেই context manager, সরাসরি with bank_lock: লিখুন।

  3. চাপা নাকি ওপরে: তিনটি __exit__ ভাবুন। (ক) কিছুই ফেরত দেয় না। (খ) return exc_type is KeyError. (গ) return True. প্রতিটির ক্ষেত্রে ব্লকে KeyError আর TypeError হলে কী হবে? কোনটি কখন ন্যায্য?

    (ক) None মিথ্যা-মান, তাই দুটি exception-ই ওপরে যায় — সাধারণ ও নিরাপদ, বেশিরভাগ manager এমন। (খ) KeyError হলে True → চাপা; TypeError হলে False → ওপরে। এটি suppress(KeyError)-এর মতো — ন্যায্য, যদি manager-এর উদ্দেশ্যই “চাবি না থাকলে সমস্যা নেই” হয়, আর ব্লক ছোট হয়। (তবে is দিয়ে তুলনা KeyError-এর subclass ধরে না; সেগুলোও ধরতে exc_type is not None and issubclass(exc_type, KeyError).) (গ) দুটিই চাপা — TypeError-এর মতো আসল বাগও অদৃশ্য। প্রায় কখনোই ন্যায্য নয়; AI-এর কোডে দেখলে সবার আগে প্রশ্ন করুন।

  4. একটি manager লিখুন (এবং AI-কে যাচাই করুন): @contextmanager দিয়ে temporary_setting(config, key, value) লিখুন — ব্লকের জন্য একটি dict config-এ key-এর মান বদলাবে, আর ব্লক শেষে (error হলেও) আগের মান ফেরাবে; চাবিটি আগে না থাকলে শেষে মুছে দেবে। যেমন টেস্টে কিছুক্ষণের জন্য config["currency"] = "USD". তারপর একই কাজ AI-কে দিয়ে লিখিয়ে অংশ ৮-এর চেকলিস্ট ধরে মিলিয়ে নিন।

    একটি সম্ভাব্য সমাধান:

    from contextlib import contextmanager
    
    _MISSING = object()                     # “চাবি ছিল না” বোঝাতে নিজস্ব চিহ্ন
    
    @contextmanager
    def temporary_setting(config, key, value):
        old = config.get(key, _MISSING)
        config[key] = value
        try:
            yield config
        finally:
            if old is _MISSING:
                del config[key]             # আগে ছিল না → মুছে ফেলো
            else:
                config[key] = old           # আগে ছিল → পুরোনো মান ফেরত

    তিনটি জিনিস লক্ষ করুন। এক, পুরোনো মান মনে রাখা হয়েছে বদলের আগে. দুই, ফেরানোর কাজ finally-তে, তাই ব্লকে error হলেও config নষ্ট থাকে না। তিন, “চাবি ছিল না” আর “চাবির মান None ছিল” আলাদা করতে None-এর বদলে একটি নিজস্ব sentinel object. AI-এর কোডে খেয়াল করুন: finally আছে কি? চাবি আগে না থাকলে সে কি শেষে None বসিয়ে দেয় (ভুল), নাকি মুছে দেয়? exception চাপা দিচ্ছে কি?

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

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

ক্রমwith Manager() as m: ব্লকের body-তে একটি exception উঠলে, আর __exit__ False ফেরত দিলে, যে ক্রমে ঘটনাগুলো ঘটে সেই ক্রমে সাজান (▲▼ চেপে সরান)

  1. Manager() হিসাব হয়ে object তৈরি হয়
  2. __enter__() চলে, তার ফেরত মান m নামে বসে
  3. body চলতে চলতে exception ওঠে
  4. __exit__ exception-এর type, object আর traceback পায়
  5. __exit__ পরিষ্কার করে False ফেরত দেয়
  6. exception আবার উঠে ওপরের except-এ যায়

Python-এ with X() as v: লিখলে v নামে কী বসে?

  • সবসময় X() দিয়ে তৈরি object নিজে
  • X()-এর __enter__() method যা ফেরত দেয়
  • __exit__()-এর ফেরত মান
  • X class নিজে

একটি context manager-এর __exit__ method কোনো return ছাড়াই শেষ হয়। with ব্লকে একটি ValueError উঠলে কী হবে?

  • exception চাপা পড়বে, with-এর পরের লাইন চলবে
  • __exit__ চলবে না, সরাসরি traceback আসবে
  • TypeError, কারণ __exit__-কে কিছু ফেরত দিতেই হয়
  • __exit__ চলবে, তারপর ValueError আবার উঠবে

@contextlib.contextmanager দিয়ে লেখা একটি generator-এ পরিষ্কারের কোড yield-এর পরে কেন finally-এর ভেতরে রাখতে হয়?

  • ব্লকে exception হলে সেটি yield লাইনে ছুড়ে দেওয়া হয়, তাই finally ছাড়া পরিষ্কার বাদ পড়ে
  • finally ছাড়া generator একবারও yield করতে পারে না
  • finally exception-কে চাপা দেয়, যা context manager-এর জন্য জরুরি
  • Python-এর সিনট্যাক্স অনুযায়ী yield-এর পরে finally বাধ্যতামূলক, নইলে SyntaxError

Python-এ with sqlite3.connect("bank.db") as conn: ব্লক সফলভাবে শেষ হলে connection-এর কী হয়?

  • transaction commit হয় আর connection বন্ধ হয়ে যায়
  • transaction rollback হয়, connection খোলা থাকে
  • transaction commit হয়, কিন্তু connection খোলাই থাকে
  • কিছুই হয় না; commit নিজে হাতে ডাকতে হয়

contextlib.ExitStack-এ পরপর a, b, c — তিনটি context manager enter_context দিয়ে যোগ করা হলো। ব্লক শেষে কোন ক্রমে তাদের __exit__ চলবে?

  • a, b, c
  • c, b, a
  • যেকোনো ক্রমে — নিশ্চিত নয়
  • তিনটি একসাথে, আলাদা thread-এ

with suppress(Exception): ব্লকে পাঁচটি লাইন আছে, আর দ্বিতীয় লাইনে একটি TypeError উঠল। কী হবে?

  • exception চাপা পড়বে, আর তৃতীয় থেকে পঞ্চম লাইন কোনো খবর ছাড়াই বাদ যাবে
  • শুধু দ্বিতীয় লাইন বাদ যাবে, বাকি তিন লাইন চলবে
  • TypeError ওপরে যাবে, কারণ এটি suppress-এর তালিকায় নেই
  • একটি সতর্কবার্তা ছাপা হবে, তারপর বাকি লাইন চলবে

শব্দকোষ

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

মনে রাখুন

  1. সম্পদ (ফাইল, lock, transaction, সাময়িক সেটিং) নেওয়ার পরে ফেরত দিতেই হবে। with নিশ্চিত করে বেরোনোর কাজ চলবেই — return, break বা exception, যেভাবেই বেরোন।
  2. Protocol: __enter__()-এর ফেরত মান as-এ বসে; __exit__(exc_type, exc, tb) সবসময় চলে। True ফেরত দিলে exception চাপা — শুধু নির্দিষ্ট, প্রত্যাশিত exception-এর জন্য।
  3. context manager পরিষ্কার করে, except সিদ্ধান্ত নেয়। Transaction: exception দেখলে rollback, তারপর exception ওপরে।
  4. @contextmanager: yield-এর আগে ঢোকা, পরে বেরোনো — পরিষ্কার সবসময় finally-তে। contextlib-এ তৈরি: suppress (সাবধানে), closing, redirect_stdout, ExitStack (উল্টো ক্রমে বন্ধ), nullcontext.
  5. with sqlite3.connect() commit/rollback করে, বন্ধ করে না। C++-এর RAII destructor-এ নিশ্চিত; Python-এ __del__ অনিশ্চিত — তাই পরিষ্কার মানেই with.

পরের পর্বে: এই পর্বে __exit__(self, exc_type, exc, tb)-এর তিনটি argument-এর ধরন মুখে বলতে হলো। পরের পর্বে দেখবেন কীভাবে কোডেই লিখে দেওয়া যায় কোন ভ্যারিয়েবলে কোন ধরনের মান থাকবে — type hint — আর mypy কীভাবে কোড না চালিয়েই ভুল ধরে।

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

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