क्या (लैम्ब्डा) फ़ंक्शन क्लोजर कैप्चर करते हैं?


249

हाल ही में मैंने पायथन के साथ खेलना शुरू किया और मैं काम को बंद करने के तरीके में कुछ अजीबों के आसपास आया। निम्नलिखित कोड पर विचार करें:

adders=[0,1,2,3]

for i in [0,1,2,3]:
   adders[i]=lambda a: i+a

print adders[1](3)

यह एक सरल इनपुट बनाता है जो एकल इनपुट लेता है और उस इनपुट को किसी संख्या द्वारा जोड़ा जाता है। फंक्शंस का निर्माण forलूप में किया जाता है, जहां से इटरेटर iचलता 0है 3। इनमें से प्रत्येक संख्या के लिए एक lambdaफंक्शन बनाया जाता है जो कैप्चर करता है iऔर इसे फंक्शन के इनपुट में जोड़ता है। अंतिम पंक्ति एक पैरामीटर के रूप में दूसरे lambdaफ़ंक्शन को कॉल करती है 3। मेरे आश्चर्य के लिए आउटपुट था 6

मैं एक उम्मीद 4। मेरा तर्क यह था: अजगर में सब कुछ एक वस्तु है और इस प्रकार हर चर इसके लिए एक सूचक है। इसके लिए lambdaक्लोजर बनाते समय i, मुझे उम्मीद थी कि वर्तमान में इंगित किए गए पूर्णांक ऑब्जेक्ट के लिए एक पॉइंटर को स्टोर करना होगा i। इसका मतलब है कि जब iएक नया पूर्णांक ऑब्जेक्ट सौंपा गया है तो यह पहले से निर्मित क्लोजर को प्रभावित नहीं करना चाहिए। अफसोस की बात है, addersएक डिबगर के भीतर सरणी का निरीक्षण करना दर्शाता है कि यह करता है। सभी lambdaकार्यों का अंतिम मान को देखें i, 3में है, जो परिणाम adders[1](3)लौटने 6

जो मुझे निम्नलिखित के बारे में आश्चर्यचकित करते हैं:

  • क्लोज़र वास्तव में क्या कैप्चर करते हैं?
  • lambdaवर्तमान मूल्य को iइस तरह से कैप्चर करने के लिए फ़ंक्शंस को समझाने का सबसे सुरुचिपूर्ण तरीका क्या है जो iइसके मूल्य को बदलने पर प्रभावित नहीं होगा ?

35
मुझे UI कोड में यह समस्या आई है। मुझे पागल कर दिया। चाल यह याद रखना है कि लूप नए दायरे नहीं बनाते हैं।
detly

3
@TimMB नेमस्पेस कैसे iछोड़ता है?
detly

3
@ अच्छी तरह से मैं यह कहने जा रहा था कि print iलूप के बाद काम नहीं करेगा। लेकिन मैंने इसे अपने लिए परीक्षण किया और अब मैं देखता हूं कि आपका क्या मतलब है - यह काम करता है। मुझे नहीं पता था कि अजगर में लूप बॉडी के बाद लूप वैरिएबल लिंचिंग करता है।
टिम एमबी

1
@ टिम - हाँ, यही मेरा मतलब है। के लिए एक ही if, with, tryआदि
detly

13
यह आधिकारिक पायथन एफएक्यू में है, क्यों विभिन्न मूल्यों के साथ एक लूप में परिभाषित लंबोदा सभी एक ही परिणाम लौटाते हैं? एक स्पष्टीकरण और सामान्य वर्कअराउंड दोनों के साथ।
अपहरण

जवाबों:


161

आपके दूसरे प्रश्न का उत्तर दिया गया है, लेकिन आपके पहले प्रश्न के लिए:

क्लोजर वास्तव में क्या कैप्चर करता है?

पायथन में स्कोपिंग गतिशील और शाब्दिक है। एक क्लोजर हमेशा चर का नाम और दायरा याद रखेगा, न कि वह वस्तु जो वह इंगित कर रहा है। चूँकि आपके उदाहरण में सभी फ़ंक्शन समान दायरे में बनाए गए हैं और समान चर नाम का उपयोग करते हैं, वे हमेशा एक ही चर का संदर्भ देते हैं।

