लेक्सिकल क्लोजर कैसे काम करते हैं?


149

जब मैं जावास्क्रिप्ट कोड में लेक्सिकल क्लोजर के साथ एक समस्या की जांच कर रहा था, मैं पायथन में इस समस्या के साथ आया था:

flist = []

for i in xrange(3):
    def func(x): return x * i
    flist.append(func)

for f in flist:
    print f(2)

ध्यान दें कि यह उदाहरण ध्यान से बचता है lambda । यह "4 4 4" प्रिंट करता है, जो आश्चर्यजनक है। मुझे उम्मीद है "0 2 4"।

यह समतुल्य पर्ल कोड इसे सही करता है:

my @flist = ();

foreach my $i (0 .. 2)
{
    push(@flist, sub {$i * $_[0]});
}

foreach my $f (@flist)
{
    print $f->(2), "\n";
}

"2 2 4" मुद्रित है।

क्या आप कृपया अंतर बता सकते हैं?


अपडेट करें:

समस्या वैश्विक होने के साथ नहीं हैi । यह समान व्यवहार प्रदर्शित करता है:

flist = []

def outer():
    for i in xrange(3):
        def inner(x): return x * i
        flist.append(inner)

outer()
#~ print i   # commented because it causes an error

for f in flist:
    print f(2)

जैसा कि टिप्पणी लाइन से पता चलता है, iउस बिंदु पर अज्ञात है। फिर भी, यह "4 4 4" प्रिंट करता है।



3
यहाँ इस मुद्दे पर एक बहुत अच्छा लेख है। me.veekun.com/blog/2011/04/24/gotcha-python-scoping-closures
updogliu

जवाबों:


151

पायथन वास्तव में परिभाषित के रूप में व्यवहार कर रहा है। तीन अलग-अलग फ़ंक्शन बनाए गए हैं, लेकिन उनमें से प्रत्येक को उस वातावरण में बंद किया गया है जिसे उन्होंने परिभाषित किया है - इस मामले में, वैश्विक वातावरण (या बाहरी फ़ंक्शन का वातावरण यदि लूप को किसी अन्य फ़ंक्शन के अंदर रखा गया है)। यह वास्तव में समस्या है, हालांकि - इस वातावरण में, मैं उत्परिवर्तित हूं , और क्लोजर सभी एक ही i को संदर्भित करते हैं

एक समारोह creater बना सकते हैं और आह्वान - यहाँ सबसे अच्छा समाधान मैं के साथ आ सकता है कि बजाय। यह प्रत्येक कार्य के लिए अलग वातावरण तैयार करेगा, एक अलग i के साथ

flist = []

for i in xrange(3):
    def funcC(j):
        def func(x): return x * j
        return func
    flist.append(funcC(i))

for f in flist:
    print f(2)

यह तब होता है जब आप दुष्प्रभावों और कार्यात्मक प्रोग्रामिंग को मिलाते हैं।


5
आपका समाधान भी जावास्क्रिप्ट में उपयोग किया जाता है।
एली बेंडरस्की

9
यह दुर्व्यवहार नहीं है। यह बिल्कुल परिभाषित के रूप में व्यवहार कर रहा है।
एलेक्स कॉवेंट्री

6
IMO पिरो के पास एक बेहतर समाधान है stackoverflow.com/questions/233673/…
jfs

2
मैं शायद स्पष्टता के लिए अंतरतम 'i' को 'j' में बदल दूंगा।
अंडेसिनेक्स

7
इस बारे में इसे परिभाषित करने के बारे में क्या है:def inner(x, i=i): return x * i
डैश 2

152

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

iअपने मूल्य का मूल्यांकन और उपयोग करने के लिए , एक सामान्य पैटर्न इसे एक पैरामीटर डिफ़ॉल्ट के रूप में सेट करने के लिए है: पैरामीटर चूक का मूल्यांकन तब किया जाता है जबdef कथन निष्पादित किया जाता है, और इस प्रकार लूप वेरिएबल का मान जम जाता है।

निम्नलिखित उम्मीद के अनुसार काम करता है:

flist = []

for i in xrange(3):
    def func(x, i=i): # the *value* of i is copied in func() environment
        return x * i
    flist.append(func)

for f in flist:
    print f(2)

7
s / संकलन समय पर / उस समय जब defस्टेटमेंट निष्पादित किया जाता है /
jfs

23
यह एक सरल उपाय है, जो इसे भयानक बनाता है।
स्टावरोस कोरोकिथिक 13

