স্কোপ আর পরিবর্তনীয় ডিফল্ট
একটি নাম কোথায় বাস করে আর কে তাকে দেখতে পায়, বাইরের নাম পড়া ও লেখার পার্থক্য, UnboundLocalError, আর পাইথনের সবচেয়ে পরিচিত ফাঁদ — ডিফল্ট হিসেবে খালি লিস্ট।
- 1সমস্যা
- 2বোঝা
- 3উদাহরণ
- 4অনুমান
- 5নিজে করা
- 6কঠিন করা
যে সমস্যাটা আমরা সমাধান করছি
উনিশতম অধ্যায়ের «ভাঙা অবস্থা» তালিকায় একটি লাইন অমীমাংসিত রেখে দেওয়া হয়েছিল: «ফাংশনের ভেতরের ভেরিয়েবল বাইরে পাওয়া যাচ্ছে না — সেটাই স্বাভাবিক, কেন তা একবিংশ অধ্যায়ের বিষয়।»
এটি সেই অধ্যায়।
def add_tax(price):
tax = price * 0.15
return price + tax
print(add_tax(100))
print(tax)115.0
NameError: name 'tax' is not defined. Did you mean: 'max'?প্রথম লাইনটি কাজ করেছে, তাই tax নিশ্চয়ই তৈরি হয়েছিল। তবু বাইরে সে নেই।
এটি সীমাবদ্ধতা নয়, সুরক্ষা। একটি প্রোগ্রামে পঞ্চাশটি ফাংশন থাকলে আর প্রত্যেকের ভেতরের নাম বাইরে ছড়িয়ে পড়লে, দুটি ফাংশনের দুটি total একে অপরকে মুছে দিত — আর কেউ টেরও পেত না। ফাংশনের ভেতরের নাম ভেতরেই থাকে, তাই একটি ফাংশন লেখার সময় বাকি ঊনপঞ্চাশটির নাম নিয়ে ভাবতে হয় না।
আজকের অধ্যায় সেই নিয়মটি, আর তার একটি পরিণতি যা পাইথনের সবচেয়ে বিখ্যাত ফাঁদ।
এই অধ্যায় শেষে আপনি পারবেন
- বলতে একটি নাম কোথায় «বাঁচে» আর কে তাকে দেখতে পায়
- ফাংশনের ভেতর থেকে বাইরের নাম পড়তে, আর বলতে কেন লেখা যায় না
UnboundLocalErrorপড়ে বুঝতে কী ঘটেছে- বলতে কেন একটি লিস্ট ফাংশনে পাঠালে সেটি বদলে যেতে পারে
- পাইথনের সবচেয়ে বিখ্যাত ফাঁদটি চিনতে ও এড়াতে: পরিবর্তনীয় ডিফল্ট
আগে যা জানা লাগবে: ফাংশনের আর্গুমেন্ট।
ভেতরের নাম ভেতরেই
ফাংশনের ভেতরে তৈরি প্রতিটি নাম — প্যারামিটারসহ — কেবল সেই কলটুকুর জন্য বাঁচে। ফাংশন শেষ হলে নামগুলো মুছে যায়।
একেই বলে স্কোপ: একটি নাম কোন এলাকায় পরিচিত।
বাইরের নাম পড়া যায়
RATE = 0.15
def add_tax(price):
return price + price * RATE
print(add_tax(100))115.0ফাংশনের ভেতরে RATE নেই, তাই পাইথন বাইরে খোঁজে আর পেয়ে যায়। ভেতর থেকে বাইরের নাম পড়া চলে।
সব বড় হাতের অক্ষরের নামটি একটি রীতি, পাইথনের নিয়ম নয় — এটি পড়ার মানুষকে বলে «এটি একটি ধ্রুবক, বদলায় না»। এভাবে ব্যবহার করাই বাইরের নামের সবচেয়ে নিরাপদ ব্যবহার।
কিন্তু লেখা যায় না
count = 10
def bump():
count = 99
return count
print(bump())
print(count)99
10ভেতরের count = 99 বাইরের count-কে ছোঁয়নি। সে একটি নতুন, স্থানীয় নাম তৈরি করেছে যা কেবল ফাংশনের ভেতরে বাঁচে, আর বাইরেরটিকে ঢেকে রেখেছে।
এটিই নিয়ম, আর ইচ্ছাকৃত: একটি ফাংশন দুর্ঘটনাক্রমে বাইরের কিছু বদলে দিতে পারে না।
আর এখান থেকেই একটি অদ্ভুত ত্রুটি আসে:
count = 10
def bump():
count = count + 1
return count
print(bump())UnboundLocalError: cannot access local variable 'count' where it is not associated with a valueলাইনটি পড়ে মনে হয় বাইরের count থেকে পড়ে তাতে এক যোগ করা হচ্ছে। কিন্তু পাইথন ফাংশনটি চালানোর আগেই পুরোটা দেখে নেয়, আর দেখে যে count-এ কোথাও মান বসানো হচ্ছে — তাই সে সিদ্ধান্ত নেয় count এই ফাংশনের একটি স্থানীয় নাম, শুরু থেকে শেষ পর্যন্ত।
এরপর ডান পাশের count + 1 চালাতে গিয়ে সে স্থানীয় count খোঁজে, যেটিতে তখনো কিছু বসানো হয়নি — আর থেমে যায়।
বার্তাটি তাই আশ্চর্যরকম সঠিক: নামটি স্থানীয়, কিন্তু তার সাথে এখনো কোনো মান জোড়া লাগেনি।
সমাধান প্রায় সবসময় একই, আর সেটি global নয়: মানটি আর্গুমেন্ট হিসেবে নিন আর ফল return করুন।
def bump(count):
return count + 1
count = bump(count)global শব্দটি পাইথনে আছে, কিন্তু এই কোর্সে সেটি শেখানো হচ্ছে না, কারণ যে সমস্যার সমাধান হিসেবে মানুষ সেটি ব্যবহার করে সেটির ভালো সমাধান আছে। যে ফাংশন বাইরের অবস্থা বদলায়, তার আচরণ কল করার জায়গা দেখে বোঝা যায় না — আর সেটাই পরীক্ষা করা ও বিশ্বাস করা কঠিন কোডের সংজ্ঞা।
পাঠানো জিনিসটা কিন্তু বদলাতে পারে
স্কোপ নাম রক্ষা করে, জিনিস নয়। এই পার্থক্যটা সূক্ষ্ম এবং গুরুত্বপূর্ণ।
def add_item(items):
items.append("pen")
return items
basket = []
add_item(basket)
add_item(basket)
print(basket)['pen', 'pen']বাইরের basket বদলে গেছে, যদিও কোনো return ধরা হয়নি।
কারণটা দ্বাদশ অধ্যায়ের সেই একই কথা: items নামটি নতুন, কিন্তু সে সেই একই লিস্টটির দিকে দেখাচ্ছে। append লিস্টটাকেই বদলায়, আর দুটি নাম দিয়েই সেই বদল দেখা যায়।
তুলনা করুন:
def replace(items):
items = ["new"]
return items
basket = ["old"]
result = replace(basket)
print(basket)
print(result)['old']
['new']এখানে items = [...] একটি নতুন মান বসিয়েছে, অর্থাৎ স্থানীয় নামটিকে অন্য একটি লিস্টের দিকে ঘুরিয়ে দিয়েছে। বাইরের basket যেখানে ছিল সেখানেই।
নিয়মটি এক লাইনে: জিনিসটাকে বদলালে (append, sort, [i] =) বাইরেও দেখা যায়; নামটিকে অন্য জিনিসে বসালে (=) দেখা যায় না।
পাইথনের সবচেয়ে বিখ্যাত ফাঁদ
def add_item(item, basket=[]):
basket.append(item)
return basket
print(add_item("pen"))
print(add_item("bag"))
print(add_item("ink"))['pen']
['pen', 'bag']
['pen', 'bag', 'ink']তিনবার আলাদাভাবে ডাকা হয়েছে, আর প্রতিবার আগেরগুলোও রয়ে গেছে।
কারণটা বুঝলে আর কখনো ভুলবেন না: ডিফল্ট মানটি একবারই হিসাব হয় — যখন def লাইনটি চলে, ফাংশনটি যখন কল হয় তখন নয়। অর্থাৎ ওই খালি লিস্টটি ফাংশনের সাথে জুড়ে থাকা একটিমাত্র লিস্ট, আর প্রতিটি কল সেটিকেই ভাগ করে নিচ্ছে।
সমাধানটি প্রচলিত ও সরল:
def add_item(item, basket=None):
if basket is None:
basket = []
basket.append(item)
return basket
print(add_item("pen"))
print(add_item("bag"))['pen']
['bag']None অপরিবর্তনীয়, তাই ভাগ করে নেওয়ায় কোনো ক্ষতি নেই, আর নতুন লিস্টটি প্রতিটি কলে নতুন করে তৈরি হয়।
নিয়ম হিসেবে: ডিফল্ট মান হিসেবে কখনো লিস্ট, ডিকশনারি বা সেট লিখবেন না। সংখ্যা, লেখা, True/False, None — এগুলো নিরাপদ, কারণ এদের বদলানোই যায় না।
is Noneকেন,== Noneনয়?isজিজ্ঞেস করে «এটি কি ঠিক ওই জিনিসটাই?», আরNoneগোটা প্রোগ্রামে একটিই।==সমতা যাচাই করে, যা কিছু ক্ষেত্রে অন্যরকম আচরণ করতে পারে।None-এর বেলায়isলেখাই প্রচলিত ও নির্ভরযোগ্য।
একটা সম্পূর্ণ উদাহরণ
basket.py:
# Where a name lives decides who can see it and who can change it
TAX_RATE = 0.15
def line_total(price, quantity):
"""Pure: takes values, returns a value, touches nothing outside."""
return round(price * quantity * (1 + TAX_RATE), 2)
def add_line(basket, name, price, quantity):
"""Impure on purpose: it changes the basket it was handed."""
basket.append({"name": name, "total": line_total(price, quantity)})
return basket
def summarise(basket, note=None):
"""`None` as the default, so no list or dict is shared between calls."""
notes = [] if note is None else [note]
total = sum(line["total"] for line in basket)
notes.append(f"{len(basket)} lines, {total:.2f} in total")
return notes
shopping = []
add_line(shopping, "pen", 15.0, 3)
add_line(shopping, "bag", 850.0, 1)
for line in shopping:
print(f"{line['name']:<6} {line['total']:>8.2f}")
print()
for note in summarise(shopping, "morning order"):
print(note)
print()
print("basket outside the function:", len(shopping), "lines")pen 51.75
bag 977.50
morning order
2 lines, 1029.25 in total
basket outside the function: 2 linesতিনটি ফাংশন, তিনটি আলাদা সম্পর্ক বাইরের জগতের সাথে — আর সেটাই এখানে দেখার জিনিস।
line_total কিছুই ছোঁয় না। সে মান নেয়, মান ফেরত দেয়। TAX_RATE পড়ে, কিন্তু বদলায় না। এই ধরনের ফাংশনকে বলা হয় pure — একই ইনপুটে সবসময় একই আউটপুট, আর কোথাও কোনো পার্শ্বপ্রতিক্রিয়া নেই। পরীক্ষা করা সবচেয়ে সহজ, আর ভুল হওয়ার সুযোগও সবচেয়ে কম।
add_line ইচ্ছে করেই বাইরের জিনিস বদলায়। তাকে একটি লিস্ট দেওয়া হয়, আর সে সেটিতে যোগ করে — শেষ লাইনটি প্রমাণ করছে বাইরের shopping-এ সত্যিই দুটি সারি আছে। এটি দোষের নয়, যতক্ষণ নামটি সেটা বলে দেয়। add_line নামটা পড়লেই বোঝা যায় কিছু একটা যোগ হবে।
summarise ডিফল্ট হিসেবে None ব্যবহার করে। note=[] লিখলে সব কল একটি লিস্ট ভাগ করে নিত, আর দ্বিতীয় রিপোর্টে প্রথমটির নোটও দেখা যেত।
কিছু ভাঙা অবস্থা ও তার সমাধান
NameError: name 'tax' is not defined ফাংশনের ভেতরের নাম বাইরে ব্যবহার করা হয়েছে। যা বাইরে দরকার তা return করুন।
UnboundLocalError: cannot access local variable ... where it is not associated with a value ফাংশনের ভেতরে একটি নামে মান বসানো হয়েছে এবং সেই একই নাম মান বসানোর আগে পড়াও হয়েছে। পাইথন নামটিকে স্থানীয় ধরে নিয়েছে। মানটি আর্গুমেন্ট হিসেবে নিন আর ফল return করুন।
ফাংশনে পাঠানোর পর আমার লিস্টটা বদলে গেছে এটাই প্রত্যাশিত আচরণ — নামটি নতুন, লিস্টটি সেই একই। বদলাতে না চাইলে ফাংশনের ভেতরে items = items.copy() লিখে নিন, অথবা নতুন একটি লিস্ট বানিয়ে return করুন।
ফাংশনটি প্রতিবার আগের ফলও মনে রাখছে পরিবর্তনীয় ডিফল্ট। def f(x, acc=[]) নয়, def f(x, acc=None) — আর ভেতরে if acc is None: acc = []।
দুটি ফাংশনে একই নাম ব্যবহার করেছি, সমস্যা হচ্ছে কি? হচ্ছে না, আর সেটাই স্কোপের পুরো উদ্দেশ্য। দুটি ফাংশনের দুটি total সম্পূর্ণ আলাদা।
ধাপ ৪ / ৬ — অনুমান
যাচাই করুন
ফাংশনের ভিতরে count-এ মান বসানো হয়েছে। দুটি লাইন কী ছাপবে?
count = 10
def bump():
count = 99
return count
print(bump())
print(count)- A99 10
- B99 99
- C10 10
- Dএকটি `UnboundLocalError`
কোনো return নেই, তবু বাইরের লিস্টটি ছাপা হচ্ছে। কী দেখা যাবে?
def add_item(items):
items.append("pen")
basket = []
add_item(basket)
add_item(basket)
print(basket)- A['pen', 'pen']
- B[]
- C['pen']
- DNone
দুটি আলাদা কল, ডিফল্ট হিসেবে একটি খালি লিস্ট। কী ছাপা হবে?
def collect(item, box=[]):
box.append(item)
return box
print(collect("pen"))
print(collect("bag"))- A['pen'] ['pen', 'bag']
- B['pen'] ['bag']
- C['pen', 'bag'] ['pen', 'bag']
- D['pen'] ['pen']
উত্তর দিতে অ্যাকাউন্ট লাগবে
উত্তর মিলিয়ে দেখতে সাইন ইন করুন
প্রশ্নগুলো উপরে আছে, আর মাথায় মাথায় উত্তর ভেবে নেওয়াই আসল কাজ। সঠিক উত্তর, ব্যাখ্যা আর তিন ধাপের ইঙ্গিত দেখতে সাইন ইন করুন।
নিজে করুন
cart.py নামে একটি ফাইল লিখুন, যাতে তিনটি ফাংশন থাকবে:
line_total(price, quantity, tax_rate=0.15)— pure, কিছু ছোঁবে নাadd_to(cart, name, price, quantity)— যে কার্টটি দেওয়া হয়েছে তাতে যোগ করবেreceipt(cart, header=None)— লাইনের একটি লিস্ট ফেরত দেবে,headerনা দিলে নিজে একটি বানিয়ে নেবে
তারপর একটি খালি কার্ট বানিয়ে কয়েকটি জিনিস যোগ করুন আর রসিদটি ছাপান।
শেষে তিনটি পরীক্ষা, আর তিনটিই নিজের চোখে দেখা জরুরি:
receipt-এর ডিফল্টটিheader=[]বানিয়ে দিন, তারপর ফাংশনটি দুবার ডেকে দুটি ফলই ছাপান। দ্বিতীয়বার কী দেখছেন?add_to-এর ভেতরে প্রথম লাইনেcart = cart.copy()লিখে দিন, তারপর বাইরের কার্টটি ছাপান। কয়টি জিনিস আছে, আর কেন?- একটি ফাংশনের ভেতরে
TAX_RATE = 0.2লিখুন, তারপর ফাংশনের বাইরেTAX_RATEছাপান। কোন মানটি দেখা যাচ্ছে?
তিনটি প্রশ্নের উত্তর একসাথে এই অধ্যায়ের পুরো বিষয়টা ধরে ফেলে: কোন নাম কোথায় বাঁচে, আর কোন বদলটা বাইরে পৌঁছায়।
ধাপ ৬ / ৬
কঠিন করা — অধ্যায়ের কুইজ
সহজ থেকে কঠিন — দশটি প্রশ্ন, শেষেরগুলো ইচ্ছে করেই কঠিন।
সাইন ইন করে কুইজ দিন