[英]Is it advisable to write recursive functions in Python
我已經在python中編寫了一個verilog(邏輯門及其連接描述)模擬器作為實驗的一部分。
我遇到了堆棧限制的問題,所以我做了一些閱讀,發現Python沒有“尾調用優化”功能(即隨着遞歸的進行動態刪除堆棧條目)
我在這方面主要有兩個問題:
1) 如果我將堆棧限制提升到sys.setrecursionlimit(15000)
它是否會影響性能(內存 - 我不在乎)?
2) 我有什么方法可以繞過這個限制,假設我可以沒有堆棧跟蹤。
我問這個是因為Verilog主要處理可以使用遞歸函數以優雅方式實現的狀態機。
另外,如果我可以添加,在遞歸函數調用的情況下,如果有錯誤,我更依賴於導致此錯誤的輸入而不是堆棧跟蹤。
我是Python新手,所以也許專家可能認為Python堆棧跟蹤對調試遞歸函數調用非常有用......如果是這種情況,我會非常樂意學習如何做到這一點。
最后,建議在Python中編寫遞歸函數還是應該轉移到其他語言?
如果有任何解決方法,我可以繼續使用python進行遞歸函數,我想知道是否有任何性能影響(我可以做分析)。
2)我有什么方法可以繞過這個限制,假設我可以沒有堆棧跟蹤。 我問這個是因為Verilog主要處理可以使用遞歸函數以優雅方式實現的狀態機。
有一種方法可以避免尾調用而不會過多地更改現有邏輯,只需重寫尾調用以返回thunk並使用trampoline來調用thunk。 如果需要在轉換之間傳遞復雜狀態,可以使用連續傳遞樣式來傳遞它們。 這種編寫代碼的方式非常適合編寫狀態機。
一個例子可能更清楚,假設您從fizzbuzz狀態機的這種遞歸實現開始,該狀態機使用尾調用將控制傳遞到下一個轉換:
def start():
return increment(0)
def fizz(n):
print 'fizz'
return increment(n)
def buzz(n):
print 'buzz'
return increment(n)
def fizzbuzz(n):
print 'fizzbuzz'
return increment(n)
def increment(n):
n = n + 1
if n > 100:
return terminate()
elif n % 3 == 0 and n % 5 == 0:
return fizzbuzz(n)
elif n % 3 == 0:
return fizz(n)
elif n % 5 == 0:
return buzz(n)
else:
print n
return increment(n)
def terminate():
raise StopIteration
try:
start()
except StopIteration:
pass
要避免尾調用,只需將所有尾調用包裝在lambda(或者functools.partial)中並添加一個trampoline:
def start():
return lambda: increment(0)
def fizz(n):
print 'fizz'
return lambda: increment(n)
def buzz(n):
print 'buzz'
return lambda: increment(n)
def fizzbuzz(n):
print 'fizzbuzz'
return lambda: increment(n)
def increment(n):
n = n + 1
if n > 2000:
# strictly speaking, transitions that takes no arguments
# like terminate don't need to be wrapped in lambda
# this is added here only for symmetry with others
return lambda: terminate()
elif n % 3 == 0 and n % 5 == 0:
return lambda: fizzbuzz(n)
elif n % 3 == 0:
return lambda: fizz(n)
elif n % 5 == 0:
return lambda: buzz(n)
else:
print n
return lambda: increment(n)
def terminate():
raise StopIteration
def trampoline(func):
try:
while True:
func = func()
except StopIteration:
pass
trampoline(lambda: start())
現在你可以有更多的fizzbuzz而不會達到遞歸限制。
很大程度上取決於您嘗試實現的遞歸解決方案的特定性質。 讓我舉一個具體的例子。 假設您想要列表中所有值的總和。 您可以通過將第一個值添加到列表其余部分的總和來設置遞歸 - 遞歸應該是顯而易見的。 但是,遞歸子問題僅比原始問題小1,因此遞歸堆棧將增長到與列表中的項目數一樣大。 對於大型列表,這將是一個問題。 另一種遞歸是要注意所有值的總和是列表的前半部分加上列表后半部分的總和。 同樣,遞歸應該是顯而易見的,並且終止條件是當您到達長度為1的子列表時。但是,對於此版本,堆棧將僅增長為列表大小的log 2 ,並且您可以處理沒有堆棧的巨大列表問題。 並非所有問題都可以考慮到一半大小的子問題,但是當你可以這樣做時,這是避免堆棧溢出情況的好方法。
如果遞歸解決方案是尾遞歸,則可以輕松轉換為循環而不是遞歸調用。
如果沒有尾遞歸,另一種可能性是使用循環實現事物並在顯式堆棧上顯式存儲中間狀態。
請參閱Python優化尾遞歸?
Guido Van Rossum表示,使用大量遞歸是“簡單的非語音”: http ://neopythonic.blogspot.co.uk/2009/04/tail-recursion-elimination.html
但無論如何,許多人都試圖破解自己的支持。 例如http://tomforb.es/adding-tail-call-optimization-to-python 。 或者只是google“python tail call”
注意:這個答案僅限於您最關心的問題,即“在Python中編寫遞歸函數是否明智?”。
簡短的回答是否定的,這不是“明智的”。 在沒有尾調用優化的情況下,考慮到內存和處理器時間上的強大函數調用,遞歸在Python中會變得非常緩慢。 只要有可能,最好迭代地重寫代碼。
特別針對標記為1)的問題,更改遞歸限制是危險的,因為它可能允許底層C堆棧溢出。 另請參閱此問題: Python中的最大遞歸深度是多少,以及如何增加它?
我使用sys.setrecursionlimit
將遞歸限制設置為其最大可能值,因為我遇到了大類/函數達到默認最大遞歸深度的問題。 為遞歸限制設置較大的值不應該影響腳本的性能,即如果它在高遞增和低遞歸限制下完成,則需要相同的時間才能完成。 唯一的區別是,如果你有一個低遞歸限制,它會阻止你做愚蠢的事情(比如運行無限遞歸循環)。 有了一個上限,而不是達到極限,一個使用遞歸過多的非常低效的腳本將永遠運行(或直到它耗盡內存,具體取決於任務)。
正如其他答案更詳細地解釋的那樣,大多數情況下,除了一系列遞歸調用之外,還有一種更快的方式來做你正在做的事情。
我看到裝飾器試圖在python中實現尾遞歸,所以我自己就把它刺了一下。 這是一個純粹的python(沒有sys._getframe)尾遞歸優化實現,它允許相互遞歸。
class TailRecurseException(Exception):
def __init__(self, func, args, kwargs):
self.func = func
self.args = args
self.kwargs = kwargs
def tail_recursive(g, rec=[]):
def func(*args, **kwargs):
if g in rec:
raise TailRecurseException(g, args, kwargs)
rec.append( g )
while True:
try:
r = g(*args, **kwargs)
rec.remove( g )
return r
except TailRecurseException, e:
if e.func==g:
args = e.args
kwargs = e.kwargs
else:
rec.remove( g )
raise e
return func
@tail_recursive
def g(n):
if n==0:
return 0
else:
return f(n-1)
@tail_recursive
def f(n):
if n == 0:
return 0
else:
return g(n-1)
print f(100000)
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.