अध्याय 21

स्कोप और म्युटेबल डिफ़ॉल्ट्स

एक नाम कहाँ रहता है और उसे कौन देख सकता है, बाहरी नाम को पढ़ने और लिखने का अंतर, UnboundLocalError, और पायथन का सबसे प्रसिद्ध जाल — डिफ़ॉल्ट के रूप में खाली लिस्ट।

34 मिनटPython 3.12
  1. 1समस्या
  2. 2समझें
  3. 3उदाहरण
  4. 4अनुमान
  5. 5स्वयं करें
  6. 6चुनौती

वह समस्या जिसे हम हल कर रहे हैं

अध्याय 19 के "जब यह टूटता है" खंड में एक पंक्ति अधूरी रह गई थी: "फ़ंक्शन के अंदर का वेरिएबल बाहर दिखाई नहीं देता — यह अपेक्षित है, और इसका कारण अध्याय 21 का विषय है।"

यह वही अध्याय है।

python
def add_tax(price):
    tax = price * 0.15
    return price + tax


print(add_tax(100))
print(tax)
text
115.0
NameError: name 'tax' is not defined. Did you mean: 'max'?

पहली पंक्ति ने काम किया, इसलिए tax अस्तित्व में रहा होगा। और बाहर, यह मौजूद नहीं है।

यह एक सीमा नहीं बल्कि सुरक्षा है। पचास फ़ंक्शंस वाले प्रोग्राम में, यदि प्रत्येक आंतरिक नाम बाहर लीक हो जाता, तो दो फ़ंक्शंस के total वेरिएबल एक-दूसरे को ओवरराइट कर देते — और किसी को पता भी नहीं चलता। फ़ंक्शन के अंदर के नाम अंदर ही रहते हैं, इसलिए एक फ़ंक्शन लिखने के लिए अन्य उनचास के नामों को जानने की आवश्यकता नहीं होती है।

यह अध्याय उसी नियम के बारे में है, और इसका एक परिणाम पायथन का सबसे प्रसिद्ध जाल है।

इस मिशन के अंत तक आप सीखेंगे

  • यह बताना कि कोई नाम कहाँ "रहता है" और उसे कौन देख सकता है
  • फ़ंक्शन के अंदर से किसी बाहरी नाम को पढ़ना, और यह समझना कि आप बाहरी नाम में लिख क्यों नहीं सकते
  • UnboundLocalError को पढ़ना और यह समझना कि क्या हुआ
  • यह समझाना कि फ़ंक्शन में पास की गई सूची बाहर बदली हुई क्यों वापस आ सकती है
  • पायथन के सबसे प्रसिद्ध जाल को पहचानना और उससे बचना: म्युटेबल डिफ़ॉल्ट (mutable default)

पूर्वापेक्षाएँ: फ़ंक्शन के तर्क.


अंदर की चीज़ अंदर रहती है

फ़ंक्शन के अंदर बनाया गया प्रत्येक नाम — जिसमें पैरामीटर भी शामिल हैं — केवल उस कॉल के लिए जीवित रहता है। जब फ़ंक्शन समाप्त होता है, तो वे नाम समाप्त हो जाते हैं।

इसे स्कोप (Scope) कहा जाता है: वह क्षेत्र जिसमें कोई नाम पहचाना जाता है।

किसी बाहरी नाम को पढ़ना काम करता है

python
RATE = 0.15


def add_tax(price):
    return price + price * RATE


print(add_tax(100))
text
115.0

फ़ंक्शन के अंदर कोई RATE नहीं है, इसलिए पायथन बाहर देखता है और उसे एक मिल जाता है। अंदर से किसी बाहरी नाम को पढ़ना पूरी तरह ठीक है।

सभी बड़े अक्षरों वाला नाम (ALL_CAPS) एक परंपरा है — यह पाठक को बताता है "यह एक स्थिरांक (constant) है, यह बदलता नहीं है"।

लेकिन बाहरी नाम में लिखना काम नहीं करता

python
count = 10


def bump():
    count = 99
    return count


print(bump())
print(count)
text
99
10

आंतरिक count = 99 ने कभी भी बाहरी count को नहीं छुआ। इसने एक नया, स्थानीय (local) नाम बनाया जो केवल फ़ंक्शन के अंदर रहता है और उस दौरान बाहरी नाम को छुपा देता है।

और इसी से एक अजीब सी दिखने वाली त्रुटि सामने आती है:

python
count = 10


def bump():
    count = count + 1
    return count


print(bump())
text
UnboundLocalError: cannot access local variable 'count' where it is not associated with a value

पंक्ति ऐसी लगती है जैसे यह बाहरी count लेती है और एक जोड़ती है। लेकिन पायथन फ़ंक्शन को चलाने से पहले उसका पूरा निरीक्षण करता है, देखता है कि कुछ count को असाइन कर रहा है, और निर्णय लेता है कि count शुरू से अंत तक इस फ़ंक्शन के लिए लोकल है।

