Python kuyruk özyinelemesini optimize ediyor mu?


206

Aşağıdaki hata ile başarısız olan aşağıdaki kod parçası var:

RuntimeError: maksimum yineleme derinliği aşıldı

Kuyruk özyineleme optimizasyonuna (TCO) izin vermek için bunu yeniden yazmaya çalıştım. Bir TCO gerçekleşmiş olsaydı bu kodun başarılı olması gerektiğine inanıyorum.

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

print(trisum(1000, 0))

Python'un herhangi bir TCO yapmadığı sonucuna varmalı mıyım yoksa sadece farklı tanımlamam mı gerekiyor?


11
@Wessie TCO, dilin ne kadar dinamik veya statik olduğu konusunda basittir. Lua, örneğin, bunu da yapıyor. Sadece kuyruk çağrılarını tanımanız gerekir (hem AST düzeyinde hem de bayt kod düzeyinde oldukça basittir) ve ardından yeni bir tane oluşturmak yerine mevcut yığın çerçevesini yeniden kullanmanız gerekir (ayrıca basit, aslında çevirmenlerde yerel koddan daha basittir) .

11
Oh, bir nitpick: Sadece kuyruk özyineleme hakkında konuşuyorsunuz, ancak kuyruk çağrısı optimizasyonu anlamına gelen ve özyinelemeli olsun veya olmasın (açık veya örtülü) herhangi bir örneği için geçerli olan "TCO" kısaltmasını kullanın return func(...). TCO, TRE'nin uygun bir üst kümesidir ve daha yararlıdır (örneğin, devam eden geçiş stilini mümkün kılar, TRE'nin yapamayacağı şekilde yapar) ve uygulanması çok zor değildir.

1
İşte bunu uygulamak için hackish bir yol - yürütme çerçevelerini atmak için istisna kullanan bir dekoratör: metapython.blogspot.com.br/2010/11/…
jsbueno

2
Kendinizi kuyruk özyineleme ile kısıtlarsanız, uygun bir geri izlemenin süper yararlı olduğunu düşünmüyorum. Sen bir çağrı var fooGörüşme içeriden için fooiçeriden içinfoo bir çağrı içinden foo... Ben herhangi bir yararlı bilgiler bu kaybetmesini kaybedilecek sanmıyorum.
Kevin

1
Son zamanlarda Hindistan cevizi hakkında öğrendim ama henüz denemedim. Bir göz atmaya değer görünüyor. Kuyruk özyineleme optimizasyonuna sahip olduğu iddia ediliyor.
Alexey

Yanıtlar:


216

Hayır, ve Guido van Rossum'un uygun izleri sürmeyi tercih ettiği için asla olmayacak :

Kuyruk Özyineleme Ortadan Kaldırma (2009-04-22)

Kuyruk Çağrıları Üzerine Son Sözler (2009-04-27)

Özyinelemeyi, böyle bir dönüşümle el ile ortadan kaldırabilirsiniz:

>>> 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
Ya da bu şekilde dönüştürecekseniz - sadece from operator import add; reduce(add, xrange(n + 1), csum):?
Jon Clements

38
@JonClements, bu özel örnekte çalışır. While döngüsüne dönüşüm, genel durumlarda kuyruk özyineleme için çalışır.
John La Rooy

25
+1 Doğru cevap olduğu için bu inanılmaz kemik kafalı bir tasarım kararı gibi görünüyor. Verilen nedenler , "Python'un nasıl yorumlandığı göz önüne alındığında bunu yapmak zordur ve yine de hoşuma gitmiyor!"
Temel

12
@jwg Peki ... Ne? Kötü tasarım kararları hakkında yorum yapabilmek için önce bir dil mi yazmalısınız? Pek mantıklı veya pratik görünmüyor. Yorumunuzdan, yazdığınız herhangi bir dilde herhangi bir özellik (veya eksikliği) hakkında hiçbir fikriniz olmadığını varsayıyorum?
Temel

2
@Temel Hayır, ancak yorum yaptığınız makaleyi okumak zorundasınız. Size nasıl "kaynadığını" göz önünde bulundurarak, gerçekten okumadığınız çok güçlü görünüyor. (Maalesef bazı makalelerin her ikisine de yayıldığı için, bağlantılı makalelerin her ikisini de okumanız gerekebilir.) Dilin uygulanmasıyla hemen hemen hiçbir ilgisi yoktur, ancak amaçlanan semantikle ilgisi vardır.
Veky

179

Kuyruk çağrısı optimizasyonu gerçekleştiren bir modül yayınladım (hem kuyruk özyinelemeyi hem de devam geçen stili işliyor): https://github.com/baruchel/tco

Python'da kuyruk yinelemeyi optimize etme

Kuyruk özyinelemenin Pythonic kodlama yöntemine uymadığı ve bir döngüye nasıl gömüleceğinin umurunda olmaması gerektiği sıklıkla iddia edilmiştir. Bu bakış açısıyla tartışmak istemiyorum; ancak bazen çeşitli nedenlerden dolayı döngülerden ziyade yeni fikirleri kuyruk özyinelemeli işlevler olarak denemeyi veya uygulamayı seviyorum (sadece süreçte yirmi kısa işlev yerine üç "Piton" işlevler, kodumu düzenlemek yerine etkileşimli bir oturumda çalışma vb.).

