क्या पायथन पूंछ पुनरावृत्ति का अनुकूलन करता है?


205

मेरे पास निम्नलिखित कोड का कोड है जो निम्नलिखित त्रुटि के साथ विफल रहता है:

RuntimeError: अधिकतम पुनरावर्तन गहराई पार हो गई

मैंने पूंछ पुनरावृत्ति अनुकूलन (TCO) के लिए अनुमति देने के लिए इसे फिर से लिखने का प्रयास किया। मेरा मानना ​​है कि अगर TCO हुआ होता तो यह कोड सफल होना चाहिए था।

def trisum(n, csum):
    if n == 0:
        return csum
    else:
        return trisum(n - 1, csum + n)

print(trisum(1000, 0))

क्या मुझे यह निष्कर्ष निकालना चाहिए कि पायथन किसी भी प्रकार का TCO नहीं करता है, या क्या मुझे इसे अलग तरह से परिभाषित करने की आवश्यकता है?


11
@ भाषा TCO सरल है कि भाषा कितनी गतिशील या स्थिर है। उदाहरण के लिए, लुआ, यह भी करता है। आपको केवल टेल कॉल को पहचानने की आवश्यकता है (एएसटी स्तर पर और बाईटकोड स्तर पर बहुत सरल), और फिर एक नया बनाने के बजाय वर्तमान स्टैक फ्रेम का फिर से उपयोग करें (मूल कोड की तुलना में दुभाषियों में सरल, वास्तव में सरल भी) ।

11
ओह, एक नाइटपिक: आप विशेष रूप से पूंछ पुनरावृत्ति के बारे में बात करते हैं, लेकिन संक्षिप्त नाम "TCO" का उपयोग करते हैं, जिसका अर्थ है पूंछ कॉल अनुकूलन और स्पष्ट रूप से या अंतर्निहित रूप से किसी भी उदाहरण पर लागू होता है return func(...), चाहे वह पुनरावर्ती हो या न हो। TCO TRE का एक उचित सुपरसेट है, और अधिक उपयोगी है (जैसे यह निरंतर गुजरने की शैली को संभव बनाता है, जिसे TRE नहीं कर सकता है), और लागू करने के लिए अधिक कठिन नहीं है।

1
यहाँ इसे लागू करने के लिए एक हैक तरीका है - निष्पादन फ्रेम को दूर फेंकने के लिए अपवाद का उपयोग करके एक डेकोरेटर: metapython.blogspot.com.br/2010/11/…
jsbueno

2
यदि आप अपने आप को पुनरावृत्ति के लिए प्रतिबंधित करते हैं, तो मुझे नहीं लगता कि एक उचित ट्रेसबैक सुपर-उपयोगी है। आपके पास किसी कॉल के fooअंदर से कॉल करने के लिए fooअंदर fooसे कॉल करने के लिए एक कॉल है foo... मुझे नहीं लगता कि इसे खोने से कोई उपयोगी जानकारी खो जाएगी।
केविन

1
मैंने हाल ही में नारियल के बारे में सीखा है, लेकिन अभी तक इसकी कोशिश नहीं की है। यह देखने लायक है। यह पूंछ पुनरावृत्ति अनुकूलन होने का दावा किया जाता है।
एलेक्सी

जवाबों:


214

नहीं, और यह कभी नहीं होगा क्योंकि गुइडो वैन रोसुम उचित ट्रेसबैक करने में सक्षम होना पसंद करते हैं:

टेल पुनरावर्तन उन्मूलन (2009-04-22)

टेल कॉल पर अंतिम शब्द (2009-04-27)

आप मैन्युअल रूप से इस तरह परिवर्तन के साथ पुनरावृत्ति को समाप्त कर सकते हैं:

>>> def trisum(n, csum):
...     while True:                     # Change recursion to a while loop
...         if n == 0:
...             return csum
...         n, csum = n - 1, csum + n   # Update parameters instead of tail recursion

>>> trisum(1000,0)
500500

12
या यदि आप इसे इस तरह बदलना चाहते हैं - बस from operator import add; reduce(add, xrange(n + 1), csum):?
जॉन क्लेमेंट्स

38
@JonClements, जो इस विशेष उदाहरण में काम करता है। सामान्य मामलों में पूंछ पुनरावृत्ति के लिए थोड़ी देर के लूप में परिवर्तन काम करता है।
जॉन ला रूय

