Как добиться непрерывного интерактивного диалога с подпроцессом в Python?

Может быть, «непрерывное взаимодействие» - неправильное словосочетание. Мне интересно, может ли кто-нибудь помочь мне понять основы вызова программы как подпроцесса из программы Python? Я взламывал, но продолжаю сталкиваться с неприятными ошибками. Я лучше всего понимаю на простых примерах. У меня есть программа под названием square.py, сохраненная на моем рабочем столе, которая использует следующий код:

i=0
while i<10:
    x=int(raw_input('Enter x-dimension: '))
    x1 = x*x
    print str(x1)
    i=i+1

Не мог бы кто-нибудь объяснить мне простым языком, как вызвать эту программу в IDLE и поддерживать с ней непрерывный интерактивный диалог (держать ее открытой и работать в фоновом режиме), пока она не завершится сама по себе?

В конце концов мне нужно будет использовать эти знания для вызова программы генетического алгоритма, написанной на C, из графического интерфейса Python (с использованием tkinter). Генетический алгоритм выводит массив значений, пользователь использует эти значения, чтобы что-то делать, и дает пользователю обратную связь относительно полезности этих значений. Отзывы пользователей представлены в виде 0–100. Когда генетический алгоритм получает ввод, он творит чудеса и выводит другой массив чисел, который, будем надеяться, будет иметь немного лучшую полезность. Итак, я хочу обернуть графический интерфейс Python вокруг устрашающей программы на C, передать программе C значение обратной связи и получить массив чисел.

Надеюсь, я достаточно хорошо объяснил, что пытаюсь сделать; Если кто-нибудь может помочь мне использовать подпроцесс для вызова square.py, передать ему значение и вернуть его результат, я был бы очень счастлив. Ваше здоровье!


person Thetravellingfool    schedule 25.11.2013    source источник


Ответы (2)


Программы, предназначенные для взаимодействия с человеком, отличаются от программ, предназначенных для взаимодействия с другими программами. Ваш square.py скрипт ближе к первой категории. Возможные проблемы:

  • приглашение заканчивается в середине строки, которая заставляет родительский сценарий читать по одному байту за раз вместо полных строк или известных фрагментов, чтобы избежать блокировки
  • ответы ребенка явно не сбрасываются. Это означает, что может быть включена буферизация блоков в неинтерактивном режиме например, когда он запускается через subprocess без pty

Вот как вы могли бы взаимодействовать с ним, используя модуль subprocess в его текущей форме:

#!/usr/bin/env python
from __future__ import print_function
import sys
from itertools import cycle
from subprocess import Popen, PIPE
from textwrap import dedent

# start child process
p = Popen([sys.executable or 'python', '-u', '-c', dedent("""
        for i in range(10):
            x = int(input('Enter x-dimension: '))
            print(x*x)
        """)], stdin=PIPE, stdout=PIPE, universal_newlines=True, bufsize=1)
for n in cycle([3, 1, 4, 15, 926]): # infinite loop
    while p.poll() is None: # while the subprocess is running
        # send input to the child
        print(n, file=p.stdin)
        # read & parse answer
        data = p.stdout.readline().rpartition(' ')[2]
        if not data: # EOF
            answer = None
            break # exit inner loop
        answer = int(data)
        if answer == 1: # show example when input depends on output
            n += 1
        else: # done with given `n`
            break # exit inner loop
    else: # subprocess ended
        break # exit outer loop
    if answer is not None:
        print("Input %3d Output %6d" % (n, answer))
p.communicate() # close pipes, wait for the child to terminate

И вот то же самое, но с использованием pexpect (для сравнения):

#!/usr/bin/env python
import sys
from itertools import cycle
from textwrap import dedent

import pexpect

child = pexpect.spawnu(sys.executable or 'python', ['-c', dedent("""
            for i in range(10):
                x = int(input('Enter x-dimension: '))
                print(x*x)
            """)])
for n in cycle([3, 1, 4, 15, 926]):
    while True:
        i = child.expect([pexpect.EOF, u'x-dimension:'])
        if i == 0: # EOF
            answer = None
            child.close()
            sys.exit()
        elif i == 1: # child waits for input
            child.sendline(str(n))
            child.expect(u'\\n\\d+\\s')
            answer = int(child.after)
            if answer == 1:
                n += 1
            else:
                break
        else:
            assert 0
    else: # child terminated
        break
    if answer is not None:
        print("Input %3d Output %6d" % (n, answer))

Оба сценария написаны для поддержки Python 2 и Python 3 из одного источника.

Примечание: в сценарии на основе subprocess есть аргумент -u, который позволяет читать строки, как только они становятся доступными, даже в неинтерактивном режиме. Скрипт на основе pexpect работает без такого переключателя. Программы на основе stdio можно небуферизовать / сделать строчную буферизацию с помощью stdbuf, unbuffer утилит или путем предоставления pty.

