Перенаправление Python os.dup2 включает буферизацию вывода на консолях Windows Python.

Я использую стратегию, основанную на os.dup2 (аналогично примерам на этом сайте), чтобы перенаправить вывод уровня C/fortran во временный файл для захвата.

Единственная проблема, которую я заметил, заключается в том, что если вы используете этот код из интерактивной оболочки в Windows (либо python.exe, либо ipython), он имеет странный побочный эффект включение буферизации вывода в консоли.

Перед захватом sys.stdout находится какой-то файловый объект, который возвращает True вместо istty(). Ввод print('hi') вызывает прямой вывод hi. После захвата sys.stdout указывает точно на тот же файловый объект, но print('hi') больше ничего не показывает, пока не будет вызван sys.stdout.flush().

Ниже приведен минимальный пример скрипта «test.py».

import os, sys, tempfile

class Capture(object):
    def __init__(self):
        super(Capture, self).__init__()
        self._org = None    # Original stdout stream
        self._dup = None    # Original system stdout descriptor
        self._file = None   # Temporary file to write stdout to

    def start(self):
        self._org = sys.stdout
        sys.stdout = sys.__stdout__
        fdout = sys.stdout.fileno()
        self._file = tempfile.TemporaryFile()
        self._dup = None
        if fdout >= 0:
            self._dup = os.dup(fdout)
            os.dup2(self._file.fileno(), fdout)

    def stop(self):
        sys.stdout.flush()
        if self._dup is not None:
            os.dup2(self._dup, sys.stdout.fileno())
            os.close(self._dup)
        sys.stdout = self._org
        self._file.seek(0)
        out = self._file.readlines()
        self._file.close()
        return out

def run():
    c = Capture()
    c.start()
    os.system('echo 10')
    print('20')
    x = c.stop()
    print(x)

if __name__ == '__main__':
    run()

Открытие командной строки и запуск скрипта работает нормально. Это дает ожидаемый результат:

python.exe test.py

Запуск его из оболочки Python не:

python.exe
>>> import test.py
>>> test.run()
>>> print('hello?')

Вывод не отображается, пока стандартный вывод не очищен:

>>> import sys
>>> sys.stdout.flush()

Кто-нибудь знает, что происходит?


Краткая информация:

  • Проблема возникает в Windows, не в Linux (поэтому, вероятно, не на Mac).
  • Такое же поведение как в Python 2.7.6, так и в Python 2.7.9.
  • Сценарий должен захватывать вывод C/fortran, а не только вывод python.
  • Он работает без ошибок в Windows, но после этого print() больше не сбрасывается

person Michael Clerx    schedule 20.03.2015    source источник
comment
Работает, как и ожидалось, в Python 3.4.2.   -  person roeland    schedule 23.03.2015
comment
Какую версию Python вы используете?   -  person Antti Haapala    schedule 23.03.2015


Ответы (1)


Я мог подтвердить связанную проблему с Python 2 в Linux, но не с Python 3

Основная проблема заключается в

>>> sys.stdout is sys.__stdout__
True

Таким образом, вы используете исходный объект sys.stdout все время. И когда вы делаете первый вывод, в Python 2 он выполняет системный вызов isatty() один раз для базового файла и сохраняет результат.

Вы должны открыть совершенно новый файл и заменить им sys.stdout.


Таким образом, правильным способом написания класса Capture было бы

import sys
import tempfile
import time
import os

class Capture(object):
    def __init__(self):
        super(Capture, self).__init__()

    def start(self):
        self._old_stdout = sys.stdout
        self._stdout_fd = self._old_stdout.fileno()
        self._saved_stdout_fd = os.dup(self._stdout_fd)
        self._file = sys.stdout = tempfile.TemporaryFile(mode='w+t')
        os.dup2(self._file.fileno(), self._stdout_fd)

    def stop(self):
        os.dup2(self._saved_stdout_fd, self._stdout_fd)
        os.close(self._saved_stdout_fd)
        sys.stdout = self._old_stdout
        self._file.seek(0)
        out = self._file.readlines()
        self._file.close()
        return out

def run():
    c = Capture()
    c.start()
    os.system('echo 10')
    x = c.stop()
    print(x)
    time.sleep(1)
    print("finished")

run()

С помощью этой программы как в Python 2, так и в Python 3 вывод будет таким:

['10\n']
finished

при этом первая строка появляется на терминале мгновенно, а вторая – с задержкой в ​​одну секунду.


Однако это не сработает для кода, который импортирует stdout из sys. К счастью, не так много кода делает это.

person Antti Haapala    schedule 23.03.2015
comment
Привет, Антти, я думаю, ты неправильно понял: с python 2.7 в linux проблем нет. Только в Windows. В Linux скрипт делает именно то, что ему нужно, и после этого печать работает нормально. Во-вторых, класс захвата должен захватывать вывод уровня подпроцесса (то есть вывод из расширений C или fortran), чего не делает это решение. - person Michael Clerx; 23.03.2015
comment
@MichaelClerx, в любом случае, я изменил ваш вопрос, чтобы он использовал не sys.stdout, а фактический дескриптор файла. - person Antti Haapala; 23.03.2015
comment
Какой линукс/питон используете? И вы выполняете скрипт или работаете с терминала? - person Michael Clerx; 23.03.2015
comment
Я выполняю скрипт ofc - person Antti Haapala; 23.03.2015
comment
Я имею в виду, делаете ли вы python test.py или открываете терминал, а затем выполняете тест импорта. Это важно для версии Windows... - person Michael Clerx; 23.03.2015
comment
Хорошо, перенаправление sys.stdout в файл также устраняет проблему. Тем не менее, я бы все же изменил вторую строку start() на self._stdout_fd = sys.__stdout__.fileno(), чтобы предотвратить проблемы, когда стандартный вывод уже был перенаправлен (возможно, на что-то без метода fileno()). Кроме того, проблема с окнами в моем примере перестает возникать, если вы пропустите print() внутри бита захвата, поэтому я бы добавил print('20') после вызова os, чтобы сделать сравнение справедливым. - person Michael Clerx; 23.03.2015
comment
ах, действительно: D, да, печать необходима в Windows/Linux, чтобы возникла эта проблема (и никаких отпечатков до этого) - person Antti Haapala; 23.03.2015
comment
Наткнулся на это через проблему pytest (github.com/pytest-dev/ pytest/issues/5134#issuecomment-546523358). Там все еще проблема, когда используется sys.__stdout__ или iostream через программу C++. В то время как первое можно исправить, заменив также sys.__stdout__, последнее, вероятно, действительно требует закрытия fd 1 или чего-то подобного. Но, похоже, проблема только с py2. - person blueyed; 26.10.2019