फिर, दाईं ओर count + 1 का मूल्यांकन करते समय, यह लोकल count की तलाश करता है, जिसे अभी तक कोई मान नहीं दिया गया है — और रुक जाता है।

इसका उपाय लगभग हमेशा एक ही होता है: मान को एक तर्क के रूप में लें और परिणाम को return करें।

text
def bump(count):
    return count + 1

count = bump(count)

लेकिन जो वस्तु आपने पास की है वह बदल सकती है

स्कोप नामों (names) की रक्षा करता है, वस्तुओं (objects) की नहीं।

python
def add_item(items):
    items.append("pen")
    return items


basket = []
add_item(basket)
add_item(basket)

print(basket)
text
['pen', 'pen']

बाहरी basket बदल गया, भले ही कोई return संग्रहीत न किया गया हो।

कारण अध्याय 12 वाला ही है: items नाम नया है, लेकिन यह उसी सूची की ओर इशारा करता है। append उस सूची को संशोधित करता है, और दोनों नाम परिवर्तन दिखाते हैं।

तुलना करें:

python
def replace(items):
    items = ["new"]
    return items


basket = ["old"]
result = replace(basket)

print(basket)
print(result)
text
['old']
['new']

यहाँ items = [...] ने एक नया मान असाइन किया, लोकल नाम को एक अलग सूची की ओर मोड़ दिया। बाहरी basket वहीं है जहाँ वह था।

एक पंक्ति में नियम: वस्तु को संशोधित करना (append, sort, [i] =) बाहर दिखाई देता है; नाम को किसी अन्य वस्तु की ओर इंगित करना (=) बाहर दिखाई नहीं देता।

पायथन का सबसे प्रसिद्ध जाल

python
def add_item(item, basket=[]):
    basket.append(item)
    return basket


print(add_item("pen"))
print(add_item("bag"))
print(add_item("ink"))
text
['pen']
['pen', 'bag']
['pen', 'bag', 'ink']

तीन अलग-अलग कॉल्स, और प्रत्येक कॉल वह रखती है जो पिछली वाली ने छोड़ा था।

कारण को समझें: डिफ़ॉल्ट मान का मूल्यांकन केवल एक बार किया जाता है, जब def पंक्ति चलती है — तब नहीं जब फ़ंक्शन को कॉल किया जाता है। वह खाली सूची फ़ंक्शन से जुड़ी एक ही सूची है, और प्रत्येक कॉल इसे साझा करती है।

समाधान सरल और पारंपरिक है:

python
def add_item(item, basket=None):
    if basket is None:
        basket = []
    basket.append(item)
    return basket


print(add_item("pen"))
print(add_item("bag"))
text
['pen']
['bag']

None अपरिवर्तनीय है, इसलिए इसे साझा करने में कोई हर्ज नहीं है, और नई सूची प्रत्येक कॉल पर ताज़ा बनाई जाती है।

एक नियम के रूप में: कभी भी डिफ़ॉल्ट मान के रूप में लिस्ट, डिक्शनरी या सेट न लिखें।


एक संपूर्ण उदाहरण

basket.py:

python
# एक नाम कहाँ रहता है यह तय करता है कि उसे कौन देख सकता है और कौन बदल सकता है
TAX_RATE = 0.15


def line_total(price, quantity):
    """Pure: मान लेता है, मान लौटाता है, बाहर कुछ भी नहीं छूता।"""
    return round(price * quantity * (1 + TAX_RATE), 2)


def add_line(basket, name, price, quantity):
    """जानबूझकर impure: यह उस बास्केट को बदलता है जो इसे सौंपी गई थी।"""
    basket.append({"name": name, "total": line_total(price, quantity)})
    return basket


def summarise(basket, note=None):
    """डिफ़ॉल्ट के रूप में `None`, ताकि कॉल्स के बीच कोई सूची साझा न हो।"""
    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")
text
pen       51.75
bag      977.50

morning order
2 lines, 1029.25 in total

basket outside the function: 2 lines

जब यह टूटता है

NameError: name 'tax' is not defined फ़ंक्शन के अंदर के नाम का बाहर उपयोग किया गया था। बाहर जो भी चाहिए उसे return करें।

UnboundLocalError: cannot access local variable ... where it is not associated with a value फ़ंक्शन के अंदर एक नाम को असाइन किया गया है और उस असाइनमेंट से पहले पढ़ा गया है। मान को एक तर्क के रूप में लें और परिणाम को return करें।

फ़ंक्शन में पास करने के बाद मेरी लिस्ट बदल गई यह अपेक्षित व्यवहार है — नाम नया है, लिस्ट वही है। इससे बचने के लिए फ़ंक्शन के शीर्ष पर items = items.copy() लिखें।

फ़ंक्शन अपने पिछले परिणामों को याद रखता है म्युटेबल डिफ़ॉल्ट। def f(x, acc=[]) नहीं बल्कि def f(x, acc=None) लिखें, अंदर if acc is None: acc = [] के साथ।