25
+1 सही उत्तर होने के लिए लेकिन यह एक अविश्वसनीय रूप से हड्डी की तरह डिजाइन वाला निर्णय है। दिए गए कारण नीचे उबालने के लिए करने के लिए "यह कैसे अजगर व्याख्या की है दिया है करने के लिए कठिन है और मैं तो वहाँ वैसे भी यह पसंद नहीं है!" लग रहे हैं
मूल

12
@jwg तो ... क्या? खराब डिज़ाइन निर्णयों पर टिप्पणी करने से पहले आपको एक भाषा लिखनी होगी? शायद ही तार्किक या व्यावहारिक लगता है। मैं आपकी टिप्पणी से मान लेता हूं कि आपके द्वारा लिखी गई किसी भी भाषा में किसी भी विशेषता (या इसके अभाव) के बारे में आपकी कोई राय नहीं है?
बेसिक

2
@Basic नहीं, लेकिन आपको वह लेख पढ़ना होगा जिस पर आप टिप्पणी कर रहे हैं। यह बहुत दृढ़ता से लगता है कि आपने वास्तव में इसे पढ़ा नहीं था, यह देखते हुए कि यह आपके लिए कैसे "उबलता है"। (आपको वास्तव में लिंक किए गए दोनों लेखों को पढ़ने की आवश्यकता हो सकती है, दुर्भाग्य से, चूंकि कुछ तर्क दोनों पर फैले हुए हैं।) इसका भाषा के कार्यान्वयन से कोई लेना-देना नहीं है, लेकिन इच्छित अर्थ-विज्ञान के साथ सब कुछ करना है।
वैक्सी

178

मैंने टेल-कॉल ऑप्टिमाइज़ेशन (टेल-रिकर्सन और कंटीन्यू-पासिंग स्टाइल दोनों को हैंडल करते हुए): https://github.com/baruchel/tco पर काम करते हुए एक मॉड्यूल प्रकाशित किया।

पायथन में पूंछ-पुनरावृत्ति का अनुकूलन

अक्सर यह दावा किया गया है कि पूंछ-पुनरावृत्ति कोडिंग के पायथोनिक तरीके के अनुरूप नहीं है और किसी को यह ध्यान नहीं देना चाहिए कि इसे लूप में कैसे एम्बेड किया जाए। मैं इस दृष्टिकोण से बहस नहीं करना चाहता; कभी-कभी मुझे विभिन्न कारणों से छोरों के बजाय पूंछ-पुनरावर्ती कार्यों के रूप में नए विचारों की कोशिश करना या लागू करना पसंद है, केवल प्रक्रिया पर विचार करने पर ध्यान केंद्रित करने के बजाय केवल तीन "पाइथोनिक" के बजाय मेरी स्क्रीन पर बीस छोटे कार्य करना। फ़ंक्शंस, मेरे कोड को संपादित करने के बजाय एक इंटरैक्टिव सत्र में काम करना, आदि)।

पायथन में पूंछ-पुनरावृत्ति का अनुकूलन वास्तव में काफी आसान है। हालांकि इसे असंभव या बहुत मुश्किल कहा जाता है, मुझे लगता है कि इसे सुरुचिपूर्ण, छोटे और सामान्य समाधानों के साथ प्राप्त किया जा सकता है; मुझे भी लगता है कि इन समाधानों में से अधिकांश पायथन सुविधाओं का उपयोग नहीं करते हैं, अन्यथा उन्हें करना चाहिए। पूंछ-पुनरावृत्ति अनुकूलन को लागू करने के लिए बहुत मानक छोरों के साथ काम करने वाले स्वच्छ लंबोदर भाव त्वरित, कुशल और पूरी तरह से उपयोग करने योग्य उपकरण का नेतृत्व करते हैं।

एक व्यक्तिगत सुविधा के रूप में, मैंने दो अलग-अलग तरीकों से इस तरह के अनुकूलन को लागू करने वाला एक छोटा मॉड्यूल लिखा। मैं अपने दो मुख्य कार्यों के बारे में यहां चर्चा करना चाहूंगा।

साफ तरीका: वाई कॉम्बीनेटर को संशोधित करना