Python'da kuyruk özyinelemeyi optimize etmek aslında oldukça kolaydır. İmkansız veya çok zor olduğu söylense de, zarif, kısa ve genel çözümlerle elde edilebileceğini düşünüyorum; Hatta bu çözümlerin çoğunun olması gerekenden başka Python özelliklerini kullanmadığını düşünüyorum. Çok standart döngülerle birlikte çalışan temiz lambda ifadeleri, kuyruk özyineleme optimizasyonunu uygulamak için hızlı, verimli ve tamamen kullanılabilir araçlara yol açar.

Kişisel bir rahatlık olarak, böyle bir optimizasyonu iki farklı şekilde uygulayan küçük bir modül yazdım. Burada iki ana fonksiyonum hakkında konuşmak istiyorum.

Temiz yol: Y birleştiriciyi değiştirmek

Y, combinator iyi bilinmektedir; lambda işlevlerini özyinelemeli bir şekilde kullanmaya izin verir, ancak özyinelemeli çağrıları bir döngüye katıştırmaya izin vermez. Lambda hesabı böyle bir şey yapamaz. Ancak Y birleştiricisindeki küçük bir değişiklik, değerlendirilecek yinelemeli çağrıyı koruyabilir. Bu nedenle değerlendirme gecikebilir.

İşte Y birleştiricisinin ünlü ifadesi:

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

Çok küçük bir değişiklikle şunları elde edebilirim:

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

Kendini çağırmak yerine, f işlevi artık aynı çağrıyı gerçekleştiren bir işlevi döndürür, ancak döndürdüğü için değerlendirme daha sonra dışarıdan yapılabilir.

Kodum:

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

Fonksiyon şu şekilde kullanılabilir; faktöriyel ve Fibonacci'nin kuyruk özyinelemeli versiyonlarına sahip iki örnek:

>>> 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

Açıkçası özyineleme derinliği artık sorun değil:

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

Bu elbette işlevin tek gerçek amacıdır.

Bu optimizasyon ile sadece bir şey yapılamaz: başka bir işleve göre değerlendirilen kuyruk özyinelemeli işlevle kullanılamaz (bu, çağrılabilir döndürülen nesnelerin hepsinin, hiçbir ayrım olmadan daha fazla özyinelemeli çağrılar olarak ele alınmasından kaynaklanır). Genellikle böyle bir özelliğe ihtiyacım olmadığından, yukarıdaki koddan çok memnunum. Bununla birlikte, daha genel bir modül sağlamak için, bu sorun için bir çözüm bulmak için biraz daha düşündüm (bir sonraki bölüme bakın).

Bu sürecin hızı ile ilgili olarak (ancak asıl mesele bu değildir), oldukça iyi olur; kuyruk özyinelemeli işlevler, daha basit ifadeler kullanılarak aşağıdaki koddan çok daha hızlı değerlendirilir:

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

Bir ifadeyi değerlendirmenin, hatta karmaşık bile olsa, bu ikinci versiyonda olduğu gibi, birkaç basit ifadeyi değerlendirmekten çok daha hızlı olduğunu düşünüyorum. Bu yeni işlevi modülümde tutmadım ve "resmi" işlevden ziyade kullanılabileceği hiçbir durum görmüyorum.

İstisnalar dışında devam tarzı

İşte daha genel bir işlev; diğer işlevleri döndürenler de dahil olmak üzere tüm kuyruk özyinelemeli işlevleri işleyebilir. Yinelemeli çağrılar, istisnalar kullanılarak diğer dönüş değerlerinden tanınır. Bu çözümler öncekinden daha yavaştır; daha hızlı bir kod muhtemelen ana döngüde "bayraklar" algılanıyor gibi bazı özel değerler kullanılarak yazılabilir, ancak özel değerler veya dahili anahtar kelimeler kullanma fikrini sevmiyorum. İstisnaları kullanmanın komik bir yorumu vardır: Python kuyruk yinelemeli çağrıları sevmiyorsa, kuyruk yinelemeli bir çağrı meydana geldiğinde bir istisna ortaya çıkarılmalı ve Pythonic yolu, bazı temiz bulmak için istisnayı yakalamak olacaktır. çözüm, aslında burada olan şey ...

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

Artık tüm fonksiyonlar kullanılabilir. Aşağıdaki örnekte, f(n)n'nin herhangi bir pozitif değeri için kimlik fonksiyonu değerlendirilir:

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

Tabii ki, istisnaların yorumlayıcıyı kasıtlı olarak yeniden yönlendirmek için kullanılması amaçlanmadığı söylenebilir (bir tür gotoifade veya muhtemelen bir çeşit devam tarzı), itiraf etmeliyim. Ama yine de, trytek bir satırla ifade olarak kullanma fikrini komik buluyorum return: bir şey döndürmeye çalışıyoruz (normal davranış), ancak yinelenen bir çağrı nedeniyle (istisna) yapamayız.

