Apakah Python mengoptimalkan rekursi ekor?

206

Saya memiliki potongan kode berikut yang gagal dengan kesalahan berikut:

RuntimeError: kedalaman rekursi maksimum terlampaui

Saya mencoba untuk menulis ulang ini untuk memungkinkan optimasi rekursi ekor (TCO). Saya percaya bahwa kode ini seharusnya berhasil jika TCO terjadi.

def trisum(n, csum):
    if n == 0:
        return csum
    else:
        return trisum(n - 1, csum + n)

print(trisum(1000, 0))

Haruskah saya menyimpulkan bahwa Python tidak melakukan semua jenis TCO, atau apakah saya hanya perlu mendefinisikannya secara berbeda?

Jordan Mack
sumber
11
@Wessie TCO adalah hal sederhana tentang seberapa dinamis atau statis bahasa tersebut. Lua, misalnya, juga melakukannya. Anda hanya perlu mengenali panggilan ekor (cukup sederhana, baik pada level AST dan pada level bytecode), dan kemudian menggunakan kembali frame stack saat ini alih-alih membuat yang baru (juga sederhana, sebenarnya lebih sederhana dalam interpreter daripada dalam kode asli) .
11
Oh, satu nitpick: Anda berbicara secara eksklusif tentang rekursi ekor, tetapi gunakan akronim "TCO", yang berarti optimisasi panggilan ekor dan berlaku untuk setiap instance return func(...)(secara eksplisit atau implisit), apakah itu rekursif atau tidak. TCO adalah superset TRE yang tepat, dan lebih bermanfaat (misalnya, membuat gaya kelanjutan lewat layak, yang TRE tidak bisa), dan tidak jauh lebih sulit untuk diterapkan.
1
Berikut ini cara meretas untuk mengimplementasikannya - dekorator yang menggunakan pengecualian meningkatkan untuk membuang frame eksekusi: metapython.blogspot.com.br/2010/11/...
jsbueno
2
Jika Anda membatasi diri untuk rekursi ekor, saya tidak berpikir traceback yang tepat sangat berguna. Anda memiliki panggilan foodari dalam panggilan ke foodari dalam ke foodari dari dalam panggilan ke foo... Saya tidak berpikir ada informasi yang berguna akan hilang dari kehilangan ini.
Kevin
1
Saya baru-baru ini belajar tentang Kelapa tetapi belum mencobanya. Tampaknya layak untuk dilihat. Itu diklaim memiliki optimasi rekursi ekor.
Alexey

Jawaban:

216

Tidak, dan itu tidak akan pernah terjadi sejak Guido van Rossum memilih untuk dapat memiliki traceback yang tepat:

Eliminasi Rekursi Ekor (2009-04-22)

Kata-Kata Akhir tentang Panggilan Ekor (2009-04-27)

Anda dapat secara manual menghilangkan rekursi dengan transformasi seperti ini:

>>> 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
John La Rooy
sumber
12
Atau jika Anda akan mengubahnya seperti itu - hanya from operator import add; reduce(add, xrange(n + 1), csum):?
Jon Clements
38
@ JonClements, yang berfungsi dalam contoh khusus ini. Transformasi ke loop sementara berfungsi untuk rekursi ekor dalam kasus umum.
John La Rooy
25
+1 Untuk menjadi jawaban yang benar tetapi ini sepertinya keputusan desain yang sangat sulit. The alasan yang diberikan tampaknya mendidih ke "sulit untuk dilakukan mengingat bagaimana python ditafsirkan dan saya tidak seperti itu pula jadi ada!"
Dasar
12
@ jwg Jadi ... Apa? Anda harus menulis bahasa sebelum mengomentari keputusan desain yang buruk? Tampaknya tidak logis atau praktis. Saya berasumsi dari komentar Anda bahwa Anda tidak memiliki pendapat tentang fitur (atau ketiadaan) dalam bahasa apa pun yang pernah ditulis?
Dasar
2
@ Dasar Tidak, tetapi Anda harus membaca artikel yang Anda komentari. Tampaknya sangat kuat bahwa Anda tidak benar-benar membacanya, mengingat bagaimana itu "padam" kepada Anda. (Anda mungkin benar-benar perlu membaca kedua artikel yang terhubung, sayangnya, karena beberapa argumen tersebar di keduanya.) Ini hampir tidak ada hubungannya dengan implementasi bahasa, tetapi semuanya berkaitan dengan semantik yang dimaksud.
Veky
179

