ฉันเผยแพร่โมดูลที่ใช้การเพิ่มประสิทธิภาพการโทรหาง (จัดการทั้งการเรียกซ้ำแบบหางและการส่งต่อแบบต่อเนื่อง): https://github.com/baruchel/tco
ปรับหางคำสั่งซ้ำให้เหมาะสมใน Python
มันมักถูกอ้างว่า tail-recursion ไม่เหมาะกับวิธีการเข้ารหัสแบบ Pythonic และคนเราไม่ควรสนใจว่าจะฝังมันไว้ในวงวนอย่างไร ฉันไม่ต้องการโต้เถียงกับมุมมองนี้ บางครั้งฉันชอบลองหรือนำความคิดใหม่มาใช้เป็นฟังก์ชั่นแบบเรียกซ้ำแทนที่จะวนซ้ำด้วยเหตุผลหลาย ๆ อย่าง (เน้นที่ความคิดมากกว่าในกระบวนการโดยมีฟังก์ชั่นสั้น ๆ ยี่สิบหน้าจอในเวลาเดียวกันมากกว่าสาม "Pythonic" ฟังก์ชั่นที่ทำงานในเซสชันแบบโต้ตอบแทนการแก้ไขรหัส ฯลฯ )
การเพิ่มประสิทธิภาพการเรียกซ้ำแบบหางใน Python นั้นง่ายมาก ในขณะที่มีการกล่าวถึงว่าเป็นไปไม่ได้หรือยุ่งยากมากฉันคิดว่ามันสามารถทำได้ด้วยวิธีแก้ปัญหาที่หรูหราสั้นและทั่วไป ฉันคิดว่าโซลูชันเหล่านี้ส่วนใหญ่ไม่ใช้คุณสมบัติ Python อย่างอื่นที่ควรทำ การแสดงออกแลมบ์ดาที่สะอาดทำงานพร้อมกับลูปมาตรฐานมากนำไปสู่เครื่องมือที่รวดเร็วมีประสิทธิภาพและใช้งานได้อย่างเต็มที่สำหรับการปรับใช้การปรับหางแบบเรียกซ้ำ
เพื่อความสะดวกส่วนตัวฉันเขียนโมดูลขนาดเล็กที่ใช้การเพิ่มประสิทธิภาพดังกล่าวด้วยสองวิธีที่แตกต่างกัน ฉันอยากจะพูดคุยเกี่ยวกับหน้าที่หลักของฉันที่นี่
วิธีที่สะอาด: แก้ไข Y combinator
Y Combinatorเป็นที่รู้จักกันดี; อนุญาตให้ใช้ฟังก์ชัน lambda ในลักษณะวนซ้ำ แต่ไม่อนุญาตให้ฝังการเรียกซ้ำแบบวนซ้ำ แลมบ์ดาแคลคูลัสเพียงอย่างเดียวไม่สามารถทำสิ่งนี้ได้ การเปลี่ยนแปลงเล็กน้อยใน Y combinator แต่สามารถป้องกันการโทรซ้ำเพื่อรับการประเมินจริง การประเมินผลอาจล่าช้า
นี่คือการแสดงออกที่มีชื่อเสียงสำหรับ Y combinator:
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
ฟังก์ชั่นสามารถใช้งานได้ด้วยวิธีดังต่อไปนี้ ต่อไปนี้เป็นตัวอย่างสองตัวอย่างที่มีแฟกทอเรียลและ 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
ความชัดลึกของการเรียกซ้ำไม่ได้เป็นปัญหาอีกต่อไป:
>>> bet( lambda f: lambda n: 42 if not n else f(n-1) )(50000)
42
นี่เป็นจุดประสงค์ที่แท้จริงของฟังก์ชัน
มีเพียงสิ่งเดียวเท่านั้นที่ไม่สามารถทำได้ด้วยการปรับให้เหมาะสมนี้: ไม่สามารถใช้กับฟังก์ชันแบบเรียกซ้ำ (recursive function) ที่ประเมินเป็นฟังก์ชั่นอื่นได้ (นี่มาจากข้อเท็จจริงที่ว่าวัตถุที่ส่งคืนได้ เนื่องจากปกติฉันไม่ต้องการฟีเจอร์ดังกล่าวฉันจึงมีความสุขกับโค้ดด้านบน อย่างไรก็ตามเพื่อให้เป็นโมดูลทั่วไปฉันคิดอีกเล็กน้อยเพื่อหาวิธีแก้ปัญหาสำหรับปัญหานี้ (ดูหัวข้อถัดไป)
เกี่ยวกับความเร็วของกระบวนการนี้ (ซึ่งไม่ใช่เรื่องจริงอย่างไรก็ตาม) มันค่อนข้างดี ฟังก์ชั่น 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
ฉันคิดว่าการประเมินหนึ่งนิพจน์แม้จะซับซ้อนนั้นเร็วกว่าการประเมินนิพจน์ง่าย ๆ หลายอย่างซึ่งเป็นกรณีในรุ่นที่สองนี้ ฉันไม่ได้รักษาฟังก์ชั่นใหม่นี้ไว้ในโมดูลของฉันและฉันไม่เห็นสถานการณ์ใดที่จะสามารถใช้งานได้มากกว่าที่จะเป็น "ทางการ"
สไตล์การส่งต่อที่มีข้อยกเว้น
นี่คือฟังก์ชั่นทั่วไปมากขึ้น มันสามารถจัดการฟังก์ชั่นหางแบบวนซ้ำทั้งหมดรวมถึงฟังก์ชันที่ส่งคืนฟังก์ชันอื่น การเรียกซ้ำจะรับรู้จากค่าตอบแทนอื่น ๆ โดยใช้ข้อยกเว้น โซลูชันนี้ช้ากว่าโซลูชันก่อนหน้า รหัสที่รวดเร็วกว่าอาจจะเขียนได้โดยใช้ค่าพิเศษบางอย่างเมื่อตรวจพบ "ค่าสถานะ" ในลูปหลัก แต่ฉันไม่ชอบแนวคิดของการใช้ค่าพิเศษหรือคำหลักภายใน มีการตีความที่ตลกเกี่ยวกับการใช้ข้อยกเว้น: ถ้า Python ไม่ชอบการโทรซ้ำแบบเรียกซ้ำควรมีการยกข้อยกเว้นเมื่อมีการโทรแบบเรียกซ้ำแบบหางเกิดขึ้นและวิธี Pythonic จะจับข้อยกเว้นเพื่อค้นหาความสะอาด ทางออกซึ่งเป็นสิ่งที่เกิดขึ้นจริงที่นี่ ...
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
มันสามารถฝังฟังก์ชั่นแลมบ์ดาที่เขียนด้วยรูปแบบการเรียกซ้ำหางในฟังก์ชันอื่นซึ่งจะประเมินว่าเป็นลูป
คุณสมบัติที่น่าสนใจที่สุดในฟังก์ชั่นเล็ก ๆ นี้ในความเห็นที่ต่ำต้อยของฉันคือฟังก์ชั่นนี้ไม่ได้อาศัยแฮ็คโปรแกรมที่สกปรก แต่อยู่ในแคลคูลัสแลมบ์ดาเพียง: พฤติกรรมของฟังก์ชั่นนั้นเปลี่ยนไปเป็นอีกฟังก์ชันหนึ่ง ดูเหมือน combinator Y มาก