Tôi đã xuất bản một mô-đun thực hiện tối ưu hóa cuộc gọi đuôi (xử lý cả kiểu đệ quy đuôi và kiểu chuyển tiếp liên tục): https://github.com/baruchel/tco
Tối ưu hóa đệ quy đuôi trong Python
Người ta thường tuyên bố rằng đệ quy đuôi không phù hợp với cách mã hóa của Pythonic và người ta không nên quan tâm đến cách nhúng nó vào một vòng lặp. Tôi không muốn tranh luận với quan điểm này; tuy nhiên đôi khi tôi thích thử hoặc thực hiện các ý tưởng mới dưới dạng các hàm đệ quy hơn là các vòng lặp vì nhiều lý do (tập trung vào ý tưởng hơn là vào quá trình, có hai mươi chức năng ngắn trên màn hình của tôi cùng một lúc thay vì chỉ có ba "Pythonic" các chức năng, làm việc trong một phiên tương tác thay vì chỉnh sửa mã của tôi, v.v.).
Tối ưu hóa đệ quy đuôi trong Python trên thực tế khá dễ dàng. Trong khi nó được cho là không thể hoặc rất khó, tôi nghĩ nó có thể đạt được bằng các giải pháp thanh lịch, ngắn gọn và chung chung; Tôi thậm chí nghĩ rằng hầu hết các giải pháp này không sử dụng các tính năng của Python nếu không. Các biểu thức lambda sạch làm việc cùng với các vòng lặp rất chuẩn dẫn đến các công cụ nhanh chóng, hiệu quả và hoàn toàn có thể sử dụng để thực hiện tối ưu hóa đệ quy đuôi.
Để thuận tiện cho cá nhân, tôi đã viết một mô-đun nhỏ thực hiện tối ưu hóa như vậy bằng hai cách khác nhau. Tôi muốn thảo luận ở đây về hai chức năng chính của tôi.
Cách sạch sẽ: sửa đổi tổ hợp Y
Tổ hợp Y được biết đến nhiều; nó cho phép sử dụng các hàm lambda theo cách đệ quy, nhưng bản thân nó không cho phép nhúng các cuộc gọi đệ quy trong một vòng lặp. Lambda tính toán một mình không thể làm một điều như vậy. Tuy nhiên, một thay đổi nhỏ trong bộ kết hợp Y có thể bảo vệ cuộc gọi đệ quy được đánh giá thực sự. Đánh giá do đó có thể bị trì hoãn.
Đây là biểu thức nổi tiếng cho tổ hợp Y:
lambda f: (lambda x: x(x))(lambda y: f(lambda *args: y(y)(*args)))
Với một thay đổi rất nhỏ, tôi có thể nhận được:
lambda f: (lambda x: x(x))(lambda y: f(lambda *args: lambda: y(y)(*args)))
Thay vì tự gọi, hàm f bây giờ trả về một hàm thực hiện cùng một cuộc gọi, nhưng vì nó trả về nó, nên việc đánh giá có thể được thực hiện sau đó từ bên ngoài.
Mã của tôi là:
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
Các chức năng có thể được sử dụng theo cách sau; Dưới đây là hai ví dụ với các phiên bản đệ quy đuôi của giai thừa và Fibonacci:
>>> 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
Rõ ràng độ sâu đệ quy không còn là vấn đề nữa:
>>> bet( lambda f: lambda n: 42 if not n else f(n-1) )(50000)
42
Tất nhiên đây là mục đích thực sự duy nhất của chức năng.
Chỉ có một điều không thể thực hiện được với tối ưu hóa này: nó không thể được sử dụng với hàm đệ quy đuôi đánh giá chức năng khác (điều này xuất phát từ thực tế là các đối tượng trả về có thể gọi được đều được xử lý như các cuộc gọi đệ quy tiếp theo không có sự phân biệt). Vì tôi thường không cần một tính năng như vậy, tôi rất hài lòng với mã ở trên. Tuy nhiên, để cung cấp một mô-đun tổng quát hơn, tôi đã suy nghĩ thêm một chút để tìm ra cách giải quyết cho vấn đề này (xem phần tiếp theo).
Liên quan đến tốc độ của quá trình này (tuy nhiên không phải là vấn đề thực sự), nó xảy ra khá tốt; Các hàm đệ quy đuôi thậm chí được đánh giá nhanh hơn nhiều so với mã sau sử dụng các biểu thức đơn giản hơn:
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
Tôi nghĩ rằng việc đánh giá một biểu thức, thậm chí phức tạp, nhanh hơn nhiều so với việc đánh giá một số biểu thức đơn giản, đó là trường hợp trong phiên bản thứ hai này. Tôi đã không giữ chức năng mới này trong mô-đun của mình và tôi thấy không có trường hợp nào nó có thể được sử dụng thay vì "chính thức".
Phong cách tiếp tục vượt qua với ngoại lệ
Đây là một chức năng tổng quát hơn; nó có thể xử lý tất cả các hàm đệ quy đuôi, bao gồm cả các hàm trả về các hàm khác. Các cuộc gọi đệ quy được nhận ra từ các giá trị trả về khác bằng cách sử dụng các ngoại lệ. Giải pháp này chậm hơn giải pháp trước; một mã nhanh hơn có thể được viết bằng cách sử dụng một số giá trị đặc biệt là "cờ" được phát hiện trong vòng lặp chính, nhưng tôi không thích ý tưởng sử dụng các giá trị đặc biệt hoặc từ khóa nội bộ. Có một số cách giải thích hài hước về việc sử dụng các ngoại lệ: nếu Python không thích các cuộc gọi đệ quy đuôi, một ngoại lệ sẽ được đưa ra khi một cuộc gọi đệ quy đuôi xảy ra và cách Pythonic sẽ bắt ngoại lệ để tìm ra một số ngoại lệ giải pháp, đó thực sự là những gì xảy ra ở đây ...
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
Bây giờ tất cả các chức năng có thể được sử dụng. Trong ví dụ sau, f(n)
được ước tính cho hàm nhận dạng cho bất kỳ giá trị dương nào của n:
>>> f = bet0( lambda f: lambda n: (lambda x: x) if not n else f(n-1) )
>>> f(5)(42)
42
Tất nhiên, có thể lập luận rằng các trường hợp ngoại lệ không nhằm mục đích sử dụng cho việc cố ý chuyển hướng trình thông dịch (như một loại goto
tuyên bố hoặc có thể là một kiểu chuyển tiếp tiếp tục), mà tôi phải thừa nhận. Nhưng, một lần nữa, tôi thấy buồn cười ý tưởng sử dụng try
với một dòng duy nhất là mộtreturn
tuyên bố: chúng tôi cố gắng trả lại một cái gì đó (hành vi bình thường) nhưng chúng tôi không thể thực hiện được vì một cuộc gọi đệ quy xảy ra (ngoại lệ).
Câu trả lời ban đầu (2013-08-29).
Tôi đã viết một plugin rất nhỏ để xử lý đệ quy đuôi. Bạn có thể tìm thấy nó với lời giải thích của tôi ở đó: https://groups.google.com/forum/?hl=fr#!topic/comp.lang.python/dIsnJ2BoBKs
Nó có thể nhúng một hàm lambda được viết với kiểu đệ quy đuôi trong một hàm khác sẽ đánh giá nó như một vòng lặp.
Theo tôi, tính năng thú vị nhất trong hàm nhỏ này là hàm không dựa vào một số hack lập trình bẩn mà chỉ dựa trên phép tính lambda: hành vi của hàm được thay đổi thành hàm khác khi được chèn vào hàm lambda khác. trông rất giống tổ hợp Y.