EDIT: इसे दूर करने के अपने अन्य प्रश्न के बारे में, मन में आने वाले दो तरीके हैं:

  1. सबसे संक्षिप्त, लेकिन कड़ाई से समकक्ष तरीके से एड्रियन प्लिसन द्वारा अनुशंसित एक नहीं है । एक अतिरिक्त तर्क के साथ एक लैम्ब्डा बनाएं, और अतिरिक्त तर्क के डिफ़ॉल्ट मान को उस ऑब्जेक्ट पर सेट करें जिसे आप संरक्षित करना चाहते हैं।

  2. हर बार जब आप लैम्बडा बनाते हैं तो थोड़ी सी क्रिया लेकिन कम हैसी एक नया दायरा बनाने के लिए होगा:

    >>> adders = [0,1,2,3]
    >>> for i in [0,1,2,3]:
    ...     adders[i] = (lambda b: lambda a: b + a)(i)
    ...     
    >>> adders[1](3)
    4
    >>> adders[2](3)
    5

    यहाँ गुंजाइश एक नए फ़ंक्शन (संक्षिप्तता के लिए एक लंबोदा) का उपयोग करके बनाई गई है, जो अपने तर्क को बांधता है, और उस मान को पारित करना जिसे आप तर्क के रूप में बाँधना चाहते हैं। वास्तविक कोड में, हालांकि, नए स्कोप को बनाने के लिए लैंबडा के बजाय आपके पास एक सामान्य फ़ंक्शन होगा।

    def createAdder(x):
        return lambda y: y + x
    adders = [createAdder(i) for i in range(4)]

1
अधिकतम, यदि आप मेरे अन्य (सरल प्रश्न) के लिए एक उत्तर जोड़ते हैं, तो मैं इसे एक स्वीकृत उत्तर के रूप में चिह्नित कर सकता हूं। धन्यवाद!
बोअज

3
पाइथन में स्टेटिक स्कूपिंग है, डायनेमिक स्कूपिंग नहीं है। यह सिर्फ सभी वेरिएबल्स रेफरेंस हैं, इसलिए जब आप किसी वैरिएबल को किसी नए ऑब्जेक्ट पर सेट करते हैं, तो वैरिएबल (रेफरेंस) में वही लोकेशन होता है, लेकिन यह किसी और चीज की ओर इशारा करता है। यदि आप स्कीम में भी यही बात कहते हैं set!। यहां देखें कि वास्तव में डायनेमिक स्कोप क्या है: voidspace.org.uk/python/articles/code_blocks.shtml
क्लाउडीयू

6
विकल्प 2 जैसा दिखता है कि कौन सी कार्यात्मक भाषाएं "करी फ़ंक्शन" कहेगी।
क्रैश

205

आप एक डिफ़ॉल्ट मान के साथ एक तर्क का उपयोग कर चर को पकड़ने पर मजबूर कर सकते हैं:

>>> for i in [0,1,2,3]:
...    adders[i]=lambda a,i=i: i+a  # note the dummy parameter with a default value
...
>>> print( adders[1](3) )
4

विचार एक पैरामीटर घोषित करना है (चतुराई से नाम i) और इसे उस चर का डिफ़ॉल्ट मान दें जिसे आप कैप्चर करना चाहते हैं (मूल्य i)


7
डिफ़ॉल्ट मानों का उपयोग करने के लिए +1। जब लैम्ब्डा को परिभाषित किया जाता है तो उनका मूल्यांकन किया जाना इस उपयोग के लिए उन्हें सही बनाता है।
कोर्नियन

21
+1 भी क्योंकि यह आधिकारिक FAQ द्वारा समर्थित समाधान है ।
अपहरण

23
ये अद्भुत है। हालांकि, डिफ़ॉल्ट पायथन व्यवहार नहीं है।
सेसिल करी

1
यह सिर्फ एक अच्छा समाधान की तरह प्रतीत नहीं होता है ... आप वास्तव में केवल चर की एक प्रति पकड़ने के लिए फ़ंक्शन हस्ताक्षर बदल रहे हैं। और फ़ंक्शन को लागू करने वाले भी i चर के साथ गड़बड़ कर सकते हैं, है ना?
डेविड कॉलनान

@DavidCallanan हम एक लैंबडा के बारे में बात कर रहे हैं: एक प्रकार का तदर्थ फ़ंक्शन जिसे आप आमतौर पर एक छेद प्लग करने के लिए अपने कोड में परिभाषित करते हैं, न कि कुछ जिसे आप पूरे एसडीके के माध्यम से साझा करते हैं। यदि आपको एक मजबूत हस्ताक्षर की आवश्यकता है, तो आपको एक वास्तविक फ़ंक्शन का उपयोग करना चाहिए।
एड्रियन प्लिसन

33

पूर्णता के लिए अपने दूसरे प्रश्न का एक और उत्तर: आप फंक्शंस में आंशिक उपयोग कर सकते हैं

ऑपरेटर से आयात जोड़ने के साथ क्रिस लुत्ज़ ने प्रस्ताव रखा कि उदाहरण बन जाता है:

from functools import partial
from operator import add   # add(a, b) -- Same as a + b.

adders = [0,1,2,3]
for i in [0,1,2,3]:
   # store callable object with first argument given as (current) i
   adders[i] = partial(add, i) 

print adders[1](3)

24

निम्नलिखित कोड पर विचार करें:

x = "foo"

def print_x():
    print x

x = "bar"

print_x() # Outputs "bar"