Saya menerbitkan sebuah modul yang melakukan optimasi panggilan-tail (menangani gaya rekursi ekor dan passing-kelanjutan): https://github.com/baruchel/tco

Mengoptimalkan rekursi ekor dengan Python

Telah sering diklaim bahwa rekursi ekor tidak sesuai dengan cara pengkodean Pythonic dan bahwa seseorang seharusnya tidak peduli bagaimana cara menanamkannya dalam satu lingkaran. Saya tidak ingin berdebat dengan sudut pandang ini; Namun kadang-kadang saya suka mencoba atau menerapkan ide-ide baru sebagai fungsi rekursif ekor daripada dengan loop karena berbagai alasan (berfokus pada ide daripada pada proses, memiliki dua puluh fungsi pendek di layar saya dalam waktu yang sama daripada hanya tiga "Pythonic" fungsi, bekerja dalam sesi interaktif daripada mengedit kode saya, dll.)

Mengoptimalkan rekursi ekor dengan Python sebenarnya cukup mudah. Meskipun dikatakan tidak mungkin atau sangat rumit, saya pikir itu dapat dicapai dengan solusi yang elegan, singkat dan umum; Saya bahkan berpikir bahwa sebagian besar solusi ini tidak menggunakan fitur Python selain dari yang seharusnya. Ekspresi lambda yang bersih bekerja bersama dengan loop yang sangat standar menghasilkan alat yang cepat, efisien, dan sepenuhnya dapat digunakan untuk mengimplementasikan optimisasi rekursi ekor.

Sebagai kenyamanan pribadi, saya menulis sebuah modul kecil yang mengimplementasikan optimasi dengan dua cara berbeda. Saya ingin membahas di sini tentang dua fungsi utama saya.

Cara bersih: memodifikasi combinator Y

The Y combinator terkenal; itu memungkinkan untuk menggunakan fungsi lambda secara rekursif, tetapi tidak memungkinkan dengan sendirinya untuk menanamkan panggilan rekursif dalam satu lingkaran. Kalkulus Lambda sendiri tidak dapat melakukan hal seperti itu. Namun sedikit perubahan pada Y combinator dapat melindungi panggilan rekursif untuk benar-benar dievaluasi. Evaluasi dengan demikian dapat ditunda.

Berikut adalah ungkapan terkenal untuk penggabung Y:

lambda f: (lambda x: x(x))(lambda y: f(lambda *args: y(y)(*args)))

Dengan sedikit perubahan, saya bisa mendapatkan:

lambda f: (lambda x: x(x))(lambda y: f(lambda *args: lambda: y(y)(*args)))

Alih-alih memanggil dirinya sendiri, fungsi f sekarang mengembalikan fungsi yang melakukan panggilan yang sama, tetapi karena mengembalikannya, evaluasi dapat dilakukan nanti dari luar.

Kode saya adalah:

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

Fungsi ini dapat digunakan dengan cara berikut; berikut adalah dua contoh dengan versi faktorial dan Fibonacci rekursif ekor:

>>> 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

Jelas kedalaman rekursi tidak lagi menjadi masalah:

>>> bet( lambda f: lambda n: 42 if not n else f(n-1) )(50000)
42

Ini tentu saja tujuan sebenarnya dari fungsi ini.

Hanya satu hal yang tidak dapat dilakukan dengan optimasi ini: itu tidak dapat digunakan dengan fungsi rekursif ekor mengevaluasi ke fungsi lain (ini berasal dari kenyataan bahwa objek yang dikembalikan callable semua ditangani sebagai panggilan rekursif lebih lanjut tanpa perbedaan). Karena saya biasanya tidak memerlukan fitur seperti itu, saya sangat senang dengan kode di atas. Namun, untuk menyediakan modul yang lebih umum, saya berpikir sedikit lebih banyak untuk menemukan solusi untuk masalah ini (lihat bagian selanjutnya).

