स्कोप और म्युटेबल डिफ़ॉल्ट्स
एक नाम कहाँ रहता है और उसे कौन देख सकता है, बाहरी नाम को पढ़ने और लिखने का अंतर, UnboundLocalError, और पायथन का सबसे प्रसिद्ध जाल — डिफ़ॉल्ट के रूप में खाली लिस्ट।
आप कैसे सीखना चाहते हैं?
वह समस्या जिसे हम हल कर रहे हैं
अध्याय 19 की "जब कुछ टूट जाए" लिस्ट में एक लाइन का जवाब बाकी रखा गया था: "फ़ंक्शन के अंदर का वेरिएबल बाहर नहीं मिल रहा — यह नॉर्मल है, क्यों, यह अध्याय 21 का विषय है।"
यह वही अध्याय है।
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 ज़रूर बना था। फिर भी बाहर वह नहीं है। (एरर से पहले पायथन traceback की कुछ लाइनें भी प्रिंट करता है; इस अध्याय में सिर्फ़ आख़िरी लाइन दिखाई गई है।)
यह कोई कमी नहीं, सुरक्षा है। किसी प्रोग्राम में पचास फ़ंक्शन हों और हर एक के अंदर के नाम बाहर फैल जाएँ, तो दो फ़ंक्शन के दो 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 पकड़ा तक नहीं गया।
वजह अध्याय 12 वाली ही है: 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 पूरी तरह अलग हैं।
चरण 4 / 6 — अनुमान
अपनी समझ की जाँच करें
फ़ंक्शन के अंदर count में वैल्यू रखी गई है। दोनों लाइनें क्या प्रिंट करेंगी?
count = 10
def bump():
count = 99
return count
print(bump())
print(count)- A10 10
- B99 10
- Cएक `UnboundLocalError`
- D99 99
कोई return नहीं है, फिर भी बाहर की लिस्ट प्रिंट हो रही है। क्या दिखेगा?
def add_item(items):
items.append("pen")
basket = []
add_item(basket)
add_item(basket)
print(basket)- A[]
- B['pen']
- CNone
- D['pen', 'pen']
दो अलग कॉल, डिफ़ॉल्ट के रूप में एक ख़ाली लिस्ट। क्या प्रिंट होगा?
def collect(item, box=[]):
box.append(item)
return box
print(collect("pen"))
print(collect("bag"))- A['pen', 'bag'] ['pen', 'bag']
- B['pen'] ['bag']
- C['pen'] ['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प्रिंट करें। कौन-सी वैल्यू दिख रही है?
तीनों सवालों के जवाब मिलकर इस अध्याय का पूरा विषय पकड़ लेते हैं: कौन-सा नाम कहाँ रहता है, और कौन-सा बदलाव बाहर तक पहुँचता है।
चरण 6 / 6
चुनौती — अध्याय की क्विज़
सरल से कठिन — दस प्रश्न, अंतिम वाले जानबूझकर चुनौतीपूर्ण बनाए गए हैं।
क्विज़ देने के लिए साइन इन करें