मुझे लगता है कि ज्यादातर लोगों को यह भ्रामक नहीं लगेगा। यह अपेक्षित व्यवहार है।

तो, क्यों लोगों को लगता है कि यह अलग होगा जब यह एक पाश में किया जाता है? मुझे पता है कि मैंने खुद वह गलती की थी, लेकिन मुझे नहीं पता कि क्यों। यह लूप है? या शायद मेमना?

आखिरकार, लूप केवल एक छोटा संस्करण है:

adders= [0,1,2,3]
i = 0
adders[i] = lambda a: i+a
i = 1
adders[i] = lambda a: i+a
i = 2
adders[i] = lambda a: i+a
i = 3
adders[i] = lambda a: i+a

11
यह लूप है, क्योंकि कई अन्य भाषाओं में एक लूप एक नया दायरा बना सकता है।
detly

1
यह उत्तर अच्छा है क्योंकि यह बताता है कि iप्रत्येक लैम्बडा फ़ंक्शन के लिए एक ही चर क्यों एक्सेस किया जा रहा है।
डेविड कॉलनान

3

आपके दूसरे प्रश्न के उत्तर में, ऐसा करने का सबसे सुरुचिपूर्ण तरीका एक फ़ंक्शन का उपयोग करना होगा जो एक सरणी के बजाय दो पैरामीटर लेता है:

add = lambda a, b: a + b
add(1, 3)

हालांकि, यहां लंबोदर का उपयोग करना थोड़ा मूर्खतापूर्ण है। पायथन हमें operatorमॉड्यूल देता है , जो मूल ऑपरेटरों को एक कार्यात्मक इंटरफ़ेस प्रदान करता है। ऊपर के लंबोदर में अनावश्यक ओवरहेड है, बस अतिरिक्त ऑपरेटर को कॉल करने के लिए:

from operator import add
add(1, 3)

मैं समझता हूं कि आप आस-पास खेल रहे हैं, भाषा का पता लगाने की कोशिश कर रहे हैं, लेकिन मैं ऐसी स्थिति की कल्पना नहीं कर सकता कि मैं एक ऐसे फ़ंक्शंस का उपयोग करूं जहां पाइथन की स्कूपिंग अजीब तरह से हो।

यदि आप चाहते हैं, तो आप एक छोटा वर्ग लिख सकते हैं जो आपके ऐरे-इंडेक्सिंग सिंटैक्स का उपयोग करता है:

class Adders(object):
    def __getitem__(self, item):
        return lambda a: a + item

adders = Adders()
adders[1](3)

2
बेशक, उपरोक्त कोड का मेरी मूल समस्या से कोई लेना-देना नहीं है। इसका निर्माण एक साधारण तरीके से मेरी बात को स्पष्ट करने के लिए किया गया है। यह बेशक बेकार और मूर्खतापूर्ण है।
बोअज

3

यहां एक नया उदाहरण है जो डेटा संरचना और एक क्लोजर की सामग्री को हाइलाइट करता है, ताकि स्पष्ट करने में मदद मिल सके कि जब एन्कोडिंग संदर्भ "सहेजा गया है।"

def make_funcs():
    i = 42
    my_str = "hi"

    f_one = lambda: i

    i += 1
    f_two = lambda: i+1

    f_three = lambda: my_str
    return f_one, f_two, f_three

f_1, f_2, f_3 = make_funcs()

एक बंद में क्या है?

>>> print f_1.func_closure, f_1.func_closure[0].cell_contents
(<cell at 0x106a99a28: int object at 0x7fbb20c11170>,) 43 

विशेष रूप से, my_str f1 के बंद होने में नहीं है।

F2 के बंद होने में क्या है?

>>> print f_2.func_closure, f_2.func_closure[0].cell_contents
(<cell at 0x106a99a28: int object at 0x7fbb20c11170>,) 43

ध्यान दें (मेमोरी पतों से) दोनों क्लोजर में समान ऑब्जेक्ट होते हैं। तो, आप शुरू कर सकते हैं लैम्बडा फंक्शन के बारे में सोचना क्योंकि इसमें स्कोप का संदर्भ होता है। हालाँकि, my_str f_1 या f_2 के लिए क्लोजर में नहीं है, और मैं f_3 (नहीं दिखाया गया) के लिए क्लोजर में नहीं हूं, जो बताता है कि क्लोजर ऑब्जेक्ट्स स्वयं अलग ऑब्जेक्ट हैं।

क्या बंद वस्तुएं स्वयं एक ही वस्तु हैं?

>>> print f_1.func_closure is f_2.func_closure
False

NB उत्पादन int object at [address X]>मुझे लगता है कि बंद करने के लिए [पता एक्स] एकेए एक संदर्भ संग्रहीत है। हालाँकि, अगर लैम्बडा स्टेटमेंट के बाद वेरिएबल को फिर से असाइन किया जाता है तो [एड्रेस एक्स] बदल जाएगा।
जेफ
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.