İlk cevap (2013-08-29).

Kuyruk özyineleme işlemek için çok küçük bir eklenti yazdım. Burada yaptığım açıklamalarla bulabilirsiniz: https://groups.google.com/forum/?hl=fr#!topic/comp.lang.python/dIsnJ2BoBKs

Kuyruk özyineleme stiliyle yazılmış bir lambda işlevini, bir döngü olarak değerlendirecek başka bir işleve gömer.

Bu küçük fonksiyondaki en ilginç özellik, benim düşünceme göre, fonksiyonun kirli programlama hackine değil, sadece lambda kalkülüsüne dayanmasıdır: başka bir lambda fonksiyonuna eklendiğinde fonksiyonun davranışı başka birine değiştirilir. Y birleştiricisine çok benziyor.


Lütfen, yönteminizi kullanarak bazı koşullara bağlı olarak diğer birkaç işlevden birini çağıran bir işlevi (tercihen normal bir tanıma benzer şekilde) tanımlamak için bir örnek verebilir misiniz? Ayrıca, sarma fonksiyonunuz bet0sınıf yöntemleri için dekoratör olarak kullanılabilir mi?
Alexey

@Alexey Bir yorumun içine blok tarzında kod yazabileceğimden emin değilim, ancak elbette defişlevleriniz için sözdizimini kullanabilirsiniz ve aslında yukarıdaki son örnek bir koşula dayanmaktadır. Benim yazı baruchel.github.io/python/2015/11/07/… "Herhalde tanım sözdizimi ile bir örnek vermek istiyorum" Tabii ki kimsenin böyle bir kod yazamazsınız "ile başlayan bir paragraf görebilirsiniz. Sorunuzun ikinci kısmı için, bir süredir zaman geçirmediğim için biraz daha düşünmeliyim. Saygılarımızla.
Thomas Baruchel

TCO olmayan bir dil uygulaması kullanıyor olsanız bile, özyinelemeli çağrının işlevinizde nerede gerçekleştiğine dikkat etmelisiniz. Bunun nedeni, işlevin özyinelemeli çağrıdan sonra oluşan bölümünün yığın üzerinde depolanması gereken bölüm olmasıdır. Bu nedenle, kuyruk özyinelemesini yapmak, özyinelemeli çağrı başına depolamanız gereken bilgi miktarını en aza indirir, bu da gerekirse büyük özyinelemeli çağrı yığınlarına sahip olmanız için daha fazla alan sağlar.
Josiah

21

Guido kelimesi şu adrestedir : http://neopythonic.blogspot.co.uk/2009/04/tail-recursion-elimination.html

Kısa süre önce Python Geçmişi bloguma Python'un işlevsel özelliklerinin kökenleri hakkında bir yazı gönderdim. Kuyruk özyineleme eliminasyonunu (TRE) desteklememeye ilişkin bir yan açıklama, Python'un Python'a eklenebileceğini "kanıtlamaya" çalışan başkalarının son blog girişlerine bağlantılar da dahil olmak üzere Python'un bunu yapmaması ne yazık ki hakkında birkaç yorum başlattı. kolayca. Öyleyse konumumu savunmama izin verin (yani dilde TRE istemiyorum). Kısa bir cevap almak istiyorsanız, bu sadece basit. İşte uzun cevap:


12
Ve burada BDsFL denilen sorun yatıyor.
Adam Donahue

6
@AdamDonahue bir komiteden alınan her karardan memnun kaldınız mı? En azından BDFL'den mantıklı ve yetkili bir açıklama alırsınız.
Mark Ransom

2
Hayır, elbette hayır, ama onlar bana daha dengeli davranıyorlar. Bu bir tanımlayıcıdan değil, bir kuralcıdan. İroni.
Adam Donahue

6

CPython , Guido van Rossum'un konuyla ilgili açıklamalarına dayanarak kuyruk çağrı optimizasyonunu desteklemez ve muhtemelen desteklemez .

Yığın izlemesini nasıl değiştirdiğinden dolayı hata ayıklamayı daha zor hale getirdiğine dair argümanlar duydum.


18
@mux CPython, Python programlama dilinin referans uygulamasıdır. Aynı dili uygulayan ancak uygulama ayrıntılarında farklılık gösteren başka uygulamalar da (PyPy, IronPython ve Jython gibi) vardır. Ayrım burada yararlıdır çünkü (teoride) TCO'yu yapan alternatif bir Python uygulaması oluşturmak mümkündür. Ben bile olsa düşünen kimsenin farkında değilim ve koduna güvenen diğer Python uygulamaları kırmak gibi yararlılık sınırlı olacaktır.


2

Kuyruk yinelemesini optimize etmenin yanı sıra, yineleme derinliğini manuel olarak şu şekilde ayarlayabilirsiniz:

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

5
Neden sadece jQuery kullanmıyorsunuz?
Jeremy Hert

5
O Çünkü aynı zamanda TCO'nuzu sunmuyor? :-D stackoverflow.com/questions/3660577/…
Veky
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.