Y Combinator अच्छी तरह से जाना जाता है; यह एक पुनरावर्ती तरीके से लैम्ब्डा कार्यों का उपयोग करने की अनुमति देता है, लेकिन यह स्वयं को लूप में पुनरावर्ती कॉल एम्बेड करने की अनुमति नहीं देता है। लैंबडा कैलकुलस अकेले ऐसा काम नहीं कर सकता। वाई कॉम्बिनेटर में थोड़ा बदलाव हालांकि पुनरावर्ती कॉल की रक्षा कर सकता है जिसका वास्तव में मूल्यांकन किया जा सकता है। इस प्रकार मूल्यांकन में देरी हो सकती है।

यहाँ Y संयोजन के लिए प्रसिद्ध अभिव्यक्ति है:

lambda f: (lambda x: x(x))(lambda y: f(lambda *args: y(y)(*args)))

बहुत मामूली बदलाव के साथ, मुझे मिल सकता है:

lambda f: (lambda x: x(x))(lambda y: f(lambda *args: lambda: y(y)(*args)))

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

मेरा कोड है:

def bet(func):
    b = (lambda f: (lambda x: x(x))(lambda y:
          f(lambda *args: lambda: y(y)(*args))))(func)
    def wrapper(*args):
        out = b(*args)
        while callable(out):
            out = out()
        return out
    return wrapper

फ़ंक्शन का उपयोग निम्न तरीके से किया जा सकता है; यहाँ गुटबाजी और फाइबोनैचि के पूंछ-पुनरावर्ती संस्करणों के साथ दो उदाहरण हैं:

>>> from recursion import *
>>> fac = bet( lambda f: lambda n, a: a if not n else f(n-1,a*n) )
>>> fac(5,1)
120
>>> fibo = bet( lambda f: lambda n,p,q: p if not n else f(n-1,q,p+q) )
>>> fibo(10,0,1)
55

जाहिर है कि पुनरावृत्ति की गहराई अब कोई मुद्दा नहीं है:

>>> bet( lambda f: lambda n: 42 if not n else f(n-1) )(50000)
42

यह निश्चित रूप से फ़ंक्शन का एकमात्र वास्तविक उद्देश्य है।

इस अनुकूलन के साथ केवल एक चीज नहीं की जा सकती है: इसका उपयोग दूसरे फ़ंक्शन के मूल्यांकन के लिए पूंछ-पुनरावर्ती फ़ंक्शन के साथ नहीं किया जा सकता है (यह इस तथ्य से आता है कि कॉल करने योग्य रिटर्न ऑब्जेक्ट्स को बिना किसी भेद के आगे पुनरावर्ती कॉल के रूप में नियंत्रित किया जाता है)। चूंकि मुझे आमतौर पर ऐसी सुविधा की आवश्यकता नहीं है, इसलिए मैं ऊपर दिए गए कोड से बहुत खुश हूं। हालाँकि, एक अधिक सामान्य मॉड्यूल प्रदान करने के लिए, मैंने इस मुद्दे के लिए कुछ समाधान खोजने के लिए थोड़ा और सोचा (अगले भाग देखें)।

इस प्रक्रिया की गति के बारे में (जो कि वास्तविक मुद्दा नहीं है), यह काफी अच्छा होता है; पूंछ-पुनरावर्ती कार्यों का मूल्यांकन सरल कोड का उपयोग करके निम्न कोड की तुलना में बहुत तेज किया जाता है:

def bet1(func):
    def wrapper(*args):
        out = func(lambda *x: lambda: x)(*args)
        while callable(out):
            out = func(lambda *x: lambda: x)(*out())
        return out
    return wrapper

मुझे लगता है कि एक अभिव्यक्ति का मूल्यांकन, यहां तक ​​कि जटिल, कई सरल अभिव्यक्तियों का मूल्यांकन करने की तुलना में बहुत तेज है, जो इस दूसरे संस्करण में मामला है। मैंने इस नए फ़ंक्शन को अपने मॉड्यूल में नहीं रखा था, और मुझे ऐसी कोई भी परिस्थिति नहीं दिखी जहां इसे "आधिकारिक" एक के बजाय इस्तेमाल किया जा सके।

अपवादों के साथ निरंतरता शैली

