파이썬은 꼬리 재귀를 최적화합니까?


206

다음 오류로 실패하는 다음 코드 조각이 있습니다.

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
@Wessie TCO는 언어가 얼마나 역동적이거나 정적인지 간단하게 고려합니다. 예를 들어 루아도 마찬가지입니다. 테일 호출 (AST 수준과 바이트 코드 수준 모두에서 매우 간단 함)을 인식 한 다음 새 스택을 만드는 대신 현재 스택 프레임을 재사용하면됩니다 (기본 코드보다 인터프리터에서 더 단순하고 더 단순함). .

11
아, 하나의 nitpick : 꼬리 재귀에 대해 독점적으로 이야기하지만 꼬리 호출 최적화 를 의미 하고 재귀 여부에 관계없이 (명시 적으로 또는 암시 적으로) 모든 인스턴스에 적용되는 약어 "TCO"를 사용하십시오 return func(...). TCO는 TRE의 적절한 상위 집합이며보다 유용합니다 (예 : TRE로는 불가능한 연속 전달 스타일을 실현 가능하게 함). 구현하기가 훨씬 어렵지 않습니다.

1
다음은이를 구현하는 해킹 방법입니다. 예외 발생을 사용하여 실행 프레임을 버리는 데코레이터 : metapython.blogspot.com.br/2010/11/…
jsbueno

2
꼬리 재귀로 자신을 제한하면 적절한 역 추적이 유용하다고 생각하지 않습니다. 당신은 전화 foo내부에서 전화 foo내부 foo에서 내부로 전화를 걸었습니다 foo... 나는 유용한 정보가 이것을 잃어 버릴 것이라고 생각하지 않습니다.
Kevin

1
나는 최근 코코넛 에 대해 배웠지 만 아직 시도하지 않았습니다. 살펴볼 가치가 있습니다. 꼬리 재귀 최적화가 있다고 주장합니다.
Alexey

답변:


215

Guido van Rossum 은 적절한 역 추적을 선호하기 때문에 결코 그렇지 않습니다 .

꼬리 재귀 제거 (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)?
Jon Clements

38
이 특정 예제에서 작동하는 @JonClements. while 루프로의 변환은 일반적인 경우 꼬리 재귀에 적용됩니다.
John La Rooy 2013 년

25
+1 정답은되었지만, 이것은 엄청나게 골머리 디자인 결정처럼 보입니다. 주어진 이유 에 졸이다 것 같다 "는 파이썬 해석하는 방법을 제공 할 수있는 어려운 내가 그렇게 거기에 어쨌든 좋아하지 않아!"
기본

12
@jwg 그래서 ... 뭐? 열악한 디자인 결정에 대해 언급하기 전에 언어를 작성해야합니까? 논리적이거나 실용적으로 보이지 않습니다. 귀하의 의견으로는 귀하가 작성한 언어의 기능 (또는 기능이 없음)에 대한 의견이 없다고 생각하십니까?
기본

2
@Basic 아니요,하지만 댓글을 달고있는 기사를 읽어야합니다. 그것이 당신에게 어떻게 "끓어 오르는 지"고려할 때, 당신이 실제로 그것을 읽지 않은 것이 매우 강력 해 보입니다. (불행히도, 일부 논증이 둘 다에 퍼져 있기 때문에 실제로는 링크 된 기사를 모두 읽어야 할 수도 있습니다.) 언어 구현과는 거의 관련이 없지만 의도 된 의미와 관련이 있습니다.
Veky

178

꼬리 호출 최적화 (꼬리 재귀 및 연속 전달 스타일 모두 처리)를 수행하는 모듈을 게시했습니다. https://github.com/baruchel/tco

파이썬에서 꼬리 재귀 최적화

꼬리 재귀는 파이썬의 코딩 방식에 적합하지 않으며 루프에 포함시키는 방법에 신경 쓰지 않아야한다고 주장되었습니다. 나는이 견해로 논쟁하고 싶지 않다. 그러나 때로는 여러 가지 이유로 루프가 아닌 꼬리 재귀 함수로 새로운 아이디어를 시도하거나 구현하는 것을 좋아합니다 (프로세스가 아닌 아이디어에 초점을 맞추고 3 개의 "Pythonic"이 아니라 동시에 화면에 20 개의 짧은 기능이 있음) 기능, 내 코드 편집 등이 아닌 대화식 세션에서 작업).

파이썬에서 꼬리 재귀를 최적화하는 것은 실제로 매우 쉽습니다. 불가능하거나 매우 까다 롭다고 말하지만, 우아하고 짧고 일반적인 솔루션으로 달성 할 수 있다고 생각합니다. 심지어 이러한 솔루션의 대부분은 Python 기능을 다른 용도로 사용하지 않는다고 생각합니다. 매우 표준적인 루프와 함께 작동하는 깨끗한 람다 식은 꼬리 재귀 최적화를 구현하기위한 빠르고 효율적이며 완벽하게 사용할 수있는 도구로 이어집니다.

개인적 편의를 위해 두 가지 방법으로 이러한 최적화를 구현하는 작은 모듈을 작성했습니다. 두 가지 주요 기능에 대해 여기에서 논의하고 싶습니다.

깨끗한 방법 : Y 조합기 수정

Y 연결자은 잘 알려져있다 람다 함수를 재귀 방식으로 사용할 수는 있지만 반복 호출을 루프에 포함시킬 수는 없습니다. 람다 미적분학만으로는 그런 일을 할 수 없습니다. 그러나 Y 조합의 약간의 변화는 재귀 호출이 실제로 평가되도록 보호 할 수 있습니다. 따라서 평가가 지연 될 수 있습니다.

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