Вы можете видеть, что даже самый простой дочерний сценарий (square.py) требует решения нескольких проблем, чтобы вообще работать.

Все проще, когда дочерняя программа ожидает запуска из другой программы, оставаясь при этом доступной для чтения (отладки). В этом случае square.py может выглядеть так:

#!/usr/bin/env python
import sys
import time

for line in iter(sys.stdin.readline, ''): # get line as soon as it is available
    print(int(line)**2) # find square
    sys.stdout.flush()  # make the answer available immediately
    time.sleep(.5) # a delay to show that the answer is available immediately

Его можно использовать из модуля на основе subprocess в режиме «все сразу»:

import sys
from subprocess import Popen, PIPE

L = [2, 7, 1] # numbers to be squared
p = Popen([sys.executable or 'python', 'square.py'], stdin=PIPE, stdout=PIPE,
          universal_newlines=True, bufsize=-1)
answers = map(int, p.communicate("\n".join(map(str, L)))[0].splitlines())

Или по одному номеру за раз:

#!/usr/bin/env python
import sys
from subprocess import Popen, PIPE

answers = []
p = Popen([sys.executable or 'python', 'square.py'], stdin=PIPE, stdout=PIPE,
          bufsize=1)
for c in [b'2', b'7', b'1']:
    p.stdin.write(c + b'\n')
    p.stdin.flush()
    answers.append(int(p.stdout.readline()))
    print(answers)
p.communicate() # close pipes, wait for child to finish
print(answers)

Чтобы получить массив из вашей программы на C; вы могли json модуль:

import json
from subprocess import Popen, PIPE

p = Popen(['./c-program', 'other', 'args'], stdin=PIPE, stdout=PIPE, bufsize=1)
p.stdin.write(json.dumps({'parameter': 8}).encode() + b'\n') # send input
p.stdin.flush()
result = json.loads(p.stdout.readline().decode()) # e.g., {"result": [0, 0, 7]}
# ...
p.communicate() # close pipes, wait for child to finish
person jfs    schedule 25.11.2013

«Непрерывный интерактив» сильно конфликтует с модулем subprocess, который использует конвейеры уровня ОС (в Unix-подобных системах; он обязательно делает что-то немного другое в Windows). Однако, чтобы взаимодействовать с процессом так, как это делают пользователи, например, с подключениями ssh pty, вы должны создать сеанс pty. Многие программы предполагают, разговаривая по каналам или через каналы, что они не интерактивны.

модуль pexpect - это перевод старого Дона Либеса expect в Python. Он нацелен на такого рода идеи.

В модуле sh также есть все необходимое для достижения желаемых результатов.

(Сам я ни один из них не использовал.)

person torek    schedule 25.11.2013
comment
Может быть, «непрерывное взаимодействие» - не та фраза. Мне не нужно беспокоить код C, пока он это делает, мне просто нужно присвоить ему значение, позволить ему творить чудеса, принять его вывод, а затем повторить все заново. Поскольку для получения приличного результата может потребоваться 10 000 испытаний, мне нужно оставаться в том же сеансе генетического алгоритма, пока я не решу выйти. Есть идеи по поводу не такого сложного примера, о котором я спрашивал? Спасибо! - person Thetravellingfool; 25.11.2013
comment
В общем, вам все равно понадобится что-то, что открывает pty и использует его для взаимодействия с базовым процессом, что означает что-то вроде этих пакетов. Библиотека C устанавливает stdout для полной (а не строчной) буферизации, когда stdout является конвейером, и большинство программ C не вызывают fflush достаточно сознательно для работы с конвейерами. - person torek; 25.11.2013
comment
@torek: OP в другом потоке упоминается, что программа C разработана коллегой, поэтому в этом случае можно избежать проблем с блочной буферизацией и границами сообщений и можно использовать модуль subprocess. - person jfs; 25.11.2013
comment
@ J.F.Sebastian: Ага, это действительно меняет ситуацию. Хотя, если у вас есть такой большой контроль над программой C, возможно, ее можно было бы изменить для хранения ее состояния при независимых вызовах программы, и тогда было бы еще проще запускать из оболочки Python. :-) - person torek; 25.11.2013
comment
Если бы я сохранил вывод кода C в файле .csv, я мог бы прочитать его в программе python. Думаю, мы могли бы настроить его так, чтобы я мог сохранить отзывы пользователей в отдельном файле и позволить программе на языке C прочитать их. Остается только вопрос о том, как программа Python узнает, когда доступен новый вывод C, и как код C узнает, что доступно новое значение обратной связи от пользователя. Я ценю вашу помощь @ J.F.Sebastian и @torek! - person Thetravellingfool; 26.11.2013
comment
@Thetravellingfool: было бы еще лучше скомпилировать код C как библиотеку и написать крошечный модуль расширения Python, чтобы обернуть его, или просто вызвать его напрямую, используя ctypes (пример). Затем используйте его в своем коде Python, как и любой другой код. - person jfs; 26.11.2013