यहां एक अधिक सामान्य कार्य है; यह अन्य कार्यों को वापस करने सहित सभी पूंछ-पुनरावर्ती कार्यों को संभालने में सक्षम है। पुनरावर्ती कॉल अपवादों के उपयोग द्वारा अन्य रिटर्न मूल्यों से पहचाने जाते हैं। यह समाधान पिछले वाले की तुलना में धीमा है; एक तेज कोड शायद कुछ विशेष मूल्यों का उपयोग करके लिखा जा सकता है क्योंकि मुख्य लूप में "झंडे" का पता लगाया जा रहा है, लेकिन मुझे विशेष मूल्यों या आंतरिक कीवर्ड का उपयोग करने का विचार पसंद नहीं है। अपवादों का उपयोग करने की कुछ मज़ेदार व्याख्या है: यदि पायथन पूंछ-पुनरावर्ती कॉल को पसंद नहीं करता है, तो एक अपवाद तब उठाया जाना चाहिए जब एक पूंछ-पुनरावर्ती कॉल होता है, और पायथोनिक तरीका अपवाद को पकड़ने के लिए होगा ताकि उसे साफ मिल सके समाधान, जो वास्तव में यहाँ क्या होता है ...

class _RecursiveCall(Exception):
  def __init__(self, *args):
    self.args = args
def _recursiveCallback(*args):
  raise _RecursiveCall(*args)
def bet0(func):
    def wrapper(*args):
        while True:
          try:
            return func(_recursiveCallback)(*args)
          except _RecursiveCall as e:
            args = e.args
    return wrapper

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

>>> f = bet0( lambda f: lambda n: (lambda x: x) if not n else f(n-1) )
>>> f(5)(42)
42

बेशक, यह तर्क दिया जा सकता है कि अपवाद का उपयोग जानबूझकर इंटरप्रेटर को पुनर्निर्देशित करने के लिए नहीं किया जाना चाहिए (एक तरह का gotoबयान या शायद एक तरह की निरंतरता गुजर शैली के रूप में), जिसे मुझे स्वीकार करना होगा। लेकिन, फिर से, मुझे tryएक लाइन के साथ एक returnस्टेटमेंट के रूप में उपयोग करने का विचार मज़ेदार लगता है : हम कुछ (सामान्य व्यवहार) वापस करने की कोशिश करते हैं, लेकिन एक पुनरावर्ती कॉल होने (अपवाद) के कारण हम ऐसा नहीं कर सकते।

प्रारंभिक उत्तर (2013-08-29)।

मैंने पूंछ पुनरावृत्ति से निपटने के लिए एक बहुत छोटा प्लगइन लिखा। आप इसे मेरे स्पष्टीकरण के साथ वहां पा सकते हैं: https://groups.google.com/forum/?hl=fr# .topic/ comp.lang.python / dIsnJ2BoBKs

यह एक लैंबडा फ़ंक्शन को एक अन्य फ़ंक्शन में पूंछ पुनरावृत्ति शैली के साथ लिखा जा सकता है, जो इसे लूप के रूप में मूल्यांकन करेगा।

इस छोटे से समारोह में सबसे दिलचस्प विशेषता, मेरी विनम्र राय में, यह है कि फ़ंक्शन कुछ गंदे प्रोग्रामिंग हैक पर भरोसा नहीं करता है, लेकिन मात्र लैंबडा कैलकुलस पर: फ़ंक्शन का व्यवहार दूसरे में तब बदल जाता है जब किसी अन्य लंबो फ़ंक्शन में डाला जाता है वाई कॉम्बिनेटर की तरह दिखता है।


क्या आप कृपया, किसी फ़ंक्शन को परिभाषित करने का एक उदाहरण प्रदान कर सकते हैं (अधिमानतः एक सामान्य परिभाषा के समान) जो किसी शर्त के आधार पर कई अन्य कार्यों में से एक को पूंछता है, जो आपके तरीके का उपयोग करता है? इसके अलावा, क्या आपके रैपिंग फंक्शन bet0को क्लास के तरीकों के लिए डेकोरेटर के रूप में इस्तेमाल किया जा सकता है?
एलेक्सी

