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

scope, *args/**kwargs, closure আর default-এর ফাঁদ

Scope (LEGB), *args/**kwargs, Closures & the Mutable Default Trap

Taught in Bangla, this lesson covers how Python finds names: the LEGB rule, UnboundLocalError, global and nonlocal, *args and **kwargs, unpacking arguments at the call site, the mutable default argument trap, closures, late binding in loops, and choosing between a closure and a class.

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

  • Scope কী, আর Python একটি নাম কোন ক্রমে খোঁজে (LEGB); built-in নাম ঢেকে ফেলার বিপদ
  • ভেতর থেকে বাইরের নাম পড়া বনাম বদলানো, UnboundLocalError কেন আসে, global ও nonlocal কখন
  • *args, **kwargs, parameter-এর ক্রমের নিয়ম, ডাকার সময় unpacking আর argument ফরোয়ার্ড করা wrapper
  • Mutable default argument-এর ফাঁদের পুরো ব্যাখ্যা আর None দিয়ে সমাধান
  • Closure, factory function, nonlocal দিয়ে counter, লুপে late binding-এর ফাঁদ, আর closure বনাম class

১ · প্রতিটি নামের একটি ঠিকানা — scope আর LEGB নিয়ম

পর্ব ১৩-এ দেখেছিলেন, ফাংশনের ভেতরে তৈরি নাম local variable — সেই ডাকের frame-এ থাকে, ডাক শেষে মুছে যায়। কিন্তু তখন একটি প্রশ্ন খোলা রেখেছিলাম: ফাংশনের ভেতরে যদি এমন নাম লেখেন যা ভেতরে তৈরি হয়নি — যেমন বাইরের rate, বা round, বা print — Python সেটি কোথায় পায়? এই পর্ব সেই প্রশ্নের উত্তর দিয়ে শুরু।

পর্ব ০৫ থেকে আপনি জানেন, Python-এ ভ্যারিয়েবল মানে একটি name, যা কোনো object-এর গায়ে লেবেলের মতো লাগানো। প্রতিটি লেবেল লেখা থাকে কোনো একটি “খাতায়”। Python-এর ভাষায় এই খাতাকে বলে namespace, আর কোডের কোন অংশ থেকে কোন খাতা দেখা যায়, সেটাই scope. সহজ কথায়: scope বলে দেয় একটি নাম কোথায় থাকে, আর কোথা থেকে তাকে দেখা যায়.

ধরুন আপনি রাজশাহীর একটি মহল্লায় থাকেন, আর কেউ বলল “রহিমকে ডাকো”। আপনি প্রথমে নিজের বাসায় খুঁজবেন — বাসায় কোনো রহিম আছে? না থাকলে মহল্লায়। মহল্লায়ও না পেলে পুরো শহরে। শেষে জাতীয় পর্যায়ের পরিচিত নাম — সবাই যাকে চেনে। আর যেই প্রথম রহিমকে পেলেন, সেখানেই খোঁজা শেষ; শহরে আরও দশজন রহিম থাকলেও তাতে কিছু যায় আসে না। Python ঠিক এভাবেই নাম খোঁজে: বাসা = Local, মহল্লা = Enclosing, শহর = Global, দেশ = Built-in.

চারটি স্তর একে একে দেখি:

  • L — Local: এখন যে ফাংশন চলছে, তার নিজের নাম — parameter আর ভেতরে assign করা ভ্যারিয়েবল।
  • E — Enclosing: ফাংশনের ভেতরে ফাংশন থাকলে, বাইরের (ঘিরে রাখা) ফাংশনের local নাম। একে বলে enclosing scope. শুধু nested ফাংশনেই এই স্তরটি থাকে।
  • G — Global: ফাইলের (module-এর) একেবারে বাইরের স্তরে তৈরি নাম — যেমন ফাইলের শুরুতে লেখা rate = 0.15, বা def দিয়ে বানানো ফাংশনের নাম। Python-এ “global” মানে আসলে এই module-এর global, পুরো প্রোগ্রামের নয়; অন্য ফাইলের global আলাদা (পর্ব ২২-এ module-এর গল্প)।
  • B — Built-in: Python নিজে যে নামগুলো সব জায়গায় দিয়ে রাখে — print, len, round, sum, list, ValueError ইত্যাদি। এরা থাকে builtins নামের একটি module-এ।

LEGB — একটি নাম খোঁজার চার ধাপ

ভেতর থেকে বাইরে; প্রথম মিলেই থামা

  1. Localpriceএখনকার ফাংশন
  2. Enclosingdiscountঘিরে রাখা ফাংশন
  3. Globalratemodule-এর স্তর
  4. Built-inroundPython-এর নিজের
ছবি ২০.১ — চার স্তরের কোথাও না পেলে NameError. খোঁজা সবসময় ভেতর থেকে বাইরে; বাইরের কোড ভেতরের নাম কখনো দেখে না।

এবার একটি বাস্তব উদাহরণে চারটি স্তর একসাথে দেখুন। একটি দোকানের বিল-ফাংশন, যার ভেতরে আরেকটি ফাংশন। ভেতরের total ফাংশন চারটি নাম ব্যবহার করে — চারটিই চার আলাদা জায়গা থেকে আসে। “পরে” চাপুন:

1rate = 0.15                    # global2def make_bill(shop):3    discount = 0.10            # enclosing4    def total(price):          # local: price5        return round(price * (1 - discount) * (1 + rate))6    return total78bill = make_bill("Aarong")9print(bill(1000))
কোন নাম কোথায় পাওয়া গেল
price
discount
rate
round
  1. price — Local. bill(1000) ডাকায় total-এর নতুন frame তৈরি হলো, যেখানে parameter price = 1000. লাইন ৫-এ প্রথম নাম price — Python প্রথমেই Local খাতায় দেখে, পেয়ে যায়। খোঁজা শেষ।
  2. discount — Enclosing. discount নামটি total-এর ভেতরে তৈরি হয়নি, তাই Local-এ নেই। পরের স্তর Enclosing — ঘিরে রাখা make_bill-এর local নাম। সেখানে discount = 0.10 আছে। মজার ব্যাপার: make_bill অনেক আগেই শেষ হয়ে গেছে, তবু নামটি পাওয়া গেল — এর রহস্য খুলবে অংশ ৬-এ, closure-এ।
  3. rate — Global. Local-এ নেই, Enclosing-এও নেই। এবার Global, অর্থাৎ ফাইলের বাইরের স্তর — লাইন ১-এ rate = 0.15. পাওয়া গেল।
  4. round — Built-in. round কোথাও আপনি তৈরি করেননি। Local, Enclosing, Global — তিনটিতেই নেই। শেষ স্তর Built-in-এ Python-এর নিজের round ফাংশন পাওয়া গেল। এখানেও না পেলে আসত NameError.
  5. হিসাব। চারটি নামই মিলেছে: 1000 × 0.9 × 1.15 = 1035.0, আর round তাকে পূর্ণসংখ্যা 1035 করল। লক্ষ করুন, total ফাংশনটি নিজে মাত্র একটি নাম (price) জানে; বাকি তিনটি ধার করেছে বাইরের তিন স্তর থেকে।
  6. ছাপা। print-ও একটি built-in নাম — একই LEGB খোঁজার পরে পাওয়া গেছে — আর সেটি ছাপল 1035. প্রতিটি নাম, প্রতিবার, এই খোঁজার ভেতর দিয়েই যায়; Python শুধু চতুরভাবে এটিকে দ্রুত করে।
মূল ধারণা

Scope ঠিক হয় কোড কোথায় লেখা আছে তা দেখে, কোথা থেকে ডাকা হলো তা দেখে নয়। total-কে আপনি যেখান থেকেই ডাকুন, তার Enclosing সবসময় make_bill — কারণ সে লেখা আছে make_bill-এর ভেতরে। একে বলে lexical scope (lexical = লেখা অনুযায়ী)। প্রায় সব আধুনিক ভাষা — C, Java, JavaScript, Python — এই নিয়ম মানে।

২ · ভেতর থেকে পড়া সহজ, বদলানো নয় — UnboundLocalError, global আর nonlocal

LEGB খোঁজা চলে পড়ার সময়। কিন্তু ফাংশনের ভেতরে কোনো নামে assign করলে (=, +=, for x in, def x ইত্যাদি) নিয়ম আলাদা: Python ধরে নেয় নামটি এই ফাংশনেরই local. আর এই সিদ্ধান্ত হয় ফাংশন চালানোর আগে, পর্ব ০৫-এর সেই compile ধাপে — Python পুরো ফাংশনের body একবারে দেখে, আর যেখানেই একটি নামে assign পায়, পুরো ফাংশনে নামটিকে local ঘোষণা করে।

এর ফল একটি বিখ্যাত error. একটি ওয়েবসাইটের ভিজিট গোনার কথা ভাবুন:

count = 0                # global

def click():
    count += 1           # মানে: count = count + 1
    return count

click()

দেখে মনে হয় এটি বাইরের count এক বাড়াবে। কিন্তু চালালে traceback আসে, যার শেষ লাইন:

UnboundLocalError: cannot access local variable 'count' where it is not associated with a value

কেন? count += 1 একটি assignment, তাই compile-এর সময়েই Python ঠিক করে ফেলেছে: click-এর ভেতরে count একটি local নাম। চালানোর সময় count + 1 হিসাব করতে গিয়ে সে local count খোঁজে — কিন্তু সেটি এখনো কোনো object-এর সাথে জোড়া হয়নি (unbound). Global-এর count সে দেখতেই যায় না, কারণ নামটি আগেই local ঘোষিত। Error-এর নাম UnboundLocalError ঠিক এই কথাই বলে: “local নামটি আছে, কিন্তু এখনো কোনো মানের সাথে বাঁধা নয়”। (পুরোনো Python version-এ বার্তাটি ছিল একটু অন্য রকম — local variable 'count' referenced before assignment; Python 3.11 থেকে বর্তমান লেখা।)

অদ্ভুত কিন্তু সত্যি: assignment যদি ফাংশনের শেষ লাইনেও থাকে, তবু পুরো ফাংশনে নামটি local. যেমন def show(): print(count); count = 5 — প্রথম লাইনের print(count)-ও একই error দেবে, যদিও assignment তার পরে। Python লাইন ধরে সিদ্ধান্ত নেয় না, পুরো ফাংশন দেখে নেয়।

global — “এই নামটি বাইরের, local বানিও না”

সমাধানের একটি উপায় হলো Python-কে আগেই জানিয়ে দেওয়া: global count. এই লাইনটিকে বলে global statement. এর পরে ফাংশনের ভেতরে count-এ যত assignment, সব যায় module-এর global count-এ:

count = 0

def click():
    global count         # count = module-এর global নাম
    count += 1
    return count

এটি কাজ করে। কিন্তু অভিজ্ঞ প্রোগ্রামাররা global দেখলেই সতর্ক হন। কারণ এখন click-এর ফল শুধু তার argument-এর ওপর নির্ভর করে না — নির্ভর করে একটি লুকানো বাইরের মানের ওপর, যা প্রোগ্রামের যেকোনো জায়গা থেকে যে কেউ বদলাতে পারে। এই ধরনের মানকে বলে global state. এর সমস্যাগুলো:

  • লুকানো যোগাযোগ: ফাংশনের def লাইন দেখে বোঝা যায় না সে কী কী পড়ে বা বদলায়। বাগ খুঁজতে পুরো ফাইল ঘাঁটতে হয়।
  • পরীক্ষা কঠিন: প্রতিটি টেস্টের আগে global মান “রিসেট” করতে হয়, নইলে আগের টেস্টের ফল পরেরটিতে চুঁইয়ে পড়ে।
  • একসাথে একাধিক কাজ অসম্ভব: দুটি আলাদা ওয়েবসাইটের ভিজিট গুনতে চাইলে? একটিই count — দুটি counter রাখার উপায় নেই।
  • Thread-এ ঝুঁকি: একাধিক thread একই global বদলালে ফল এলোমেলো হতে পারে (পর্ব ৪১)।

নিয়ম: global পড়া ঠিক আছে যদি সেটি ধ্রুবক হয় — যেমন VAT_RATE = 0.15 (PEP 8 অনুযায়ী ধ্রুবকের নাম বড় হাতের অক্ষরে)। কিন্তু ফাংশনের ভেতর থেকে global বদলানো প্রায় সবসময় এড়ানো যায়: মান parameter-এ নিন, ফল return করুন। অথবা অবস্থা ধরে রাখতে চাইলে closure বা class ব্যবহার করুন — দুটিই এই পর্বে আসছে।

nonlocal — ঘিরে রাখা ফাংশনের নাম বদলানো

একই সমস্যা nested ফাংশনেও হয়। ভেতরের ফাংশন বাইরের ফাংশনের একটি নাম বদলাতে চাইলে += লিখলেই নামটি ভেতরের local হয়ে যায়। এখানে global কাজে আসবে না, কারণ নামটি global নয় — Enclosing স্তরের। এর জন্য আছে nonlocal: “এই নামটি আমার নয়, আমাকে ঘিরে রাখা নিকটতম ফাংশনের; সেখানেই বদলাও”। nonlocal এসেছে Python 3.0-তে; অংশ ৬-এ এটি দিয়ে একটি পরিষ্কার counter বানাব।

ফাংশনের ভেতরে আপনি যা করছেনPython কী করে
বাইরের নাম শুধু পড়া (print(rate))LEGB ধরে খোঁজে — কোনো ঘোষণা লাগে না
একই নামে assign (rate = 0.05)নতুন local নাম তৈরি; বাইরেরটি অপরিবর্তিত
পড়া এবং assign দুটোই (count += 1)UnboundLocalError
global count লিখে assignmodule-এর global বদলায়
nonlocal count লিখে assignনিকটতম ঘিরে রাখা ফাংশনের নাম বদলায়
বাইরের list-এ cart.append(x)কোনো assign নেই, তাই ঘোষণা লাগে না — একই object বদলায় (পর্ব ১৯)

শেষ সারিটি খেয়াল করুন। cart.append(x) নামটিকে নতুন object-এ লাগায় না, শুধু পুরোনো object-টিকে ভেতর থেকে বদলায় — পর্ব ১৯-এর mutability. তাই এখানে global লাগে না, আর ঠিক এই কারণেই বাইরের list চুপচাপ বদলে যাওয়া আরও বিপজ্জনক: কোনো ঘোষণা ছাড়াই side effect.

Built-in নাম ঢেকে ফেলা — shadowing

LEGB-র একটি পার্শ্বফল: যেহেতু Global খোঁজা হয় Built-in-এর আগে, আপনি যদি module-এ list, sum, max, input বা id নামে ভ্যারিয়েবল বানান, তাহলে Python-এর আসল built-in আর পাওয়া যায় না — আপনার নামটি তাকে ঢেকে দেয়। একে বলে shadowing, যা পর্ব ০৬-এ এক লাইনে দেখেছিলেন:

sum = 0
for price in [120, 80, 50]:
    sum += price         # “মোট” রাখতে sum নামটি নেওয়া হলো
print(sum([1, 2, 3]))    # TypeError: 'int' object is not callable

এখানে sum এখন একটি int (250), ফাংশন নয় — তাই তাকে ডাকা যায় না। দশ লাইন পরে কেউ sum(...) লিখলে error বার্তা মূল ভুলের জায়গা দেখায় না, দেখায় ব্যবহারের জায়গা। list = [...] লেখার পর list("abc") ডাকলেও একই TypeError. সমাধান সহজ: অর্থপূর্ণ নাম দিন — total, prices, items. REPL-এ ভুল করে ঢেকে ফেললে del sum লিখলে আপনার নামটি মুছে যায়, আর LEGB আবার built-in পর্যন্ত পৌঁছায়। VS Code-এর মতো editor আর ruff-এর মতো linter (পর্ব ৪৪) এই ভুলে সতর্ক করে।

৩ · *args আর **kwargs — যত খুশি argument

এবার প্রসঙ্গ বদলাই — নামের ঠিকানা থেকে argument-এর দিকে। পর্ব ১৩-এর সব ফাংশনে parameter-এর সংখ্যা ছিল নির্দিষ্ট। কিন্তু print-কে ভাবুন: print(1), print(1, 2, 3), print("a", "b", sep="-") — একটি argument হোক বা বিশটি, সব চলে। নিজের ফাংশনে এই সুবিধা পেতে Python দেয় দুটি বিশেষ parameter.

*args → একটি tuple

নামের আগে একটি তারা। নির্দিষ্ট parameter-গুলো ভরার পর বাড়তি যত positional argument, সব একটি tuple-এ জমা হয়। একটিও না এলে খালি tuple (). দেখুন: *args.

**kwargs → একটি dict

নামের আগে দুটি তারা। কোনো parameter-এর সাথে নাম না মেলা বাড়তি যত keyword argument, সব একটি dict-এ জমা হয় — চাবি হলো নাম (string), মান হলো argument. দেখুন: **kwargs.

আসল জাদু তারা চিহ্নে, নামে নয়। args আর kwargs (keyword arguments-এর সংক্ষেপ) শুধু প্রচলিত নাম; *prices বা **options লিখলেও একই কাজ হয়, আর অর্থপূর্ণ নাম দিলে পড়তে আরও সুবিধা। একটি সহজ উদাহরণ — যতগুলো দাম দেবেন, সবগুলোর যোগফল:

def bill_total(*prices, vat=0.15):
    return round(sum(prices) * (1 + vat), 2)

bill_total(100, 250, 80)        # prices = (100, 250, 80)
bill_total(500, vat=0.05)       # prices = (500,)
bill_total()                    # prices = ()  →  0.0

লক্ষ করুন, *prices-এর পরে লেখা vat স্বয়ংক্রিয়ভাবে keyword-only হয়ে গেছে। কারণ সহজ: সব বাড়তি positional argument তো *prices-ই খেয়ে ফেলে, তাই vat-এ কিছু পৌঁছানোর একমাত্র উপায় নাম ধরে পাঠানো। পর্ব ১৩-এর একক * চিহ্ন আসলে এরই নামহীন রূপ — “এখানে বাড়তি positional নেব না, তবে এর পরেরগুলো keyword-only”.

Parameter-এর ক্রমের নিয়ম

সব ধরনের parameter একসাথে লিখতে চাইলে একটি নির্দিষ্ট ক্রম মানতে হয়। ভুল ক্রম লিখলে চালানোর আগেই SyntaxError:

def f(a, b=1, *args, c, d=2, **kwargs)
সাধারণ → default-ওয়ালা → *args → keyword-only → **kwargs

এই ক্রমের যুক্তি আছে। Python argument বসায় বাঁ থেকে ডানে: আগে positional-গুলো সাধারণ parameter-এ (a, b), যা বাকি থাকে তা *args-এ। তারপর keyword-গুলো নাম মিলিয়ে বসে (c, d, এমনকি a বা b-ও নাম ধরে পাওয়া যায়), আর যেগুলোর নাম কোনো parameter-এর সাথে মেলে না, সেগুলো যায় **kwargs-এ। তাই **kwargs সবসময় শেষে — সে “বাকি সব”-এর ঝুড়ি। পর্ব ১৩-এর / (positional-only) থাকলে সেটি বসে একেবারে শুরুর দিকে — positional-only parameter-গুলোর ঠিক পরে, সাধারণ parameter-গুলোর আগে। উদাহরণ: f(10, 20, 30, 40, c=5, color="red") ডাকলে a=10, b=20, args=(30, 40), c=5, d=2 (default), kwargs={'color': 'red'}.

৪ · ডাকার সময় প্যাকেট খোলা — f(*lst, **dct) আর argument ফরোয়ার্ড করা

সংজ্ঞায় তারা চিহ্ন জমা করে (অনেক argument → একটি tuple বা dict). ডাকার সময় তারা চিহ্ন করে উল্টো কাজ — খুলে ছড়িয়ে দেয়. একে বলে argument unpacking:

  • f(*[500, 120]) মানে f(500, 120) — list (বা যেকোনো iterable) খুলে প্রতিটি উপাদান আলাদা positional argument.
  • f(**{"sep": " / ", "end": "!"}) মানে f(sep=" / ", end="!") — dict খুলে প্রতিটি চাবি-মান জোড়া একটি keyword argument.

বাস্তবে এটি খুব কাজের। ধরুন একটি CSV ফাইলের একটি সারি list হিসেবে পেলেন — row = ["রিনা", 85, "ঢাকা"] — আর একটি ফাংশন তিনটি আলাদা parameter চায়: register(*row). অথবা একটি settings dict থেকে argument পাঠানো: connect(**config). পর্ব ১৫-এর unpacking-এর মতোই ধারণা, শুধু এবার ফাংশন ডাকের ভেতরে।

Wrapper — argument হাতে নিয়ে অন্য ফাংশনে পাঠানো

দুটি দিক একসাথে মেলালে পাওয়া যায় Python-এর একটি অতি শক্তিশালী কৌশল: wrapper function. ফাংশনটি *args, **kwargs দিয়ে যা আসে সব জমা করে, নিজের কিছু কাজ যোগ করে, তারপর *args, **kwargs দিয়ে হুবহু আরেকটি ফাংশনে পাঠিয়ে দেয় — না জেনেই সেখানে কী কী argument ছিল। একে বলে argument “forward” করা।

একটি লগিং সহায়ক ভাবুন: প্রতিটি বার্তার সামনে স্তর (INFO, WARN) বসাবে, কিন্তু বাকি সব — যত মান, sep, end — print-এর কাছে হুবহু যাবে। print-এর প্রতিটি option আপনাকে আলাদা করে লিখতে হবে না। নিচের প্রথম runnable প্রোগ্রামে (অংশ ৫-এর শেষে) এটি চালিয়ে দেখবেন।

সামনে যা আসছে: এই wrapper কৌশল আর পরের অংশগুলোর closure মিলে তৈরি হয় decorator — একটি ফাংশনকে না বদলে তার চারপাশে নতুন আচরণ মোড়ানো, @ চিহ্ন দিয়ে লেখা। সময় মাপা, লগ রাখা, লগইন যাচাই — সবই decorator দিয়ে হয়। পুরো গল্প পর্ব ৩৫-এ।

*args, **kwargs-এর অপব্যবহার: সব জায়গায় def process(*args, **kwargs) লিখলে ফাংশনটি যেকোনো কিছু নেয় — কিন্তু তাতে তার আসল ইন্টারফেস লুকিয়ে যায়. কী কী argument লাগে, কোনটি বাধ্যতামূলক, বানান ভুল হলে কী হবে — def লাইন দেখে কিছুই বোঝা যায় না। Editor-এর auto-complete কাজ করে না, আর colr="red"-এর মতো বানান ভুল নিঃশব্দে kwargs-এ ঢুকে হারিয়ে যায়। নিয়ম: যে argument আপনি জানেন, তার নাম লিখুন; *args/**kwargs রাখুন শুধু সত্যিকারের “যেকোনো সংখ্যক” বা “অন্যকে ফরোয়ার্ড” করার কাজে।

৫ · Default-এর ফাঁদ — একবার তৈরি হওয়া list

পর্ব ১৩-এ এক লাইনে সতর্ক করেছিলাম: default হিসেবে [] দেবেন না। পর্ব ১৯-এ শিখেছেন mutable object আর alias কী। এবার দুটি মিলিয়ে পুরো রহস্য খুলি। একটি অনলাইন দোকানের শপিং-কার্ট ফাংশন — নতুন ক্রেতার জন্য খালি কার্ট, আর পণ্য যোগ হবে:

def add_to_cart(item, cart=[]):
    cart.append(item)
    return cart

লেখক ভেবেছিলেন: “cart না দিলে প্রতিবার নতুন খালি list”. আসলে তা হয় না। মূল কথাটি একটি বাক্যে: default-এর expression হিসাব হয় একবারই — def লাইন চলার মুহূর্তে, প্রতিটি ডাকে নয়. সেই মুহূর্তে একটি খালি list তৈরি হয়, আর ফাংশন object সেটি নিজের ভেতরে (add_to_cart.__defaults__-এ) রেখে দেয়। এরপর যতবার cart ছাড়া ডাকবেন, ততবার cart নামটি সেই একই list-এর গায়ে লাগবে। ধাপে ধাপে দেখুন:

1def add_to_cart(item, cart=[]):2    cart.append(item)3    return cart45a = add_to_cart("চাল")6b = add_to_cart("ডাল")7c = add_to_cart("তেল")8print(a, a is c)
ফাংশনের ভেতরে রাখা default আর বাইরের নাম
__defaults__
a
b
c
  1. def চলল — default তৈরি। Python লাইন ১ চালিয়ে ফাংশন object বানাল। এই মুহূর্তেই [] expression হিসাব হলো: একটি খালি list তৈরি হয়ে ফাংশনের __defaults__-এ জমা রইল। Body এখনো একবারও চলেনি।
  2. প্রথম ডাক। cart দেওয়া হয়নি, তাই cart নামটি লাগল __defaults__-এর সেই list-এ। append সেটিকেই বদলাল — এখন ['চাল']. ফেরত গেল সেই একই list, a তার গায়ে লাগল। এখনো সব ঠিক দেখাচ্ছে।
  3. দ্বিতীয় ডাক — ফাঁদ খুলল। আবার cart ছাড়া ডাক। নতুন list তৈরি হলো না — cart লাগল আগের সেই list-এই, যেখানে আগে থেকে 'চাল' আছে। এবার হলো ['চাল', 'ডাল']. আর a-ও বদলে গেল, কারণ a আর b একই object-এর দুটি লেবেল (পর্ব ১৯-এর alias).
  4. তৃতীয় ডাক। একই গল্প — তিনটি নাম a, b, c আর ফাংশনের default, চারটিই একটি মাত্র list দেখছে, যাতে এখন তিনটি পণ্য। তিনজন আলাদা ক্রেতার কার্ট আসলে একটিই ঝুড়ি!
  5. প্রমাণ। print(a, a is c) ছাপল ['চাল', 'ডাল', 'তেল'] True. is বলছে a আর c একই object. প্রথম ক্রেতা শুধু চাল নিয়েছিলেন, কিন্তু তাঁর কার্টে এখন অন্যদের ডাল-তেলও।
  6. সমাধান — None sentinel. সংজ্ঞা বদলান: def add_to_cart(item, cart=None):, আর body-র শুরুতে if cart is None: cart = []. এখন default হলো None, যা immutable — কেউ বদলাতে পারে না। নতুন list তৈরি হয় body-র ভেতরে, অর্থাৎ প্রতিটি ডাকে. তিনটি ডাকে তিনটি আলাদা কার্ট: ['চাল'], ['ডাল'], ['তেল'], আর a is c এখন False.

এখানে None একটি sentinel-এর কাজ করছে — একটি বিশেষ সংকেত-মান, যার মানে “কেউ কিছু পাঠায়নি”। if not cart: না লিখে if cart is None: লেখার কারণ আছে: কেউ ইচ্ছা করে নিজের খালি list পাঠালে not cart সত্য হবে, আর আপনি তাঁর list ফেলে নতুন একটি বানিয়ে ফেলবেন — তাঁর list-এ পণ্য যোগ হবে না। is None শুধু “কিছুই পাঠানো হয়নি” অবস্থাটি ধরে।

এবার প্রথম runnable প্রোগ্রাম। এতে আছে *args/**kwargs-এর ভেতরটা দেখা, print-এ সব ফরোয়ার্ড করা একটি লগ-সহায়ক, আর কার্টের বাগ ও সমাধান পাশাপাশি। চালানোর আগে শেষ তিন লাইনের আউটপুট অনুমান করুন:

def log(level, *args, **kwargs):
    print(f"[{level}]", args, kwargs)       # ভেতরে কী এল
    print(f"[{level}]", *args, **kwargs)    # হুবহু ফরোয়ার্ড

nums, opts = [500, 120], {"sep": " / "}
log("INFO", *nums, **opts)

def add_to_cart(item, cart=[]):
    cart.append(item)
    return cart

def add_to_cart_safe(item, cart=None):
    if cart is None:
        cart = []
    cart.append(item)
    return cart

print(add_to_cart("চাল"))
print(add_to_cart("ডাল"))
print(add_to_cart_safe("চাল"), add_to_cart_safe("ডাল"))

আউটপুট পাঁচ লাইনের। প্রথম লাইন [INFO] (500, 120) {'sep': ' / '} — log-এর ভেতরে কী পৌঁছেছে তার ছবি: *nums list খুলে দুটি আলাদা argument পাঠিয়েছিল, যা args-এ আবার একটি tuple হয়ে জমেছে; **opts dict খুলে sep=" / " পাঠিয়েছিল, যা kwargs-এ dict হয়ে জমেছে। দ্বিতীয় লাইন [INFO] / 500 / 120 — log সেগুলো আবার খুলে হুবহু print-এ ফরোয়ার্ড করেছে, তাই sep বসেছে [INFO]-এর পরেও। তৃতীয় আর চতুর্থ লাইন ['চাল'] আর ['চাল', 'ডাল'] — দ্বিতীয় ক্রেতার কার্টে প্রথম ক্রেতার চাল! শেষ লাইন ['চাল'] ['ডাল'] — None sentinel-এর কারণে প্রতিটি ডাক নিজের নতুন list পেয়েছে।

ফাঁদটি কখনো কাজেও লাগে: “একবার তৈরি, সব ডাকে ভাগ” আচরণটি ইচ্ছা করে ব্যবহার করে কেউ কেউ ফলাফল মনে রাখার (cache) কাজ করেন — যেমন def fib(n, _memo={}):. Python-এর অফিসিয়াল FAQ-তেও এই কৌশলের উল্লেখ আছে। তবে আজকের Python-এ এর জন্য পরিষ্কার পথ functools.lru_cache বা functools.cache (পর্ব ৩৮)। নিজের কোডে mutable default দেখলে ধরে নিন সেটি বাগ, যদি না নামে বা comment-এ স্পষ্ট লেখা থাকে যে এটি ইচ্ছাকৃত।

৬ · Closure — জন্মস্থানের কথা মনে রাখা ফাংশন

পর্ব ১৩-এ দেখেছিলেন, ফাংশনও একটি object (first-class function) — তাকে অন্য ফাংশনে পাঠানো যায়, আর ফাংশন থেকে ফেরতও দেওয়া যায়। আর এই পর্বের অংশ ১-এ দেখলেন, ফাংশনের ভেতরে আরেকটি ফাংশন লেখা যায় — একে বলে nested function. এবার দুটি মেলাই: একটি ফাংশন যদি ভেতরে আরেকটি ফাংশন বানিয়ে সেটিই ফেরত দেয়?

def make_multiplier(n):
    def multiply(x):
        return x * n      # n ভেতরে তৈরি হয়নি — এসেছে বাইরের ফাংশন থেকে
    return multiply       # বন্ধনী নেই: ফাংশন object নিজে ফেরত

triple = make_multiplier(3)
print(triple(10))         # 30

এখানে একটি ধাঁধা আছে। পর্ব ১৩ বলে, make_multiplier(3) return করলেই তার frame মুছে যায়, সঙ্গে local n-ও। তাহলে পরে triple(10) ডাকলে multiply n-এর মান 3 পায় কোথা থেকে? উত্তর: multiply জন্মের সময়েই বুঝে নিয়েছে যে সে n ব্যবহার করে, আর n-কে সঙ্গে করে নিয়ে গেছে। এমন ফাংশনকে বলে closure — যে ফাংশন তার জন্মস্থানের (Enclosing scope-এর) ভ্যারিয়েবল মনে রাখে, এমনকি সেই জায়গা শেষ হয়ে যাওয়ার পরেও.

ভাবুন, বাড়ি থেকে ঢাকায় পড়তে যাওয়ার সময় মা একটি খামে চিরকুট লিখে সিল করে দিলেন: “প্রতি মাসে ৫০০ টাকা সঞ্চয় করবে”। আপনি বাড়ি ছেড়ে এলেন, বাড়ির দরজা বন্ধ হলো — কিন্তু খামটি আপনার সাথেই আছে। যখনই সিদ্ধান্ত নিতে হয়, খাম খুলে চিরকুটটি পড়েন। Closure ঠিক এমন: ভেতরের ফাংশন হলো আপনি, বাইরের ফাংশন হলো বাড়ি, আর খামের চিরকুট হলো n = 3. বাইরের ফাংশন শেষ হলেও খামটি ফাংশনের সাথে থেকে যায়।

এই “খাম”টি Python-এ সত্যিই আছে। একে বলে cell — একটি ছোট বাক্স, যেটি একটি ভ্যারিয়েবল ধরে রাখে। ভেতরের ফাংশন যে বাইরের নাম ব্যবহার করে (যাকে বলে free variable), Python সেই নামটিকে সাধারণ local হিসেবে না রেখে একটি cell-এ রাখে, আর ভেতরের ফাংশন object সেই cell-এর একটি reference নিজের __closure__-এ রেখে দেয়। ধাপে ধাপে দেখুন:

1def make_multiplier(n):2    def multiply(x):3        return x * n4    return multiply56triple = make_multiplier(3)7print(triple(10))
Stack, cell আর ফাংশন
stack
cell (n)
multiply
triple
  1. বাইরের ফাংশন ডাকা হলো। make_multiplier(3)-এর frame তৈরি হলো, n = 3. Compile-এর সময়েই Python জেনে গেছে যে ভেতরের একটি ফাংশন n ব্যবহার করে, তাই n-কে সাধারণ local হিসেবে না রেখে একটি cell-এ রাখল।
  2. ভেতরের def চলল। লাইন ২-এর def একটি নতুন function object বানাল। বানানোর সময় সে n-এর cell-টির একটি reference নিজের __closure__-এ রেখে দিল — খামটি পকেটে ঢুকল। (Body এখনো চলেনি।)
  3. Return — frame গেল, cell রইল। make_multiplier ফাংশন object-টি ফেরত দিল, আর তার frame মুছে গেল। কিন্তু cell-টি মোছেনি, কারণ multiply এখনো সেটিকে ধরে আছে (পর্ব ০৫-এর reference counting — কেউ ধরে থাকলে object বাঁচে)। triple লেবেলটি লাগল সেই ফাংশনে।
  4. triple(10) ডাকা হলো। multiply-এর নতুন frame, x = 10. লাইন ৩-এ n দরকার: Local-এ নেই, তাই Enclosing — আর Enclosing মানে এখানে নিজের __closure__-এর cell. সেখান থেকে পাওয়া গেল 3.
  5. ফল। 10 × 3 = 30, ছাপা হলো 30. এখন make_multiplier(5) ডাকলে নতুন frame, নতুন cell (n = 5), নতুন ফাংশন — triple-এর cell-এ কোনো প্রভাব নেই। একটি “কারখানা” থেকে যত খুশি আলাদা ফাংশন।

make_multiplier-এর মতো যে ফাংশনের কাজই হলো নতুন ফাংশন বানিয়ে ফেরত দেওয়া, তাকে বলে factory function. এর একটি বাস্তব প্রয়োগ: বিভিন্ন পণ্যে বিভিন্ন হারে VAT. বাংলাদেশে পণ্য বা সেবাভেদে VAT-এর হার আলাদা হতে পারে (এখানে ১৫% আর ৫% শুধু উদাহরণ)। প্রতিটি হারের জন্য আলাদা ফাংশন হাতে লেখার বদলে একটি factory: vat15 = make_vat(0.15), vat5 = make_vat(0.05). হার একবার ঠিক হয়ে closure-এ বন্দি থাকে, তারপর শুধু দাম দিয়ে ডাকলেই হয় — আর কেউ ভুল করে বাইরে থেকে হার বদলাতেও পারে না।

nonlocal দিয়ে অবস্থা ধরে রাখা — একটি counter

অংশ ২-এর ভিজিট counter মনে আছে? global দিয়ে চলত, কিন্তু একটির বেশি counter রাখা যেত না। Closure আর nonlocal দিয়ে এর পরিষ্কার সমাধান: make_counter()-এর ভেতরে count = 0, ভেতরের click() ফাংশন nonlocal count লিখে সেটি বাড়ায়। প্রতিবার make_counter() ডাকলে নতুন cell-এ নতুন count — হোমপেজের জন্য একটি counter, ব্লগের জন্য আরেকটি, কেউ কারও সাথে মেশে না। আর বাইরের কোনো কোড count-এ সরাসরি হাত দিতে পারে না; বাড়ানোর একমাত্র উপায় click() ডাকা। এভাবে তথ্য লুকিয়ে রাখাকে বলে encapsulation — class-এরও মূল উদ্দেশ্য এটাই।

দ্বিতীয় runnable প্রোগ্রামে VAT factory, counter আর পরের অংশের লুপ-ফাঁদ একসাথে:

def make_vat(rate):
    def add_vat(price):
        return round(price * (1 + rate), 2)
    return add_vat

vat15 = make_vat(0.15)
print(vat15(1000), make_vat(0.05)(1000), vat15.__closure__[0].cell_contents)

def make_counter():
    count = 0
    def click():
        nonlocal count
        count += 1
        return count
    return click

visit = make_counter()
print("ভিজিট:", visit(), visit(), visit())

late = [lambda: i for i in range(3)]
fixed = [lambda i=i: i for i in range(3)]
print([f() for f in late], [f() for f in fixed])

আউটপুট তিন লাইনের। প্রথমটি 1150.0 1050.0 0.15 — একই factory থেকে দুটি আলাদা ফাংশন, প্রত্যেকে নিজের হার মনে রেখেছে (দ্বিতীয়টিকে আমরা নাম না দিয়েই সঙ্গে সঙ্গে ডেকেছি)। শেষের 0.15 এসেছে __closure__ থেকে — এটি cell-গুলোর একটি tuple, আর cell_contents দিয়ে খামের ভেতরের চিরকুট সরাসরি দেখা যায়। দ্বিতীয় লাইন ভিজিট: 1 2 3 — তিনবার visit(), আর nonlocal-এর কারণে প্রতিবার একই cell-এর count বেড়েছে; nonlocal লাইনটি মুছে দিলে অংশ ২-এর সেই UnboundLocalError ফিরে আসবে। শেষ লাইন [2, 2, 2] [0, 1, 2] — প্রথম তিনটি ফাংশনই 2 দিল! কেন, তা পরের অংশে। (lambda হলো এক লাইনের নামহীন ফাংশন — lambda: i মানে “কোনো parameter নেয় না, i ফেরত দেয়”; বিস্তারিত পর্ব ৩৮-এ।)

৭ · লুপে closure-এর ফাঁদ — late binding

খামের উপমায় একটি সূক্ষ্ম কথা লুকিয়ে ছিল। Closure খামে মানের ফটোকপি রাখে না, রাখে ভ্যারিয়েবলটিকেই (cell-টিকে)। ফাংশন যখন ডাকা হয়, তখন cell খুলে সেই মুহূর্তের মান পড়ে — বানানোর মুহূর্তের মান নয়। একে বলে late binding. make_multiplier-এ এটা টের পাওয়া যায়নি, কারণ n আর কখনো বদলায়নি। কিন্তু লুপে বানানো closure-এ ভ্যারিয়েবলটি প্রতিবার বদলায়:

1funcs = []2for i in range(3):3    funcs.append(lambda: i)4print([f() for f in funcs])56# সমাধান: lambda i=i: i
একটি i, তিনটি ফাংশন
i
funcs[0]
funcs[1]
funcs[2]
  1. i = 0, প্রথম ফাংশন। লুপের প্রথম ঘুরে একটি lambda তৈরি হয়ে list-এ গেল। সে মনে রাখল “আমি i নামটি ফেরত দেব” — 0 মানটি নয়, i নামটি।
  2. i = 1, দ্বিতীয় ফাংশন। একই i এখন 1. নতুন lambda-ও একই i-কে মনে রাখল। দুটি ফাংশন, কিন্তু তাদের খামে একই ঠিকানা লেখা।
  3. i = 2, তৃতীয় ফাংশন। তিনটি ফাংশন, তিনটিই একই i-এর দিকে তাকিয়ে। এখন পর্যন্ত কেউ ডাকা হয়নি, তাই কেউ টের পায়নি।
  4. লুপ শেষ। লুপ থামল, কিন্তু i নামটি শেষ মান 2 নিয়ে রয়ে গেল। পর্ব ১২-এ দেখেছিলেন, Python-এ লুপের ভ্যারিয়েবল লুপের পরেও থাকে।
  5. ডাকার মুহূর্ত। এখন তিনটি ফাংশন ডাকা হলো। প্রত্যেকে তখনই i পড়ল — আর তখন i = 2. ফল [2, 2, 2], যদিও আপনি আশা করেছিলেন [0, 1, 2].
  6. সমাধান — default argument দিয়ে মান আটকানো। lambda i=i: i লিখুন। অংশ ৫-এর নিয়ম মনে করুন: default হিসাব হয় ফাংশন তৈরির মুহূর্তে. তাই প্রতিটি lambda তৈরির সময় সেই মুহূর্তের i তার নিজের parameter-এর default হয়ে বসে যায় — 0, 1, 2. যে নিয়ম অংশ ৫-এ ফাঁদ ছিল, এখানে সেটাই সমাধান!

এই ফাঁদ শুধু lambda-তে নয়, লুপের ভেতরে def দিয়ে বানানো ফাংশনেও একই রকম। বাস্তবে এটি দেখা যায় GUI-তে বোতাম বানানোর সময় (দশটি বোতাম, সবগুলো চাপলে দশ নম্বরের কাজ হয়), বা লুপে callback নিবন্ধন করার সময়। i=i কৌশল ছাড়াও আরেকটি পরিষ্কার পথ হলো একটি factory function দিয়ে প্রতিবার নতুন scope বানানো — make_multiplier(i)-এর মতো — অথবা functools.partial (পর্ব ৩৮)। i=i-এর একটি ছোট অসুবিধা: কেউ ভুল করে f(5) ডাকলে আটকানো মানের বদলে 5 ফেরত আসে, কারণ i এখন একটি সাধারণ parameter.

৮ · Closure নাকি class? আর কোনটি কখন

Counter-এর উদাহরণে closure দুটি কাজ করল: কিছু অবস্থা (count) ধরে রাখল, আর সেটি বদলানোর একটি আচরণ (click) দিল। এই “অবস্থা + আচরণ” জুটিই আসলে object-oriented programming-এর মূল ভাবনা। একই counter class দিয়েও লেখা যায় — পর্ব ২৯-এ দেখবেন class Counter: আর self.count += 1. তাহলে কোনটি কখন?

পরিস্থিতিভালো পছন্দকেন
একটি মাত্র কাজ, কয়েকটি স্থির সেটিং (VAT হার, গুণক)Closureছোট, এক লাইনে ডাকা যায়, বাড়তি নাম লাগে না
একটি ছোট অবস্থা, একটি আচরণ (counter, id তৈরি)Closure বা class — দুটোই ঠিকরুচির বিষয়; দল যা পড়তে অভ্যস্ত
অনেকগুলো আচরণ (click, reset, report)Classকয়েকটি ফাংশন ফেরত দেওয়া জটিল; method-এ গোছানো সহজ
অবস্থা বাইরে থেকে দেখা বা ডিবাগ করা দরকারClassobj.count সরাসরি দেখা যায়; cell-এর ভেতর দেখা কঠিন
অন্য ফাংশনকে মোড়ানো (সময় মাপা, লগ)Closure (decorator)Python-এর প্রচলিত ধরন — পর্ব ৩৫

একটি পুরোনো রসিকতা আছে প্রোগ্রামারদের মধ্যে: “closure হলো গরিবের object, আর object হলো গরিবের closure”. কথাটির মানে, দুটি একই সমস্যার দুটি সমাধান — ডেটা আর তার ওপর কাজকে একসাথে বেঁধে রাখা। ছোট আর এক-কাজের হলে closure হালকা ও সুন্দর; বড় হলে class পরিষ্কার।

C/C++ জানলে

C-তে scope-এর ধারণা একই রকম lexical, কিন্তু nested ফাংশন (স্ট্যান্ডার্ড C-তে) নেই, তাই closure-ও নেই; ফাংশনের ভেতরে অবস্থা ধরে রাখতে static local ভ্যারিয়েবল ব্যবহার হয়, যা আসলে একটি লুকানো global — একটির বেশি counter বানানো যায় না। C-র variadic ফাংশন (printf-এর মতো, va_list দিয়ে) *args-এর পূর্বপুরুষ, তবে সেখানে argument-এর সংখ্যা ও type জানার কোনো নিরাপদ উপায় নেই। C++11-এর lambda capture ([=] মানে কপি, [&] মানে reference) আপনাকে বেছে নিতে দেয়; Python-এর closure সবসময় reference-এর মতো (late binding). তুলনার জন্য দেখুন C-র scope ও storage class আর C++-এর lambda.

পরীক্ষা ও ইন্টারভিউতে
  • LEGB কী? — Python নাম খোঁজার ক্রম: Local → Enclosing → Global → Built-in; প্রথম যেখানে পায় সেখানেই থামে, কোথাও না পেলে NameError.
  • global আর nonlocal-এর পার্থক্য? — global module-স্তরের নাম বদলাতে দেয়; nonlocal নিকটতম ঘিরে রাখা ফাংশনের নাম বদলাতে দেয়।
  • *args আর **kwargs ফাংশনের ভেতরে কী type? — args একটি tuple, kwargs একটি dict.
  • def f(x, items=[])-এ সমস্যা কী? — Default একবারই (def-এর সময়) তৈরি হয়, তাই সব ডাক একই list ভাগ করে; None দিয়ে ভেতরে নতুন list বানান।
  • Closure কী? — যে ভেতরের ফাংশন বাইরের ফাংশনের ভ্যারিয়েবল মনে রাখে, বাইরের ফাংশন শেষ হওয়ার পরেও; ভ্যারিয়েবলগুলো থাকে __closure__-এর cell-এ।

৯ · AI-এর যুগে — scope আর default-এর ভুল ধরা

AI-এর লেখা কোড সাধারণত চলে — আর এই পর্বের ভুলগুলোর বিপদ ঠিক সেখানেই। Mutable default বা late binding প্রথম পরীক্ষায় ধরা পড়ে না; ধরা পড়ে দ্বিতীয় ডাকে, বা প্রোডাকশনে, যখন দুজন ক্রেতার কার্ট মিশে যায়। AI-এর প্রশিক্ষণের কোডে এই ধাঁচগুলো প্রচুর আছে, তাই AI-ও সেগুলো নকল করে:

AI প্রায়ই যা করেসমস্যাকী চাইবেন
ফাইলের শুরুতে total = 0, ফাংশনে global totalলুকানো অবস্থা; পরীক্ষা কঠিন, দুটি আলাদা হিসাব অসম্ভবমান parameter-এ, ফল return-এ; অবস্থা লাগলে closure বা class
def f(x, cache={}) বা items=[]সব ডাক একই object ভাগ করে; ডেটা মিশে যায়None default আর is None পরীক্ষা
সব ফাংশনে *args, **kwargsআসল ইন্টারফেস লুকায়; বানান ভুল নিঃশব্দে হারায়নাম দেওয়া parameter; keyword-only দরকারমতো
লুপে lambda: i বা callbackসব ফাংশন শেষ মান দেখে (late binding)i=i, factory function, বা functools.partial
list, dict, id, sum নামে ভ্যারিয়েবলBuilt-in ঢাকা পড়ে; পরে অদ্ভুত TypeErrorঅর্থপূর্ণ নাম: items, user_id, total

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

আমি Python 3.12+ ব্যবহার করি। একটি অনলাইন দোকানের জন্য কোড লিখে দাও: (ক) প্রতিটি ক্রেতার আলাদা শপিং-কার্টে পণ্য যোগ করার ফাংশন, (খ) VAT-এর হার দিলে দাম হিসাবের ফাংশন বানিয়ে দেয় এমন একটি factory, (গ) পেজ-ভিজিট গোনার counter, যাতে একাধিক পেজের আলাদা counter রাখা যায়। শর্ত: ১. কোনো default argument-এ list, dict বা set দেবে না; দরকার হলে None আর "is None" পরীক্ষা। ২. global ভ্যারিয়েবল বদলাবে না, global কিওয়ার্ড ব্যবহার করবে না; অবস্থা রাখতে closure (nonlocal) ব্যবহার করো। ৩. *args/**kwargs শুধু যেখানে সত্যিই অন্য ফাংশনে ফরোয়ার্ড করতে হয়; বাকি সব parameter-এর নাম স্পষ্ট লেখো। ৪. লুপের ভেতরে কোনো lambda বা ফাংশন বানালে late binding এড়ানোর ব্যবস্থা করো এবং কেন, তা এক লাইনে বলো। ৫. list, sum, id-এর মতো built-in নামে কোনো ভ্যারিয়েবল রাখবে না। ৬. প্রমাণ হিসেবে assert লিখে দেখাও: দুজন ক্রেতার কার্ট মেশে না, আর দুটি counter একে অপরকে প্রভাবিত করে না।

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

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

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

প্র ০১এত মানুষ যে ফাঁদে পড়েন, Python কেন default মান প্রতিটি ডাকে নতুন করে হিসাব না করে def-এর সময় একবারই হিসাব করে? এটা কি নকশার ভুল?

প্রশ্নটি এত পুরোনো আর এত বারবার ওঠে যে Python-এর অফিসিয়াল Programming FAQ-তে এর জন্য আলাদা একটি প্রশ্ন আছে — “Why are default values shared between objects?” উত্তরটি বুঝতে হলে মনে রাখতে হবে, Python-এ def কোনো ঘোষণা নয়, একটি চালানো statement. ফাইল চলতে চলতে def লাইনে পৌঁছালে Python একটি function object বানায়। Default-এর expression সেই মুহূর্তেই হিসাব হয়, আর ফলটি ফাংশনের __defaults__ tuple-এ রাখা হয় — যেমন def লাইনে অন্য যেকোনো expression হিসাব হয়।

বিকল্প নকশা — প্রতি ডাকে default নতুন করে হিসাব — কেন বেছে নেওয়া হয়নি? কয়েকটি যুক্তি আছে। প্রথমত, কোন scope-এ হিসাব? def f(x=rate * 2)-এ rate কোনটি — def-এর সময়ের, নাকি ডাকার সময়ের? ডাকার সময় হিসাব করলে default-এর অর্থ নির্ভর করবে ডাকার মুহূর্তের global অবস্থার ওপর, যা আরেক ধরনের লুকানো বাগ তৈরি করে। দ্বিতীয়ত, খরচ: প্রতি ডাকে expression হিসাব করা ধীর; একবার হিসাব করে রেখে দেওয়া সহজ ও দ্রুত। তৃতীয়ত, সরলতা: নিয়মটি একটি বাক্যে বলা যায় — “def লাইনের সব কিছু def-এর সময় চলে” — কোনো ব্যতিক্রম নেই।

আর এই আচরণ সবসময় খারাপও নয়। FAQ নিজেই দেখায়, কেউ কেউ ইচ্ছা করে এটি ব্যবহার করেন — যেমন _cache={} default দিয়ে আগের ফল মনে রাখা। অংশ ৭-এ দেখলেন, লুপে lambda i=i: i কৌশলটিও এই নিয়মের ওপর দাঁড়িয়ে।

অন্য ভাষাগুলো কী করে? JavaScript (ES2015 থেকে) আর Ruby default প্রতি ডাকে নতুন করে হিসাব করে, তাই সেখানে এই ফাঁদ নেই। C++-এও default argument প্রতি ডাকে ডাকার জায়গায় হিসাব হয়। অর্থাৎ Python এখানে সংখ্যালঘু, আর এই কারণে পরিবর্তনের প্রস্তাবও এসেছে। ২০২১ সালে Chris Angelico PEP 671-এ “late-bound default”-এর প্রস্তাব দেন — def f(items=>[]) ধরনের নতুন চিহ্ন, যাতে => দিলে default প্রতি ডাকে হিসাব হবে। প্রস্তাবটি নিয়ে অনেক আলোচনা হয়েছে, কিন্তু এই লেখা পর্যন্ত এটি ভাষায় যোগ হয়নি। পুরোনো আচরণ বদলানোর প্রশ্নই ওঠে না — লাখো প্রোগ্রাম এর ওপর নির্ভর করে।

ব্যবহারিক দিক থেকে ruff বা pylint-এর মতো linter mutable default দেখলেই সতর্ক করে (pylint-এর “dangerous-default-value” সতর্কতা), তাই আধুনিক টুল ব্যবহার করলে ফাঁদটি প্রায় সবসময় আগেই ধরা পড়ে।

মূল উপলব্ধি: এটি দুর্ঘটনা নয়, একটি সুসংগত নিয়মের পার্শ্বফল — “def একটি চালানো statement”. নিয়মটি বুঝলে ফাঁদটি আর রহস্য থাকে না, আর একই নিয়ম দিয়ে late binding-এর সমাধানও হয়।

প্র ০২শুরুর দিকের Python-এ “Enclosing” স্তরই ছিল না, আর nonlocal এসেছে অনেক পরে। তখন মানুষ কীভাবে চালাতেন, আর এই বদলগুলো কেন এল?

Python-এর প্রথম দিকের version-গুলোতে (১৯৯০-এর দশকে) নাম খোঁজার নিয়ম ছিল মাত্র তিন স্তরের — local, global আর built-in. কখনো কখনো একে “LGB” নিয়মও বলা হতো। ফাংশনের ভেতরে ফাংশন লেখা যেত, কিন্তু ভেতরের ফাংশন বাইরের ফাংশনের local নাম দেখতে পেত না। লিখলে NameError, কারণ খোঁজা সোজা local থেকে global-এ লাফ দিত।

মানুষ তাহলে কীভাবে closure-এর মতো কিছু বানাতেন? এই পর্বের অংশ ৭-এর সেই কৌশল দিয়ে — default argument. def make_multiplier(n): return lambda x, n=n: x * n. n=n লিখে বাইরের মান ভেতরের ফাংশনের default-এ ঢুকিয়ে দেওয়া হতো। কাজ চলত, কিন্তু কুৎসিত ছিল, আর ফাংশনের signature-এ বাড়তি parameter থেকে যেত।

এর সমাধান আসে PEP 227 — “Statically Nested Scopes” দিয়ে, যার লেখক Jeremy Hylton. Python 2.1 (২০০১)-এ এটি ঐচ্ছিক ছিল — ফাইলের শুরুতে from __future__ import nested_scopes লিখে চালু করতে হতো — আর Python 2.2-তে এটি স্বাভাবিক নিয়ম হয়ে যায়। এই ধাপে ধাপে চালু করার কারণ: কিছু পুরোনো কোড, যেখানে একই নাম বাইরের ফাংশন আর global দুই জায়গাতেই ছিল, নতুন নিয়মে অন্য অর্থ পেত। __future__ import-এর এই কৌশলটিই পরে বড় বদল আনার প্রচলিত পথ হয়ে ওঠে।

কিন্তু PEP 227 শুধু পড়ার সুযোগ দিয়েছিল। ভেতরের ফাংশন বাইরের নামে assign করলে সেটি আগের মতোই local হয়ে যেত — এই পর্বের UnboundLocalError. Python 2-তে counter বানাতে তাই মানুষ একটি mutable পাত্র ব্যবহার করতেন: count = [0], আর ভেতরে count[0] += 1 — নামটিতে assign নয়, list-এর ভেতর বদলানো, তাই নামটি local হয় না। কাজ চলত, কিন্তু উদ্দেশ্য লুকিয়ে যেত।

এর সমাধান PEP 3104 — “Access to Names in Outer Scopes”, লেখক Ka-Ping Yee. কিওয়ার্ডের নাম নিয়ে দীর্ঘ বিতর্ক হয়েছিল — outer, nonlocal সহ নানা প্রস্তাব — শেষে nonlocal বেছে নেওয়া হয়। এটি এসেছে Python 3.0 (২০০৮)-এ। Python 2-তে এটি কখনো যোগ হয়নি, কারণ নতুন কিওয়ার্ড পুরোনো কোড ভাঙতে পারত (কেউ nonlocal নামে ভ্যারিয়েবল রাখলে)।

এই ইতিহাস একটি গুরুত্বপূর্ণ নকশা-দর্শন দেখায়: Python বড় বদল আনে ধীরে, ঐচ্ছিক থেকে বাধ্যতামূলক, আর পুরোনো কোড ভাঙার ঝুঁকি মেপে। Scope-এর নিয়ম তিন স্তর থেকে চার স্তরে যেতে লেগেছে প্রায় এক দশক, আর পুরো লেখার সুবিধা পেতে আরও সাত বছর।

মূল উপলব্ধি: LEGB নিয়মটি শুরু থেকে ছিল না; এটি এসেছে বাস্তব ব্যবহারকারীদের চাহিদা থেকে, দুটি PEP-এর মাধ্যমে। আজকের nonlocal হলো count = [0]-এর মতো কৌশলের একটি সৎ, স্পষ্ট বিকল্প।

প্র ০৩Closure কি Python-এর আবিষ্কার? Lisp আর Scheme-এর ইতিহাসে “lexical scope” আর closure-এর ধারণা কীভাবে এল?

না, closure Python-এর চেয়ে কয়েক দশক পুরোনো। গল্পটি শুরু হয় Lisp দিয়ে, যা John McCarthy ১৯৫৮ সালের দিকে MIT-তে তৈরি করেন। Lisp-এ ফাংশন প্রথম থেকেই first-class — ফাংশন অন্য ফাংশনে পাঠানো ও ফেরত দেওয়া যেত। কিন্তু প্রথম দিকের Lisp implementation-গুলো dynamic scope ব্যবহার করত: একটি free variable-এর মান খোঁজা হতো ডাকার জায়গায় (call stack ধরে), লেখার জায়গায় নয়। ফলে একটি ফাংশন কোথা থেকে ডাকা হচ্ছে তার ওপর তার আচরণ বদলে যেত।

এ থেকে জন্ম নেয় বিখ্যাত “funarg problem” (functional argument problem): একটি ফাংশন যখন অন্য জায়গায় পাঠানো হয় বা ফেরত দেওয়া হয়, তখন তার free variable কোন মান পাবে? যে frame-এ ফাংশনটি জন্মেছিল, সেটি তো হয়তো আর নেই। ১৯৭০ সালের দিকে Joel Moses-এর একটি লেখা এই সমস্যাকে স্পষ্টভাবে ব্যাখ্যা করে।

সমাধানের তাত্ত্বিক ভিত্তি এসেছিল আগেই। ব্রিটিশ কম্পিউটার বিজ্ঞানী Peter Landin ১৯৬৪ সালের একটি গবেষণাপত্রে (“The Mechanical Evaluation of Expressions”) “closure” শব্দটি ব্যবহার করেন — একটি ফাংশনের কোড আর তার জন্মের পরিবেশ (environment) একসাথে বাঁধা একটি জিনিস। ঠিক এই পর্বের “সিল করা খাম”।

ধারণাটিকে একটি ভাষার কেন্দ্রে বসান MIT-এর Gerald Jay Sussman আর Guy L. Steele Jr. — ১৯৭৫ সালে তাঁরা তৈরি করেন Scheme, একটি ছোট Lisp, যেখানে scope সম্পূর্ণ lexical. Scheme দেখিয়ে দেয় যে lexical scope আর closure দিয়ে অবাক করার মতো অনেক কিছু করা যায় — এমনকি object-ও বানানো যায় (ঠিক এই পর্বের counter-এর মতো)। এই বিষয়ে তাঁদের “Lambda: The Ultimate …” নামের গবেষণাপত্রের সিরিজ বিখ্যাত। পরে Common Lisp-ও lexical scope গ্রহণ করে।

সেখান থেকে ধারণাটি ছড়িয়েছে প্রায় সব জায়গায়: ML, Haskell, Smalltalk-এর block, JavaScript (যেখানে closure প্রতিদিনের হাতিয়ার), Ruby, C# আর Java 8-এর lambda, C++11-এর lambda. Python-এ এটি পূর্ণভাবে আসে ২০০১–২০০২ সালে (PEP 227), আর nonlocal দিয়ে সম্পূর্ণ হয় ২০০৮ সালে।

Dynamic scope কি পুরোপুরি হারিয়ে গেছে? না — Emacs Lisp দীর্ঘদিন dynamic scope-ই ব্যবহার করেছে (পরে lexical scope ঐচ্ছিকভাবে যোগ হয়েছে), আর Unix shell-এর কিছু আচরণও এর কাছাকাছি। তবে মূলধারার ভাষাগুলো lexical scope বেছে নিয়েছে, কারণ কোড পড়েই নামের অর্থ বোঝা যায়।

মূল উপলব্ধি: Closure হলো কম্পিউটার বিজ্ঞানের একটি পুরোনো, গভীর ধারণা, যা একটি বাস্তব সমস্যা (funarg problem) থেকে এসেছে। Python এটি ধার করেছে Lisp-পরিবার থেকে — আর lexical scope মানে “কোড যেখানে লেখা, অর্থ সেখান থেকেই”।

প্র ০৪global কিওয়ার্ডকে কেন “code smell” ধরা হয়? Python ভাষায় রেখেছে যখন, তখন কি এর কোনো সঠিক ব্যবহারও আছে?

“Code smell” শব্দটি জনপ্রিয় করেন Kent Beck আর Martin Fowler — Fowler-এর ১৯৯৯ সালের বই Refactoring-এ। এর মানে: কোডের এমন লক্ষণ, যা নিজে ভুল নয়, কিন্তু গভীর কোনো নকশা-সমস্যার ইঙ্গিত দেয়। ফাংশনের ভেতরে global ঠিক এমন লক্ষণ।

Global ভ্যারিয়েবলের বিরুদ্ধে আপত্তি খুবই পুরোনো। ১৯৭৩ সালে William Wulf আর Mary Shaw “Global Variable Considered Harmful” শিরোনামে একটি লেখা প্রকাশ করেন (শিরোনামটি Dijkstra-র বিখ্যাত “Go To Statement Considered Harmful”-এর অনুকরণে)। তাঁদের মূল যুক্তি এখনো খাটে: একটি global যেকোনো জায়গা থেকে পড়া ও বদলানো যায়, তাই প্রোগ্রামের যেকোনো দুটি অংশের মধ্যে একটি অদৃশ্য যোগাযোগ তৈরি হয়। প্রোগ্রাম যত বড়, এমন সম্ভাব্য যোগাযোগ তত বেশি।

বাস্তবে এর ক্ষতিগুলো:

  • পড়া কঠিন: ফাংশনের signature মিথ্যা বলে — def charge(amount) দেখে মনে হয় এটি শুধু amount-এর ওপর নির্ভর করে, অথচ ভেতরে একটি global balance বদলায়।
  • পরীক্ষা কঠিন: টেস্টের ক্রম বদলালে ফল বদলায়; প্রতিটি টেস্টের আগে সব global রিসেট করতে হয়।
  • পুনর্ব্যবহার অসম্ভব: একই কোড দুটি আলাদা ডেটার জন্য একসাথে চালানো যায় না।
  • Concurrency-তে বিপদ: একাধিক thread একই global বদলালে race condition (পর্ব ৪১)।
  • গতি: CPython-এ local নাম পড়া global নাম পড়ার চেয়ে দ্রুত — local থাকে frame-এর একটি নির্দিষ্ট ঘরে, global খুঁজতে হয় dict-এ (যদিও আধুনিক CPython এই খোঁজাকে অনেক দ্রুত করেছে)।

তাহলে Python global রাখল কেন? কারণ কিছু জায়গায় এটি সত্যিই দরকারি বা সবচেয়ে সরল: ছোট স্ক্রিপ্টে একটি “debug চালু/বন্ধ” পতাকা; module-স্তরের একটি cache বা connection একবার তৈরি করে রাখা (“lazy initialization”); শেখার সময় ছোট পরীক্ষা। লক্ষ করুন, global ধ্রুবক (VAT_RATE) পড়তে global লাগে না, আর সেগুলো একেবারেই সমস্যা নয় — সমস্যা হলো বদলানো যায় এমন global state.

বিকল্প কী? সবচেয়ে সহজ: মান parameter-এ নিন, ফল return করুন। অবস্থা ধরে রাখতে হলে closure (এই পর্ব) বা class (পর্ব ২৯)। আর অনেক জায়গায় একই সেটিং লাগলে একটি config object তৈরি করে পাঠিয়ে দিন — একে বলে dependency injection.

মূল উপলব্ধি: global নিষিদ্ধ নয়, কিন্তু প্রতিটি global একটি প্রশ্ন তোলে: “এই অবস্থা কার, আর কে কে একে বদলাতে পারে?” উত্তর যদি “যে কেউ, যেকোনো সময়” হয়, নকশা বদলানোর সময় এসেছে।

প্র ০৫Closure-এর ভ্যারিয়েবল ঠিক কোথায় থাকে? __closure__, “cell” আর bytecode-এর ভেতরে কী ঘটে — আর এটা দিয়ে late binding কীভাবে ব্যাখ্যা হয়?

পর্ব ০৫-এ দেখেছিলেন, Python চালানোর আগে আপনার কোডকে bytecode-এ অনুবাদ করে। এই অনুবাদের একটি ধাপে Python প্রতিটি ফাংশনের জন্য একটি symbol table বানায় — কোন নাম local, কোনটি global, কোনটি বাইরের ফাংশন থেকে আসা। এই সিদ্ধান্ত চালানোর আগেই হয়ে যায়, আর এজন্যই অংশ ২-এর UnboundLocalError ঘটে: Python আগেই ঠিক করে ফেলেছে নামটি local.

যখন symbol table দেখে যে বাইরের ফাংশনের একটি local নাম (যেমন n) ভেতরের কোনো ফাংশন ব্যবহার করে, তখন সেই নামটি বিশেষ চিহ্ন পায়। বাইরের ফাংশনের code object-এ এটি থাকে co_cellvars-এ, আর ভেতরের ফাংশনের code object-এ co_freevars-এ। আপনি নিজেই দেখতে পারেন: triple.__code__.co_freevars দেখাবে ('n',).

চালানোর সময় বাইরের ফাংশন n-কে frame-এর সাধারণ ঘরে না রেখে একটি cell object-এ রাখে — একটি ছোট বাক্স, যার ভেতরে একটি reference. ভেতরের def চলার সময় নতুন function object তৈরি হয়, আর সেই cell-গুলোর একটি tuple তার __closure__-এ বসে যায়। এই পর্বের দ্বিতীয় প্রোগ্রামে vat15.__closure__[0].cell_contents দিয়ে আপনি ঠিক সেই বাক্সের ভেতরটাই দেখেছেন। Python 3.8 থেকে cell-এর type types.CellType নামে প্রকাশ্যে পাওয়া যায়।

Bytecode পর্যায়ে local নাম পড়া হয় এক ধরনের নির্দেশে (LOAD_FAST), global পড়া হয় আরেকটিতে (LOAD_GLOBAL), আর cell-এর মাধ্যমে পড়া হয় তৃতীয় একটিতে (LOAD_DEREF — “dereference”, অর্থাৎ বাক্স খুলে ভেতরের জিনিস আনা)। import dis; dis.dis(triple) চালালে এগুলো নিজের চোখে দেখতে পাবেন। (নির্দেশের সঠিক নাম ও তালিকা version ভেদে কিছুটা বদলায়।)

এবার late binding-এর ব্যাখ্যা একদম পরিষ্কার। [lambda: i for i in range(3)]-এ তিনটি lambda তৈরি হয়, কিন্তু i একটিই ভ্যারিয়েবল — তাই একটিই cell. তিনটি ফাংশনের __closure__ একই cell-এর দিকে তাকিয়ে আছে। Cell তো মানের কপি নয়, একটি বাক্স — লুপ চলার সাথে সাথে বাক্সের ভেতরের জিনিস বদলে 0, 1, 2 হয়েছে। ডাকার সময় তিনজনই বাক্স খুলে দেখে 2. অন্যদিকে lambda i=i: i-এ মানটি cell-এ নয়, প্রতিটি ফাংশনের নিজস্ব __defaults__-এ কপি হয়ে বসে, যা পরে আর বদলায় না।

আর একই কারণে closure মেমরিতে জিনিস ধরে রাখে: যতক্ষণ ভেতরের ফাংশন বেঁচে আছে, cell-ও বেঁচে থাকে, আর cell যে object-কে দেখায় সেটিও। একটি বড় list বা ডেটাবেস connection ভুল করে closure-এ আটকে গেলে সেটি মুক্ত হয় না — একটি সূক্ষ্ম memory leak-এর উৎস।

মূল উপলব্ধি: Closure কোনো জাদু নয় — এটি compile-এর সময়ের একটি সিদ্ধান্ত (কোন নাম free) আর চালানোর সময়ের একটি ছোট বাক্স (cell). বাক্সটি মান নয়, ভ্যারিয়েবল ধরে রাখে — এই একটি তথ্যই closure-এর শক্তি আর late binding-এর ফাঁদ, দুটোই ব্যাখ্যা করে।

অনুশীলন

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

  1. LEGB অনুমান: খাতায় লিখুন কী ছাপা হবে — x = "global"; তারপর def outer(): যার ভেতরে x = "enclosing" আর def inner(): return x, শেষে return inner(). বাইরে print(outer(), x). এরপর ভাবুন: inner-এর ভেতরে প্রথম লাইনে x = "local" যোগ করলে কী বদলাবে?

    প্রথম ক্ষেত্রে ছাপা হবে enclosing global. inner-এর Local-এ x নেই, তাই Enclosing-এ খুঁজে "enclosing" পায়; বাইরের print-এর x হলো global, যা কেউ বদলায়নি। দ্বিতীয় ক্ষেত্রে ছাপা হবে local global — inner নিজের local x পেয়ে সেখানেই থামে। কোনো ক্ষেত্রেই global x বদলায়নি, কারণ ভেতরে assign মানে নতুন local নাম, বাইরের নাম নয়।

  2. Argument বসানো: def order(item, qty=1, *extras, gift=False, **notes). order("চা", 2, "বিস্কুট", "চানাচুর", gift=True, table=5, sugar="কম") ডাকলে প্রতিটি parameter-এ কী বসবে? আর order(qty=3) ডাকলে কী হবে?

    item="চা", qty=2, extras=("বিস্কুট", "চানাচুর") (বাড়তি positional, tuple), gift=True (keyword-only, কারণ *extras-এর পরে), notes={"table": 5, "sugar": "কম"} (নাম না মেলা keyword, dict). order(qty=3) দিলে TypeError: order() missing 1 required positional argument: 'item' — item-এর কোনো default নেই। **notes সব “অচেনা” নাম ধরে, কিন্তু অনুপস্থিত বাধ্যতামূলক argument সে পূরণ করে না।

  3. বাগ খুঁজুন: একটি পরীক্ষার ফল-ব্যবস্থায় লেখা আছে def add_mark(name, mark, record={}):, ভেতরে record[name] = mark আর return record. দুটি আলাদা স্কুলের জন্য s1 = add_mark("রিনা", 85) আর s2 = add_mark("জামিল", 72) ডাকা হলো। s1 ছাপালে কী দেখবেন, কেন, আর ঠিক করবেন কীভাবে?

    s1 ছাপালে দেখবেন {'রিনা': 85, 'জামিল': 72} — দ্বিতীয় স্কুলের ছাত্রও প্রথম স্কুলের রেকর্ডে! কারণ {} default একবারই তৈরি হয়েছে, আর s1, s2 দুটিই সেই একই dict (s1 is s2 হবে True). সমাধান: def add_mark(name, mark, record=None):, আর ভেতরে প্রথমে if record is None: record = {}. এটি dict-এর ক্ষেত্রেও list-এর মতোই ফাঁদ — যেকোনো mutable default-এ।

  4. একটি ছোট factory লিখুন (এবং AI-কে যাচাই করুন): make_discount(percent) লিখুন, যা একটি ফাংশন ফেরত দেয় — সেটি দাম নিয়ে ছাড় বাদ দেওয়া দাম দেয়। তারপর লুপে ১০, ২০, ৩০ শতাংশের তিনটি ছাড়-ফাংশন একটি list-এ বানান, আর প্রতিটিকে ১০০০ টাকায় ডেকে দেখুন। একই কাজ lambda দিয়ে লুপে লিখতে কোনো AI-কে বলুন, আর তার কোডে late binding আছে কি না দেখুন।

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

    def make_discount(percent):
        def apply(price):
            return price * (100 - percent) / 100
        return apply
    
    offers = [make_discount(p) for p in (10, 20, 30)]
    print([f(1000) for f in offers])    # [900.0, 800.0, 700.0]

    এখানে late binding-এর সমস্যা নেই, কারণ প্রতিবার make_discount(p) ডাকায় নতুন frame আর নতুন cell তৈরি হয় — প্রতিটি ফাংশনের নিজের percent. এটাই factory function দিয়ে late binding এড়ানোর পরিষ্কার পথ। AI যদি [lambda price: price * (100 - p) / 100 for p in (10, 20, 30)] লেখে, তিনটিই 700.0 দেবে — সবাই শেষ p (30) দেখবে। ঠিক রূপ: lambda price, p=p: ....

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

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

ক্রমএকটি nested ফাংশনের ভেতরে কোনো নাম পড়লে Python যে ক্রমে খোঁজে, সেই ক্রমে সাজান (▲▼ চেপে সরান)

  1. 🏠 এখনকার ফাংশনের নিজের নাম (Local)
  2. 🏘️ ঘিরে রাখা বাইরের ফাংশনের নাম (Enclosing)
  3. 🏙️ module-স্তরের নাম (Global)
  4. 🇧🇩 Python-এর নিজের নাম, যেমন print, len (Built-in)
  5. ⚠️ কোথাও না পেলে NameError

Python-এ count = 0 লেখার পর def click(): count += 1 সংজ্ঞায়িত করে click() ডাকলে কী ঘটে?

  • global count বেড়ে 1 হয়
  • UnboundLocalError আসে
  • NameError: name 'count' is not defined
  • একটি নতুন local count তৈরি হয়ে 1 হয়, global 0 থাকে

def f(*args, **kwargs): return args, kwargs সংজ্ঞায়িত। f(1, 2, x=3) কী ফেরত দেয়?

  • ([1, 2], {'x': 3})
  • ((1, 2, 3), {})
  • ((1, 2), (('x', 3),))
  • ((1, 2), {'x': 3})

def add(item, bucket=[]): bucket.append(item); return bucket সংজ্ঞায়িত। add(1) আর তারপর add(2) ডাকলে দ্বিতীয় ডাক কী ফেরত দেয়?

  • [1, 2]
  • [2]
  • TypeError, কারণ default-এ list দেওয়া যায় না
  • None

fs = [lambda: i for i in range(3)]-এর পরে [f() for f in fs]-এর মান কী?

  • [0, 1, 2]
  • [0, 0, 0]
  • [2, 2, 2]
  • NameError, কারণ লুপ শেষে i মুছে যায়

একটি nested ফাংশনের ভেতর থেকে বাইরের (ঘিরে রাখা) ফাংশনের একটি local ভ্যারিয়েবলে নতুন মান assign করতে কোন কিওয়ার্ড লাগে?

  • global
  • nonlocal
  • outer
  • static

Python-এ closure বলতে কী বোঝায়?

  • যে ফাংশন ফাইল বা connection বন্ধ করে
  • যেকোনো নামহীন lambda ফাংশন
  • যে ফাংশন শেষ হলে তার সব local ভ্যারিয়েবল মুছে যায়
  • যে ভেতরের ফাংশন তার ঘিরে রাখা scope-এর ভ্যারিয়েবল মনে রাখে, সেই scope শেষ হওয়ার পরেও

শব্দকোষ

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

মনে রাখুন

  1. Python নাম খোঁজে Local → Enclosing → Global → Built-in ক্রমে; প্রথম মিলেই থামে। Scope ঠিক হয় কোড কোথায় লেখা তা দেখে (lexical). list, sum-এর মতো built-in নামে ভ্যারিয়েবল রাখবেন না।
  2. ফাংশনের ভেতরে কোনো নামে assign করলে সেটি পুরো ফাংশনে local — পড়ে-তারপর-বদলালে UnboundLocalError. global/nonlocal আছে, কিন্তু বদলানো যায় এমন global state এড়ান।
  3. *args = বাড়তি positional-এর tuple, **kwargs = বাড়তি keyword-এর dict. ক্রম: a, b=1, *args, c, **kwargs. ডাকার সময় f(*lst, **dct) প্যাকেট খোলে; wrapper এভাবেই argument ফরোয়ার্ড করে।
  4. Default হিসাব হয় একবার, def-এর সময়. তাই mutable default সব ডাকে ভাগ হয়; None default আর if x is None দিয়ে প্রতি ডাকে নতুন object.
  5. Closure = যে ফাংশন জন্মস্থানের ভ্যারিয়েবল cell-এ ধরে রাখে। Factory function আর nonlocal counter এর প্রয়োগ। এটি মান নয়, ভ্যারিয়েবল মনে রাখে — তাই লুপে late binding; সমাধান i=i বা factory.

পরের পর্বে: মডিউল ৩ শেষ! এতদিনের সব হাতিয়ার — ফাংশন, list, dict, set, comprehension, mutability আর closure — একসাথে কাজে লাগিয়ে আপনি বানাবেন একটি সত্যিকারের কমান্ড-লাইন টুল: মাঝপথের প্রজেক্ট.

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

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