이것은 물론 함수의 단일 실제 목적입니다.

이 최적화로는 한 가지만 수행 할 수 없습니다. 다른 함수로 평가하는 꼬리 재귀 함수와 함께 사용할 수 없습니다 (이것은 호출 가능한 반환 객체가 모두 구별없이 추가 재귀 호출로 처리된다는 사실에서 비롯됩니다). 일반적으로 이러한 기능이 필요하지 않으므로 위의 코드에 매우 만족합니다. 그러나 더 일반적인 모듈을 제공하기 위해이 문제에 대한 해결 방법을 찾기 위해 조금 더 생각했습니다 (다음 섹션 참조).

이 프로세스의 속도와 관련하여 (그러나 실제 문제는 아님) 상당히 좋은 일입니다. tail-recursive 함수는 간단한 표현식을 사용하여 다음 코드보다 훨씬 빠르게 평가됩니다.

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에 대한 설명과 함께 찾을 수 있습니다.

꼬리 재귀 스타일로 작성된 람다 함수를 루프로 평가하는 다른 함수에 포함시킬 수 있습니다.

이 작은 함수에서 가장 흥미로운 특징은 겸손한 견해로는 함수가 더러운 프로그래밍 해킹에 의존하지 않고 람다 미적분학에만 의존한다는 것입니다. 함수의 동작은 다른 람다 함수에 삽입되면 다른 동작으로 변경됩니다. Y 결합기와 매우 비슷합니다.


메소드를 사용하여 어떤 조건에 따라 다른 여러 함수 중 하나를 테일 콜하는 함수 (바람직하게는 일반 정의와 유사한 방식으로)를 정의하는 예제를 제공해 주시겠습니까? 또한 랩핑 함수 bet0를 클래스 메소드의 데코레이터로 사용할 수 있습니까?
Alexey

@Alexey 주석 내에서 블록 스타일로 코드를 작성할 수 있는지 확실하지 않지만 def함수 의 구문을 사용할 수 있으며 실제로 위의 마지막 예는 조건에 의존합니다. 내 게시물 baruchel.github.io/python/2015/11/07/… 에서 "일반적으로 정의 구문으로 예제를 제공하는"아무도 그런 코드를 쓰지 않을 것 "이라고 시작하는 단락을 볼 수 있습니다. 귀하의 질문의 두 번째 부분을 위해, 나는 잠시 동안 그 시간을 보내지 않았기 때문에 그것에 대해 조금 더 생각해야합니다. 문안 인사.
토마스 Baruchel

비 TCO 언어 구현을 사용하는 경우에도 함수에서 재귀 호출이 발생하는 위치를주의해야합니다. 재귀 호출 후에 발생하는 함수 부분이 스택에 저장되어야하는 부분이기 때문입니다. 따라서 함수를 재귀 재귀로 만들면 재귀 호출마다 저장해야하는 정보의 양이 최소화되므로 필요한 경우 큰 재귀 호출 스택을 가질 수있는 더 많은 공간이 제공됩니다.
josiah

21

귀도의 단어는 http://neopythonic.blogspot.co.uk/2009/04/tail-recursion-elimination.html에 있습니다.

나는 최근 Python History 블로그에 Python의 기능적 기능의 기원에 대한 항목을 게시했습니다. TRE (tail recursion elimination)를 지원하지 않는다는 측면의 언급은 파이썬이 TRE를 파이썬에 추가 할 수 있음을 "증명"하려는 다른 사람들의 최근 블로그 항목에 대한 링크를 포함하여 파이썬이이를 수행하지 않는 것이 얼마나 유감인지에 대한 몇 가지 의견을 즉시 촉발했습니다. 용이하게. 내 입장을 지키자 (언어에서 TRE를 원하지 않는다). 짧은 대답을 원한다면 단순히 비유 론적입니다. 긴 대답은 다음과 같습니다.


12
그리고 여기에는 소위 BDsFL의 문제가 있습니다.
Adam Donahue

6
@AdamDonahue 귀하는위원회의 모든 결정에 완벽하게 만족하십니까? 적어도 당신은 BDFL로부터 합리적이고 권위있는 설명을 얻습니다.
Mark Ransom

2
물론 아닙니다. 그러나 그들은 더 고른 손으로 나를 때렸습니다. 이것은 서술자가 아닌 규범 주의자로부터 나온 것입니다. 아이러니
Adam Donahue

6

CPython은이 주제에 대한 Guido van Rossum의 진술에 근거한 테일 콜 최적화를 지원하지 않을 것입니다 .

스택 추적을 수정하는 방법으로 인해 디버깅이 더 어려워진다는 주장을 들었습니다.


18
@mux CPython은 프로그래밍 언어 Python의 참조 구현입니다. 동일한 언어를 구현하지만 구현 세부 사항이 다른 다른 구현 (예 : PyPy, IronPython 및 Jython)이 있습니다. 이론적으로 TCO를 수행하는 대체 Python 구현을 작성할 수 있기 때문에 구별이 유용합니다. 나는 그것에 대해 생각하는 사람조차 모르고 있으며, 그것에 의존하는 코드는 다른 모든 파이썬 구현에서 벗어날 수 있으므로 유용성이 제한됩니다.


2

테일 재귀를 최적화하는 것 외에도 다음을 통해 재귀 깊이를 수동으로 설정할 수 있습니다.

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

5
왜 jQuery를 사용하지 않습니까?
Jeremy Hert

5
TCO 제공하지 않기 때문에 ? :-D stackoverflow.com/questions/3660577/…
Veky
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.