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.

Другие вопросы по тегам