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 goto
ifade veya muhtemelen bir çeşit devam tarzı), itiraf etmeliyim. Ama yine de, try
tek 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.