@Alexey मुझे यकीन नहीं है कि मैं एक टिप्पणी के अंदर एक ब्लॉक-शैली में कोड लिख सकता हूं, लेकिन आप निश्चित रूप से defअपने कार्यों के लिए वाक्यविन्यास का उपयोग कर सकते हैं , और वास्तव में ऊपर दिया गया अंतिम उदाहरण एक शर्त पर निर्भर करता है। मेरी पोस्ट में baruchel.github.io/python/2015/11/07/… "आप निश्चित रूप से आपत्ति कर सकते हैं कि कोई भी ऐसा कोड नहीं लिखेगा" के साथ एक पैराग्राफ शुरू होता है, जहां मैं सामान्य परिभाषा वाक्यविन्यास के साथ एक उदाहरण देता हूं। आपके प्रश्न के दूसरे भाग के लिए, मुझे इसके बारे में थोड़ा और सोचना होगा क्योंकि मैंने इसमें कुछ समय नहीं बिताया है। सादर।
थॉमस बरूचेल

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

21

गुइडो का शब्द http://neopythonic.blogspot.co.uk/2009/04/tail-recursion-bimination.html पर है

मैंने हाल ही में पायथन के कार्यात्मक विशेषताओं की उत्पत्ति पर अपने पायथन इतिहास ब्लॉग में एक प्रविष्टि पोस्ट की है। पूंछ पुनरावृत्ति उन्मूलन (TRE) का समर्थन नहीं करने के बारे में एक टिप्पणी ने तुरंत कई टिप्पणियों को उकसाया कि यह क्या अफ़सोस की बात है कि पायथन ऐसा नहीं करता है, हाल ही में ब्लॉग प्रविष्टियों के लिंक सहित अन्य लोगों द्वारा "साबित" करने की कोशिश कर रहे हैं कि TRE को पायथन में जोड़ा जा सकता है सरलता। तो मुझे अपनी स्थिति का बचाव करने दें (जो यह है कि मुझे भाषा में TRE नहीं चाहिए)। यदि आप एक संक्षिप्त उत्तर चाहते हैं, तो यह सरल है। यहाँ लंबा जवाब है:


12
और यहाँ तथाकथित BDsFL के साथ समस्या है।
एडम डोनह्यू

6
@AdamDonahue आप एक समिति से आए हर फैसले से पूरी तरह से संतुष्ट हैं? कम से कम आपको BDFL से एक तर्कपूर्ण और आधिकारिक स्पष्टीकरण मिलता है।
मार्क रैनसम

2
नहीं, निश्चित रूप से नहीं, लेकिन उन्होंने मुझे और भी समान रूप से प्रहार किया। यह एक प्रिस्क्रिप्‍टविस्‍ट से है, डिस्‍क्रिप्‍टिविस्‍ट से नहीं। व्यंग्य।
एडम डोनह्यू

6

CPython इस विषय पर Guido van Rossum के कथनों के आधार पर टेल कॉल ऑप्टिमाइज़ेशन का समर्थन नहीं करेगा और न ही कभी करेगा ।

मैंने दलीलें सुनी हैं कि यह डिबगिंग को अधिक कठिन बनाता है क्योंकि यह स्टैक ट्रेस को कैसे संशोधित करता है।


18
@mux CPython प्रोग्रामिंग भाषा पायथन का संदर्भ कार्यान्वयन है। अन्य कार्यान्वयन हैं (जैसे कि PyPy, IronPython, और Jython), जो एक ही भाषा को लागू करते हैं लेकिन कार्यान्वयन विवरण में भिन्न होते हैं। यहाँ अंतर उपयोगी है क्योंकि (सिद्धांत रूप में) एक वैकल्पिक पायथन कार्यान्वयन बनाना संभव है जो TCO करता है। मैं किसी को भी इसके बारे में सोचने के बारे में पता नहीं कर रहा हूँ, और उपयोगिता सीमित होगी क्योंकि कोड इस पर निर्भर अन्य सभी पायथन कार्यान्वयन पर टूट जाएगा।


2

पूंछ की पुनरावृत्ति को अनुकूलित करने के अलावा, आप मैन्युअल रूप से पुनरावृत्ति की गहराई निर्धारित कर सकते हैं:

import sys
sys.setrecursionlimit(5500000)
print("recursion limit:%d " % (sys.getrecursionlimit()))

5
आप सिर्फ jQuery का उपयोग क्यों नहीं करते?
जेरेमी हर्ट सिप

5
क्योंकि यह TCO की पेशकश भी नहीं करता है? :-D stackoverflow.com/questions/3660577/…
Veky
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.