Mengenai kecepatan proses ini (yang bukan masalah sebenarnya), itu terjadi cukup baik; fungsi rekursif ekor bahkan dievaluasi jauh lebih cepat daripada dengan kode berikut ini menggunakan ekspresi yang lebih sederhana:

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

Saya pikir mengevaluasi satu ekspresi, bahkan rumit, jauh lebih cepat daripada mengevaluasi beberapa ekspresi sederhana, yang merupakan kasus dalam versi kedua ini. Saya tidak menyimpan fungsi baru ini dalam modul saya, dan saya tidak melihat keadaan di mana ia bisa digunakan daripada yang "resmi".

Gaya passing berkelanjutan dengan pengecualian

Berikut ini adalah fungsi yang lebih umum; ia mampu menangani semua fungsi rekursif ekor, termasuk yang mengembalikan fungsi lainnya. Panggilan rekursif diakui dari nilai balik lainnya dengan menggunakan pengecualian. Solusi ini lebih lambat dari yang sebelumnya; kode yang lebih cepat mungkin dapat ditulis dengan menggunakan beberapa nilai khusus sebagai "bendera" yang terdeteksi di loop utama, tetapi saya tidak suka gagasan menggunakan nilai khusus atau kata kunci internal. Ada beberapa interpretasi lucu menggunakan pengecualian: jika Python tidak suka panggilan rekursif ekor, pengecualian harus dimunculkan ketika panggilan ekor rekursif terjadi, dan cara Pythonic akan menangkap pengecualian untuk menemukan beberapa bersih solusi, yang sebenarnya apa yang terjadi di sini ...

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

Sekarang semua fungsi bisa digunakan. Dalam contoh berikut, f(n)dievaluasi ke fungsi identitas untuk nilai positif n:

>>> f = bet0( lambda f: lambda n: (lambda x: x) if not n else f(n-1) )
>>> f(5)(42)
42

Tentu saja, dapat diperdebatkan bahwa pengecualian tidak dimaksudkan untuk digunakan untuk mengarahkan ulang penerjemah (sebagai semacam gotopernyataan atau mungkin semacam gaya kelanjutan lewat), yang harus saya akui. Tetapi, sekali lagi, saya menemukan ide lucu menggunakan trysatu baris sebagai returnpernyataan: kami mencoba mengembalikan sesuatu (perilaku normal) tetapi kami tidak dapat melakukannya karena panggilan rekursif terjadi (pengecualian).

Jawaban awal (2013-08-29).

Saya menulis sebuah plugin yang sangat kecil untuk menangani rekursi ekor. Anda dapat menemukannya dengan penjelasan saya di sana: https://groups.google.com/forum/?hl=fr#!topic/comp.lang.python/dIsnJ2BoBKs

Ini dapat menanamkan fungsi lambda ditulis dengan gaya rekursi ekor di fungsi lain yang akan mengevaluasinya sebagai loop.

Fitur yang paling menarik dalam fungsi kecil ini, menurut pendapat saya, adalah bahwa fungsi tidak bergantung pada beberapa hack pemrograman kotor tetapi pada kalkulus lambda belaka: perilaku fungsi diubah ke yang lain ketika dimasukkan dalam fungsi lambda lain yang terlihat sangat seperti combinator Y.