इस समाधान के साथ एक समस्या है: फंक में अब दो पैरामीटर हैं। इसका मतलब है कि यह मापदंडों की एक चर राशि के साथ काम नहीं करता है। इससे भी बदतर, अगर आप एक दूसरे पैरामीटर के साथ फंक कहते हैं तो यह iपरिभाषा से मूल को ओवरराइट कर देगा । :-(
पास्कल

34

यहां बताया गया है कि आप इसे functoolsलाइब्रेरी का उपयोग कैसे करते हैं (जो मुझे यकीन नहीं है कि उस समय प्रश्न उपलब्ध था)।

from functools import partial

flist = []

def func(i, x): return x * i

for i in xrange(3):
    flist.append(partial(func, i))

for f in flist:
    print f(2)

आउटपुट 0 2 4, जैसा कि अपेक्षित था।


मैं वास्तव में इस का उपयोग करना चाहता था, लेकिन मेरा कार्य एक वर्ग विधि है और पारित पहला मूल्य स्व है। क्या इसके आसपास भी ऐसा है?
माइकल डेविड वॉटसन

1
पूर्ण रूप से। मान लें कि आपके पास एक विधि जोड़ने (स्व, ए, बी) के साथ एक वर्ग गणित है, और आप 'वेतन वृद्धि' विधि बनाने के लिए एक = 1 सेट करना चाहते हैं। फिर, आप 'my_math' श्रेणी का एक उदाहरण बनाएँ, और आपकी वेतन वृद्धि विधि 'increment = आंशिक (my_math.add, 1)' होगी।
लुका इन्वरनिज़ी

2
इस तकनीक को एक विधि पर लागू करने के लिए आप functools.partialmethod()अजगर 3.4 के रूप में भी इस्तेमाल कर सकते हैं
मैट एडिंग

13

इसे देखो:

for f in flist:
    print f.func_closure


(<cell at 0x00C980B0: int object at 0x009864B4>,)
(<cell at 0x00C980B0: int object at 0x009864B4>,)
(<cell at 0x00C980B0: int object at 0x009864B4>,)

इसका मतलब है कि वे सभी एक ही i चर उदाहरण की ओर इशारा करते हैं, जिसमें लूप खत्म होने के बाद 2 का मान होगा।

एक पठनीय हल:

for i in xrange(3):
        def ffunc(i):
            def func(x): return x * i
            return func
        flist.append(ffunc(i))

1
मेरा प्रश्न अधिक "सामान्य" है। अजगर को यह दोष क्यों है? मुझे उम्मीद है कि इसे सही ढंग से काम करने के लिए लेक्सिकल क्लोजर (जैसे पर्ल और पूरे लिस्प राजवंश) का समर्थन करने वाली भाषा की आवश्यकता होगी।
एली बेंडरस्की

2
यह पूछना कि किसी चीज में दोष क्यों है, यह मान लेना कि वह दोष नहीं है।
14303 पर Null303

7

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

आपकी योजना के उदाहरण के साथ अंतर लूप के शब्दार्थ के साथ करना है। योजना प्रभावी रूप से लूप के माध्यम से हर बार एक नया i वैरिएबल बना रही है, बजाय अन्य भाषाओं के साथ मौजूदा i बाइंडिंग को पुन: उपयोग करने के बजाय। यदि आप लूप के लिए बनाए गए एक अलग चर का उपयोग करते हैं और इसे म्यूट करते हैं, तो आपको योजना में समान व्यवहार दिखाई देगा। अपने लूप को बदलने का प्रयास करें:

(let ((ii 1)) (
  (do ((i 1 (+ 1 i)))
      ((>= i 4))
    (set! flist 
      (cons (lambda (x) (* ii x)) flist))
    (set! ii i))
))

आगे की कुछ चर्चा के लिए यहाँ एक नज़र डालें ।

[संपादित करें] संभवतः इसका वर्णन करने का एक बेहतर तरीका यह है कि डू लूप को एक मैक्रो के रूप में माना जाए जो निम्न चरणों का पालन करता है:

  1. एक एकल पैरामीटर (i) लेम्बडा को परिभाषित करें, लूप के शरीर द्वारा परिभाषित शरीर के साथ,
  2. मैं उस पैरामीटर के रूप में मैं के उचित मूल्यों के साथ उस मेमने की एक तत्काल कॉल।

अर्थात। नीचे के अजगर के बराबर:

flist = []

def loop_body(i):      # extract body of the for loop to function
    def func(x): return x*i
    flist.append(func)

map(loop_body, xrange(3))  # for i in xrange(3): body

मैं अब पैरेंट स्कोप से नहीं, बल्कि अपने स्कोप में एक बिल्कुल नया वैरिएबल (यानी, लैम्बडा का पैरामीटर) है और इसलिए आपको वह व्यवहार मिलता है जिसे आप देखते हैं। पायथन में यह अंतर्निहित नया दायरा नहीं है, इसलिए लूप के लिए बॉडी सिर्फ आई वेरिएबल शेयर करती है।


दिलचस्प। मुझे लूप के शब्दार्थ में अंतर की जानकारी नहीं थी। धन्यवाद
एली बेंडस्की

4

मैं अभी भी पूरी तरह से आश्वस्त नहीं हूं कि कुछ भाषाओं में यह एक तरह से काम करता है, और किसी अन्य तरीके से। आम लिस्प में यह अजगर की तरह है:

(defvar *flist* '())

(dotimes (i 3 t)
  (setf *flist* 
    (cons (lambda (x) (* x i)) *flist*)))

(dolist (f *flist*)  
  (format t "~a~%" (funcall f 2)))

प्रिंट "6 6 6" (ध्यान दें कि यहां सूची 1 से 3 तक है, और रिवर्स में बनाया गया है))। जबकि स्कीम में यह पर्ल की तरह काम करता है:

(define flist '())

(do ((i 1 (+ 1 i)))
    ((>= i 4))
  (set! flist 
    (cons (lambda (x) (* i x)) flist)))

(map 
  (lambda (f)
    (printf "~a~%" (f 2)))
  flist)

प्रिंट "6 4 2"

और जैसा कि मैंने पहले ही उल्लेख किया है, जावास्क्रिप्ट पायथन / सीएल शिविर में है। ऐसा प्रतीत होता है कि यहां एक कार्यान्वयन निर्णय है, जो विभिन्न भाषाओं में अलग-अलग तरीकों से होता है। मुझे यह समझने में अच्छा लगेगा कि निर्णय क्या है, बिल्कुल।


8
अंतर स्कूपिंग नियमों के बजाय (डू ...) में है। स्कीम में लूप से गुजरने वाला एक नया वैरिएबल बनाता है, जबकि अन्य भाषाएँ मौजूदा बाइंडिंग का पुनः उपयोग करती हैं। अधिक विवरण के लिए मेरा उत्तर देखें और लिस्प / अजगर के समान व्यवहार के साथ एक योजना संस्करण का एक उदाहरण।
ब्रायन

2

समस्या यह है कि सभी स्थानीय फ़ंक्शंस एक ही वातावरण और इस तरह एक ही iचर में बाँधते हैं । समाधान (वर्कअराउंड) प्रत्येक फ़ंक्शन (या लैम्ब्डा) के लिए अलग वातावरण (स्टैक फ्रेम) बनाना है।

t = [ (lambda x: lambda y : x*y)(x) for x in range(5)]

>>> t[1](2)
2
>>> t[2](2)
4

1

वैरिएबल iएक वैश्विक है, जिसका मूल्य हर बार फ़ंक्शन के 2 हैf कहा जाता है।

मैं आपके द्वारा किए गए व्यवहार को इस प्रकार लागू करना चाहूंगा:

>>> class f:
...  def __init__(self, multiplier): self.multiplier = multiplier
...  def __call__(self, multiplicand): return self.multiplier*multiplicand
... 
>>> flist = [f(i) for i in range(3)]
>>> [g(2) for g in flist]
[0, 2, 4]

आपके अपडेट का जवाब : यह i प्रति से वैश्विकता नहीं है, जो इस व्यवहार का कारण बन रहा है, यह तथ्य है कि यह एक घेरने वाले दायरे से एक चर है जिसका समय पर एक निश्चित मान होता है जब एफ कहा जाता है। आपके दूसरे उदाहरण में, फ़ंक्शन iके दायरे से मूल्य लिया जाता है kkk, और जब आप फ़ंक्शन को कॉल करते हैं तो कुछ भी नहीं बदल रहा है flist


0

व्यवहार के पीछे तर्क पहले से ही समझाया गया है, और कई समाधान पोस्ट किए गए हैं, लेकिन मुझे लगता है कि यह सबसे अधिक पायथोनिक है (याद रखें, पायथन में सब कुछ एक वस्तु है!):

flist = []

for i in xrange(3):
    def func(x): return x * func.i
    func.i=i
    flist.append(func)

for f in flist:
    print f(2)

एक फंक्शन जेनरेटर का उपयोग करते हुए क्लाउडी का जवाब बहुत अच्छा है, लेकिन पिरो का जवाब एक हैक है, ईमानदार होने के लिए, क्योंकि मैं इसे डिफ़ॉल्ट मान के साथ "छिपे हुए" तर्क में बना रहा हूं (यह ठीक काम करेगा, लेकिन यह "पायथोनिक" नहीं है) ।


मुझे लगता है कि यह आपके अजगर संस्करण पर निर्भर करता है। अब मैं अधिक अनुभवी हूं और मैं अब इसे करने का तरीका नहीं सुझाऊंगा। अजगर में एक क्लोजर बनाने के लिए क्लाउडीयू उचित तरीका है।
डार्कफ्लीन

1
यह पायथन 2 या 3 पर काम नहीं करेगा (वे दोनों "4 4 4" आउटपुट)। funcमें x * func.iहमेशा पिछले समारोह परिभाषित के पास भेजेगा। इसलिए भले ही प्रत्येक फ़ंक्शन व्यक्तिगत रूप से सही संख्या में अटक गया हो, फिर भी वे सभी पिछले एक से पढ़ना पसंद करते हैं।
लैम्ब्डा फेयरी
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.