2015-04-23 4 views
2

Скажем, у меня есть генератор, как такСкопировать генератор

def gen(): 
    a = yield "Hello World" 
    a_ = a + 1 #Imagine that on my computer "+ 1" is an expensive operation 
    print "a_ = ", a_ 
    b = yield a_ 
    print "b =", b 
    print "a_ =", a_ 
    yield b 

Теперь, скажем, я

>>> g = gen() 
>>> g.next() 
>>> g.send(42) 
a_ = 43 
43 

Теперь мы рассчитали a_. Теперь я хотел бы клонировать мой генератор так.

>>> newG = clonify(g) 
>>> newG.send(7) 
b = 7 
a_ = 43 
7 

но мой оригинал g все еще работает.

>>> g.send(11) 
b = 11 
a_ = 43 
11 

В частности, clonify принимает состояние генератора, и копирует его. Я мог бы просто сбросить свой генератор, как старый, но это потребует вычисления a_. Также обратите внимание, что я не хочу сильно модифицировать генератор. В идеале я мог просто взять объект-генератор из библиотеки и clonify его.

Примечание: itertools.tee не будет работать, потому что он не обрабатывает отправления.

Примечание: меня интересуют генераторы, созданные путем размещения в функции инструкций yield.

+3

Это не так просто, как кажется. Что делать, если генератор использует файл или сетевой поток, который нельзя клонировать? Хотя взгляните на ['itertools.tee'] (https://docs.python.org/3/library/itertools.html#itertools.tee) –

+0

Это дубликат? http://stackoverflow.com/questions/4945155/how-to-clone-a-python-generator-object –

+0

@ColonelThirtyTwo Это может вызвать ошибку для всех, что мне волнует. Он должен работать только для кода, который не преподает ввод или вывод. – PyRulez

ответ

0

Вы не можете, в общем. Однако, если вы параметризуете какую-то дорогостоящую операцию, почему бы не снять эту операцию, создав генераторную фабрику?

def make_gen(a): 
    a_ = [a + 1] # Perform expensive calculation 
    def gen(a_=a_): 
     while True: 
      print "a_ = ", a_ 
      a_[0] = yield a_[0] 
    return gen 

Тогда вы можете создать столько генераторов, сколько вы хотите от возвращенного объекта:

gen = make_gen(42) 
g = gen() 
g.send(None) 
# a_ = [43] 
g.send(7) 
# a_ = [7] 
new_g = gen() 
new_g.send(None) 
# a_ = [7] 
+0

Вы не можете установить 'a', используя send. – PyRulez

+0

Я уверен, что в этом примере. Хотя, честно говоря, я пропускаю 'g.send (None)' – nelfin

+2

@nelfin: Я думаю, его точка зрения заключается в том, что если вы хотите сделать 'a_ = yield b', это не сработает, потому что вы превратил 'a_' в переменную закрытия. (В Python 3 было бы тривиальное исправление: просто добавьте 'nonlocal a_' в начало генератора, но в 2.7 это не так просто.) – abarnert

5

Python не имеет никакой поддержки для клонирования генераторов.

Концептуально это должно быть реализовано, по крайней мере, для CPython. Но практически, оказывается, это очень сложно.


Под крышками генератор в основном представляет собой обертку вокруг рамки стека. *

И рамка объект, по существу, только код объект, указатель инструкции (индекс в этот коде объект), окружающая среда/Глобал встроенных функций/местных жителей, за исключение государственный, и некоторые флаги и отладочная информацию.

И оба типа подвергаются воздействию уровня Python, **, как и все необходимые им биты. Таким образом, это действительно должно быть только вопрос:

  • Создание объекта кадра так же, как g.gi_frame, но с копией местных жителей вместо первоначальных местных жителей. (Все вопросы на уровне пользователя сводятся к тому, чтобы создать мелкие копии, глубокие копии или один из вышеперечисленных и рекурсивно клонирующих генераторов.)
  • Создайте объект-генератор из нового объекта фрейма (и его код и бегущий флаг).

И нет очевидной практической причины, из-за которой невозможно построить объект кадра из его битов, как и для объекта кода или большинства других скрытых встроенных типов.


К сожалению, как оказалось, Python не предоставляет способ создания объекта фрейма. Я думал, что вы можете обойти это, просто используя ctypes.pythonapi для вызова PyFrame_New, но первым аргументом является PyThreadState, который вы определенно не можете создать из Python и не должен быть способен. Таким образом, чтобы сделать эту работу, вы должны либо:

  • Воспроизводить все PyFrame_New делает, ударяя на структуры С помощью ctypes или
  • вручную создать поддельный PyThreadState ударяя на структуры C (которая будет по-прежнему необходимо внимательно прочитать код до PyFrame_New, чтобы узнать, что вам нужно подделать).

Я думаю, что это еще может быть выполнимо (и я планирую играть с ним, если я придумываю что-нибудь, я буду обновлять Cloning generators пост в моем блоге), но это, безусловно, не будет тривиальности или, конечно, даже отдаленно портативный.


Есть также пара незначительных проблем.

  • Местные жители подвергаются Python как Dict (назовем ли вы locals() для вашей собственной, или доступ g.gi_frame.f_locals для генератора вы хотите клонировать). Под обложками местные жители фактически хранятся в стеке C. *** Вы можете обойти это, используя ctypes.pythonapi для звонка PyFrame_LocalsToFast и PyFrame_FastToLocals. Но dict просто содержит значения, а не клеточные объекты, поэтому при этом тасуляция превратит все нелокальные переменные в локальные переменные в клоне. ****

  • состояние Исключения подвергается Python как типа/значение/отладочных 3-кортеж, но внутри фрейма есть также заимствовано (не refcounted) ссылка на генератор владеющего (или если значение NULL это не кадр генератора). (The source объясняет, почему.) Итак, ваша функция построения кадра не может пересчитать генератор, или у вас есть цикл и, следовательно, утечка, но он должен пересчитать генератор или у вас есть потенциально оборванный указатель, пока кадр не будет назначен генератор. Очевидный ответ, похоже, заключается в том, чтобы оставить генератор NULL при построении кадра и иметь функцию построения генератора, эквивалентную self.gi_f.f_generator = self; Py_DECREF(self).


* Он также сохраняет копию объекта кода фрейма и работает флаг, так что они могут быть доступны после выхода генератора и избавляется от рамы.

** generator и frame скрыты от встроенных команд, но они доступны как types.GeneratorTypetypes.FrameType. И у них есть docstrings, описания их атрибутов в модуле inspect и т. Д., Как функции и объекты кода.

*** При компиляции определения функции, компилятор делает список всех местных жителей, которые хранятся в co_varnames, и превращает каждую переменную ссылку в LOAD_FAST/STORE_FAST опкодом с индексом в co_varnames в качестве аргумента. Когда выполняется вызов функции, объект фрейма хранит указатель стека в f_valuestack, толкает len(co_varnames)*sizeof(PyObject *) в стек, а затем LOAD_FAST 0 просто обращается к *f_valuestack[0]. Замки сложнее; слишком много, чтобы объяснить в комментарии к SO ответ.

**** Я предполагаю, что вы хотите, чтобы клон делил ссылки на закрытие оригинала. Если вы надеялись рекурсивно клонировать все фреймы в стек, чтобы получить новый набор ссылок на привязку к связыванию, это добавляет еще одну проблему: нет способа построить новые объекты ячеек из Python.

+1

Вы должны предложить PEP. – PyRulez

+0

@PyRulez: PEP нуждается в хорошем разделе Обоснования, и у меня нет хорошего обоснования, почему необходимо создание объектов 'frame' и' generator' (и 'cell', если вы этого хотите). Кроме того, даже с обоснованием, я не думаю, что идея PEP-готова; сначала потребуется обсуждение идей python. – abarnert

+1

@PyRulez: Конечно, вы можете начать обсуждение идей python, но имейте в виду, что первые 20 ответов потребуют хорошего использования и показывают, что вариант использования игрушек лучше обслуживается решением, которое не работает 't требуется генераторы клонирования, поэтому я сначала разработал наименьший вариант использования, который вы можете придумать, что действительно полезно и действительно требует клонирования генераторов, если вы хотите получить хорошее понимание. – abarnert

Смежные вопросы