Thomas Baruchel
sumber
Bisakah Anda, tolong, memberikan contoh mendefinisikan fungsi (lebih disukai mirip dengan definisi normal) yang memanggil salah satu dari beberapa fungsi lain berdasarkan kondisi tertentu, menggunakan metode Anda? Juga, dapatkah fungsi pembungkus Anda bet0digunakan sebagai dekorator untuk metode kelas?
Alexey
@Alexey Saya tidak yakin saya dapat menulis kode dalam gaya blok di dalam komentar, tetapi Anda tentu saja dapat menggunakan defsintaks untuk fungsi Anda, dan sebenarnya contoh terakhir di atas bergantung pada suatu kondisi. Dalam posting saya baruchel.github.io/python/2015/11/11/... Anda dapat melihat paragraf yang dimulai dengan "Tentu saja Anda bisa keberatan bahwa tidak ada yang akan menulis kode seperti itu" di mana saya memberikan contoh dengan sintaks definisi yang biasa. Untuk bagian kedua dari pertanyaan Anda, saya harus memikirkannya sedikit lebih banyak karena saya belum menghabiskan waktu untuk itu selama beberapa saat. Salam.
Thomas Baruchel
Anda harus peduli di mana panggilan rekursif terjadi dalam fungsi Anda, bahkan jika Anda menggunakan implementasi bahasa non-TCO. Ini karena bagian dari fungsi yang terjadi setelah panggilan rekursif adalah bagian yang perlu disimpan di stack. Oleh karena itu, menjadikan fungsi Anda rekursif ekor meminimalkan jumlah informasi yang harus Anda simpan per panggilan rekursif, yang memberi Anda lebih banyak ruang untuk memiliki tumpukan panggilan rekursif yang besar jika Anda membutuhkannya.
Josiah
21

Kata Guido ada di http://neopythonic.blogspot.co.uk/2009/04/tail-recursion-elimination.html

Saya baru-baru ini memposting entri di blog Python History saya tentang asal-usul fitur fungsional Python. Sebuah komentar sampingan tentang tidak mendukung penghapusan rekursi ekor (TRE) segera memicu beberapa komentar tentang betapa sayangnya bahwa Python tidak melakukan ini, termasuk tautan ke entri blog terbaru oleh orang lain yang mencoba "membuktikan" bahwa TRE dapat ditambahkan ke Python dengan mudah. Jadi izinkan saya mempertahankan posisi saya (yaitu saya tidak ingin TRE dalam bahasa ini). Jika Anda ingin jawaban singkat, itu hanya unpythonic. Inilah jawaban panjangnya:

Jon Clements
sumber
12
Dan di sinilah letak masalah dengan apa yang disebut BDsFL.
Adam Donahue
6
@AdamDonahue, apakah Anda sudah puas dengan setiap keputusan yang datang dari komite? Setidaknya Anda mendapatkan penjelasan yang masuk akal dan otoritatif dari BDFL.
Mark Ransom
2
Tidak, tentu saja tidak, tetapi mereka menganggap saya lebih adil. Ini dari seorang dokter, bukan dokter. Ironinya.
Adam Donahue
6

CPython tidak dan mungkin tidak akan pernah mendukung optimasi panggilan berdasarkan pernyataan Guido van Rossum pada subjek.

Saya pernah mendengar argumen yang membuat debugging lebih sulit karena cara memodifikasi jejak stack.

rekursif
sumber
18
@ux CPython adalah implementasi referensi dari bahasa pemrograman Python. Ada implementasi lain (seperti PyPy, IronPython, dan Jython), yang mengimplementasikan bahasa yang sama tetapi berbeda dalam detail implementasi. Perbedaan ini berguna di sini karena (secara teori) dimungkinkan untuk membuat implementasi Python alternatif yang melakukan TCO. Saya tidak mengetahui ada orang yang memikirkannya, dan kegunaannya akan terbatas karena kode yang mengandalkannya akan merusak semua implementasi Python lainnya.
3

Coba implementasi TCO makropi eksperimental untuk ukuran.

Mark Lawrence
sumber
2

Selain mengoptimalkan rekursi ekor, Anda dapat mengatur kedalaman rekursi secara manual dengan:

import sys
sys.setrecursionlimit(5500000)
print("recursion limit:%d " % (sys.getrecursionlimit()))
zhenv5
sumber
5
Mengapa Anda tidak menggunakan jQuery saja?
Jeremy Hert
5
Karena itu juga tidak menawarkan TCO? :-D stackoverflow.com/questions/3660577/…
Veky