call/cc в Python - возможно?
Скажем, у нас есть следующий код в схеме
(define cc #f)
(define bar 0)
(define (func)
(print "This should show only once")
(call/cc (lambda (k) (set! cc k)))
(print bar)
(set! bar (+ bar 1)))
(define (g)
(func)
(print "This should show multiple times"))
(g)
(cc)
который печатает что-то вроде
This should show only once
0
This should show multiple times
1
This should show multiple times
И предположим, что мы хотим сделать то же самое в Python. http://wiki.c2.com/?ContinuationsInPython этот подход не работает, потому что они сохраняют только код, а не стек. Я пытался реализовать мою версию call/cc
в Python, сохранение и восстановление стека контекста. Я не уверен на 100%, что правильно реализовал логику продолжения, но сейчас это не важно.
Моя идея - сохранить указатели стека и инструкций вызывающей функции callcc
и его абоненты в Continuation
конструктор, а затем, в продолжение __call__
метод, сбросьте указатели команд в сохраненных кадрах стека, укажите текущий кадр стека f_back
указатель на сохраненный кадр стека и возвращение к волшебному появлению в функции, вызвавшей callcc
,
Проблема в том, что, хотя выход traceback.print_stack()
показывает, что текущий стек был заменен, код все еще выполняется так, как будто я вообще не касался текущего стека. Вот моя реализация https://ideone.com/kGchEm
import inspect
import types
import ctypes
import sys
import traceback
frameobject_fields = [
# PyObject_VAR_HEAD
("ob_refcnt", ctypes.c_int64),
("ob_type", ctypes.py_object),
("ob_size", ctypes.c_ssize_t),
# struct _frame *f_back; /* previous frame, or NULL */
("f_back", ctypes.c_void_p),
# PyCodeObject *f_code; /* code segment */
("f_code", ctypes.c_void_p),
# PyObject *f_builtins; /* builtin symbol table (PyDictObject) */
("f_builtins", ctypes.py_object),
# PyObject *f_globals; /* global symbol table (PyDictObject) */
("f_globals", ctypes.py_object),
####
("f_locals", ctypes.py_object),
("f_valuestack", ctypes.POINTER(ctypes.py_object)),
("f_stacktop", ctypes.POINTER(ctypes.py_object)),
("f_trace", ctypes.py_object),
("f_exc_type", ctypes.py_object),
("f_exc_value", ctypes.py_object),
("f_exc_traceback", ctypes.py_object),
("f_tstate", ctypes.c_void_p),
("f_lasti", ctypes.c_int),
]
if hasattr(sys, "getobjects"):
# This python was compiled with debugging enabled.
frameobject_fields = [
("_ob_next", ctypes.c_void_p),
("_ob_prev", ctypes.c_void_p),
] + frameobject_fields
class PyFrameObject(ctypes.Structure):
_fields_ = frameobject_fields
class Continuation:
def __init__(self, frame):
self.frame = frame
self.lasti = frame.f_lasti
self.lastis = []
frame = frame.f_back
while frame is not None:
self.lastis.append(frame.f_lasti)
frame = frame.f_back
def __call__(self):
print('\nbefore')
traceback.print_stack()
cur_frame = PyFrameObject.from_address(id(inspect.currentframe()))
PyFrameObject.from_address(cur_frame.f_back).ob_refcnt -= 1
cur_frame.f_back = id(self.frame)
PyFrameObject.from_address(id(self.frame)).ob_refcnt += 1
frame = self.frame
_frame = PyFrameObject.from_address(id(frame))
_frame.f_lasti = self.lasti + 4
frame = frame.f_back
for lasti in self.lastis:
if len(frame.f_code.co_code) != frame.f_lasti + 2:
break
_frame = PyFrameObject.from_address(id(frame))
_frame.f_lasti = lasti + 4
frame = frame.f_back
print('\nafter')
traceback.print_stack()
def callcc(f):
f(Continuation(inspect.currentframe().f_back))
cc = None
def func():
bar = 0
print("This should show only once")
def save_cont(k):
global cc
cc = k
callcc(save_cont)
print(bar)
bar += 1
def g():
func()
print("This should show multiple times")
sys.stderr = sys.stdout
g()
cc()
1 ответ
Проблема состоит в том, что стандартный интерпретатор - CPython - является стековым интерпретатором, то есть каждый вызов функции Python приводит к рекурсивному вызову внутри интерпретатора. Итак, Питон FrameType
объекты просто взгляды (.f_back
является атрибутом только для чтения по уважительной причине) фреймов стека C, нет смысла менять f_back
указатель.
Если вы действительно хотите манипулировать стеком, вам придется написать модуль C, как это делает модуль greenlet.
Гуг удачи!
Этот ответ отлично объясняет, почему трудно зафиксировать состояние интерпретатора Python. Этот пакет сделает это за вас. Он не реализует call / cc, но он реализует longjmp и setjmp, что является всего лишь синтаксическим сахаром в стороне от call / cc.