Python запуск скрипта в фоновом режиме windows

I have a script that checks something on my PC every 5 minutes and I don’t want Python to show on my task tray. I use Windows as my operating system.

Is there any way to make Python run in the background and force it to not show in my task tray?

353 2 gold badges 4 silver badges 13 bronze badges

asked Apr 23, 2009 at 21:07

If you run a console script using pythonw.exe , it will neither display a window nor appear in the task bar. For example, I use the following command to launch ntlmaps on startup:

C:BenBlankPython2.6pythonw.exe scripts/ntlmaps 

Be aware, however, that there is no way to interact with the script, nor to terminate it save via the Task Manager.

answered Apr 23, 2009 at 21:15

Ben Blank Ben Blank

53.9k 27 gold badges 132 silver badges 153 bronze badges

Just another option you have:

You can create a shortcut to your Python script, then right-click the shortcut —> Properties —> Shortcut tab

There is a drop-down box under the Run option which lets you run the command minimized.

answered Apr 23, 2009 at 21:17

Jason Coon Jason Coon

17.3k 10 gold badges 41 silver badges 50 bronze badges

You could run it as a service. See here

answered May 1, 2009 at 8:07

12.4k 8 gold badges 45 silver badges 50 bronze badges

cron it on linux; schedule it on windows [control panel > scheduled tasks > Add scheduled task]

380k 79 gold badges 505 silver badges 775 bronze badges

answered Apr 23, 2009 at 21:10

10.9k 2 gold badges 26 silver badges 27 bronze badges

To Run python file from any where :

Create Shortcut of Python File. 
Place Shortcut in this location C:ProgramDataMicrosoftWindowsStart MenuPrograms 
Now Right Click --> Go to Properties --> Shortcut --> Press any key on keyboard it will take one shortcut key 
Now , Type the Shortcut key which you entered in previous step. 
Check out Output! :) 

answered Jun 29, 2020 at 5:05

Be Champzz Be Champzz

301 2 silver badges 4 bronze badges

Look for Schedule Tasks in the control panel.

answered Apr 23, 2009 at 21:11

David Berger David Berger

12.1k 6 gold badges 38 silver badges 51 bronze badges

I have a script that checks something on my PC every 5 minutes and I don’t want Python to show on my task tray. I use Windows as my operating system.

Is there any way to make Python run in the background and force it to not show in my task tray?

353 2 gold badges 4 silver badges 13 bronze badges

asked Apr 23, 2009 at 21:07

If you run a console script using pythonw.exe , it will neither display a window nor appear in the task bar. For example, I use the following command to launch ntlmaps on startup:

C:BenBlankPython2.6pythonw.exe scripts/ntlmaps 

Be aware, however, that there is no way to interact with the script, nor to terminate it save via the Task Manager.

answered Apr 23, 2009 at 21:15

Ben Blank Ben Blank

53.9k 27 gold badges 132 silver badges 153 bronze badges

Just another option you have:

You can create a shortcut to your Python script, then right-click the shortcut —> Properties —> Shortcut tab

There is a drop-down box under the Run option which lets you run the command minimized.

answered Apr 23, 2009 at 21:17

Jason Coon Jason Coon

17.3k 10 gold badges 41 silver badges 50 bronze badges

You could run it as a service. See here

answered May 1, 2009 at 8:07

12.4k 8 gold badges 45 silver badges 50 bronze badges

cron it on linux; schedule it on windows [control panel > scheduled tasks > Add scheduled task]

380k 79 gold badges 505 silver badges 775 bronze badges

answered Apr 23, 2009 at 21:10

10.9k 2 gold badges 26 silver badges 27 bronze badges

To Run python file from any where :

Create Shortcut of Python File. 
Place Shortcut in this location C:ProgramDataMicrosoftWindowsStart MenuPrograms 
Now Right Click --> Go to Properties --> Shortcut --> Press any key on keyboard it will take one shortcut key 
Now , Type the Shortcut key which you entered in previous step. 
Check out Output! :) 

answered Jun 29, 2020 at 5:05

Be Champzz Be Champzz

301 2 silver badges 4 bronze badges

Look for Schedule Tasks in the control panel.

answered Apr 23, 2009 at 21:11

David Berger David Berger

12.1k 6 gold badges 38 silver badges 51 bronze badges

Both capture output and run on background with threading

As mentioned on this answer, if you capture the output with stdout= and then try to read() , then the process blocks.

However, there are cases where you need this. For example, I wanted to launch two processes that talk over a port between them, and save their stdout to a log file and stdout.

The threading module allows us to do that.

First, have a look at how to do the output redirection part alone in this question: Python Popen: Write to stdout AND log file simultaneously

#!/usr/bin/env python3 import os import subprocess import sys import threading def output_reader(proc, file): while True: byte = proc.stdout.read(1) if byte: sys.stdout.buffer.write(byte) sys.stdout.flush() file.buffer.write(byte) else: break with subprocess.Popen(['./sleep.py', '0'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc1, subprocess.Popen(['./sleep.py', '10'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc2, open('log1.log', 'w') as file1, open('log2.log', 'w') as file2: t1 = threading.Thread(target=output_reader, args=(proc1, file1)) t2 = threading.Thread(target=output_reader, args=(proc2, file2)) t1.start() t2.start() t1.join() t2.join() 
#!/usr/bin/env python3 import sys import time for i in range(4): print(i + int(sys.argv[1])) sys.stdout.flush() time.sleep(0.5) 
./main.py 

stdout get updated every 0.5 seconds for every two lines to contain:

0 10 1 11 2 12 3 13 

and each log file contains the respective log for a given process.

Inspired by: https://eli.thegreenplace.net/2017/interacting-with-a-long-running-child-process-in-python/

Tested on Ubuntu 18.04, Python 3.6.7.

Сценарий мониторинга test1.py, написанный на python, работает в режиме True. Когда ssh удален (с помощью терминала замазки), запустите сценарий с помощью следующей команды:

python test1.py &

Теперь сценарий работает нормально, вы можете увидеть номер процесса через ps, в это время непосредственно закройте терминал ssh (не используя команду выхода, но непосредственно через кнопку закрытия putty), после входа в систему снова и найденный Процесс уже завершен.

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

бегать в фоновом режиме под окнами

Под окнами нет глубоких исследований. Метод, который я часто использую, заключается в изменении расширения скрипта Python «.pyw». Двойной щелчок запускается в фоновом режиме без изменения кода.

Запустите в фоновом режиме под Linux

Через вилку
В среде Linux процесс демона в c реализован с помощью fork, и python также может быть реализован таким образом. Пример кода следующий:

#!/usr/bin/env python import time,platform import os def funzioneDemo(): # Это пример конкретной бизнес-функции fout = open('/tmp/demone.log', 'w') while True: fout.write(time.ctime()+'n') fout.flush() time.sleep(2) fout.close() def createDaemon(): # форк процесс try: if os.fork() > 0: os._exit(0) except OSError, error: print 'fork #1 failed: %d (%s)' % (error.errno, error.strerror) os._exit(1) os.chdir('/') os.setsid() os.umask(0) try: pid = os.fork() if pid > 0: print 'Daemon PID %d' % pid os._exit(0) except OSError, error: print 'fork #2 failed: %d (%s)' % (error.errno, error.strerror) os._exit(1) # Перенаправить стандартный IO sys.stdout.flush() sys.stderr.flush() si = file("/dev/null", 'r') so = file("/dev/null", 'a+') se = file("/dev/null", 'a+', 0) os.dup2(si.fileno(), sys.stdin.fileno()) os.dup2(so.fileno(), sys.stdout.fileno()) os.dup2(se.fileno(), sys.stderr.fileno()) # Выполнить код в дочернем процессе funzioneDemo() # function demo if __name__ == '__main__': if platform.system() == "Linux": createDaemon() else: os._exit(0)

Через выскочку

Приложение может быть упаковано как системная служба через upstart, и полный пример прямо записан здесь.

1. Напишите скрипт на Python

[[email protected] t27]# cat test123.py #!/usr/bin/env python import os,time while True : print time.time() time.sleep(1)

2. Напишите файл конфигурации upstat

[[email protected] t27]# cat /etc/init/mikeTest.conf description "My test" author "[email protected]" start on runlevel [234] stop on runlevel [0156] chdir /test/t27 exec /test/t27/test123.py respawn
  • 3. Перезагрузить северную часть штата
initctl reload-configuration
[[email protected] t27]# start mikeTest mikeTest start/running, process 6635 [[email protected] t27]# ps aux | grep test123.py root 6635 0.0 0.0 22448 3716 ? Ss 09:55 0:00 python /test/t27/test123.py root 6677 0.0 0.0 103212 752 pts/1 S+ 09:56 0:00 grep test123.py
[[email protected] t27]# stop mikeTest mikeTest stop/waiting [[email protected] t27]# ps aux | grep test123.py root 6696 0.0 0.0 103212 752 pts/1 S+ 09:56 0:00 grep test123.py [[email protected] t27]#

Скрипт bash

1. код Python

[[email protected] test]# cat test123.py #!/usr/bin/env python import os,time while True : print time.time() time.sleep(1)
  • 2. Написать скрипт запуска
[[email protected] test]# cat start.sh #! /bin/sh python test123.py &
[[email protected] test]#./start.sh

Если вы используете & для запуска процесса напрямую:

python test123.py &

Непосредственное закрытие терминала ssh приведет к завершению процесса.

Через экран, tmux и т. Д.

Если вы временно запустите программу, вы можете запустить программу через screen, tmux, здесь описывается, как запускается tmux.

1. Запустите tmux

Введите tmux в терминал, чтобы начать

2. Запустите программу в tmux

Просто выполните следующую команду напрямую (ссылка на скрипт выше): python test123.py

3. Закройте терминал ssh напрямую (например, кнопку закрытия на замазке);

4. После перезапуска ssh выполните следующую команду:

tmux attach
  • Теперь вы можете видеть, что программа на python все еще работает нормально.

Содержание:

  • Асинхронное выполнение с помощью инкрементной итерации;
  • Запуск команды в фоновом процессе;
    • Использование обратных вызовов с фоновым процессом;
    • Интерактивные обратные вызовы;
    • Обратный вызов при завершении команды.

    Асинхронное выполнение с помощью инкрементной итерации.

    from sh import tail # цикл будет работать вечно (если не поставить # таймаут или убить процесс сигналом) for line in tail("-f", "/var/log/some_log_file.log", _iter=True): print(line) 

    По умолчанию ключевой аргумент _iter повторяет STDOUT, но можно изменить этот параметр, передав либо ‘err’ , либо ‘out’ (вместо True ). Кроме того, по умолчанию, выходные данные буферизуются строками, поэтому тело цикла будет выполняться только тогда, когда процесс создает новую строку. Вы можете изменить это, изменив размер буфера вывода команды с помощью _out_bufsize .

    Примечание. Если нужен полностью неблокирующий итератор, то используйте специальный ключевой аргумент _iter_noblock . Если текущая итерация будет заблокирована, то будет возвращен errno.EWOULDBLOCK , в противном случае получите кусок вывода.

    Запуск команды в фоновом процессе.

    По умолчанию каждая запущенная команда терминала через модуль sh блокируется до завершения. Если какая-то команда выполняется длительное время, то можно запустить ее в фоновом режиме с помощью специального ключевого аргумента _bg=True :

    import sh # блокирующий вызов sh.sleep(3) print(". 3 секунды спустя") # НЕ блокирующий вызов p = sh.sleep(3, _bg=True) print("печать сразу после запуска!") # ждем завершения `sh.sleep` p.wait() print(". и 3 секунды спустя") 

    Наверное заметили, чтобы возвратить результат после выхода из команды нужно вызвать метод RunningCommand.wait() .

    Команды, запущенные в фоновом режиме, игнорируют SIGHUP , это означает, что когда их контролирующий процесс (лидер сеанса, если есть управляющий терминал) завершается, то ядро ​​не сигнализирует о их завершении. Но поскольку команды модуля sh по умолчанию запускают свои процессы в своих собственных сеансах, то есть они сами являются лидерами сеансов, игнорирование SIGHUP обычно не оказывает никакого влияния. Таким образом, единственный случай, когда игнорирование SIGHUP будет делать что-либо, — это использование ключевого аргумента _new_session=False , и в этом случае контролирующим процессом, вероятно, будет оболочка, из которой запустили python, и выход из этой оболочки обычно отправляет SIGHUP всем дочерним процессам.

    Использование обратных вызовов с фоновым процессом.

    В сочетании с _bg=True , модуль sh может использовать обратные вызовы для постепенной обработки выходных данных, передавая вызываемую функцию в ключевые аргументы _out и/или _err . Этот вызываемый объект будет вызываться для каждой строки (или фрагмента) данных, которые выводит команда терминала:

    from sh import tail # обратный вызов def process_output(line): print(line) p = tail("-f", "/var/log/some_log_file.log", _out=process_output, _bg=True) p.wait() 

    Чтобы указать, получает ли обратный вызов строку или фрагмент, используйте ключевой аргумент _out_bufsize . Чтобы «выйти» из обратного вызова, необходимо, что бы функция обратного вызова возвратила True . Это говорит команде больше не вызывать функцию обратного вызова.

    Примечание. Возврат True не убивает процесс, он только предотвращает повторный вызов обратного вызова. Смотрите «Интерактивные обратные вызовы», чтобы узнать, как убить процесс из обратного вызова.

    Интерактивные обратные вызовы.

    Команды, вызываемые при помощи модуля sh могут взаимодействовать с базовым процессом в интерактивном режиме через определенную сигнатуру обратного вызова. Каждая команда, запущенная через sh , имеет внутреннюю очередь STDIN , которую можно использовать из обратных вызовов:

    import sh def interact(line, stdin): if line == "Какова. скорость ласточки?": stdin.put("Африканская или европейская ласточка?") elif line == "Хм? Я . этого не знаю": cross_bridge() return True else: stdin.put("Я не знаю. AAGGHHHHH") return True p = sh.bridgekeeper(_out=interact, _bg=True) p.wait() 

    Примечание. Если используете очередь, то можно сигнализировать об окончании ввода (EOF) с помощью значения очереди None .

    Можно также убить или завершить процесс или отправить любой сигнал, из обратного вызова, добавив третий аргумент в callback функцию для получения объекта процесса:

    import sh def process_output(line, stdin, process): print(line) if "ERROR" in line: process.kill() return True p = sh.tail("-f", "/var/log/some_log_file.log", _out=process_output, _bg=True) 

    Приведенный выше код будет выполняться, печатая строки из some_log_file.log до тех пор, пока в строке не появится слово ‘ERROR’ , после чего процесс команды tail будет убит и сценарий завершится.

    Примечание. Можно также использовать RunningCommand.terminate() для отправки SIGTERM или RunningCommand.signal() для отправки другого сигнала.

    Обратный вызов при завершении команды.

    Функция обратного вызова, переданная ключевому аргументу с именем _done вызывается, когда процесс завершается, либо обычно (через успешный или ошибочный код выхода), либо через сигнал.

    Вот пример использования ключевого аргумента _done для создания многопроцессорного пула, где sh.your_parallel_command выполняется одновременно не более 10 раз:

    import sh from threading import Semaphore pool = Semaphore(10) def done(cmd, success, exit_code): pool.release() def do_thing(arg): pool.acquire() return sh.your_parallel_command(arg, _bg=True, _done=done) procs = [] for arg in range(100): procs.append(do_thing(arg)) [p.wait() for p in procs] 

    Я создал скрипт, который перемещает файлы из одной папки в другую. Но поскольку исходная папка является папкой «Загрузки», мне нужно, чтобы она всегда работала в фоновом режиме.

    У меня также есть стандартный пакетный файл, который выглядит примерно так:

    @py C:\PythonScriptsmoveDLs.py %* 

    Я использую Windows 10 . Я нашел информацию для Linux и OS о том, как использовать nohup в командном файле. Есть ли версия для Windows?

    Если есть, нужно ли выполнять скрипт каждый раз при перезагрузке или включении ПК?

    Кроме того, как вы прекращаете процесс, когда вам удается сделать его постоянным?

    3 ответа

    В Windows вы можете использовать pythonw.exe по порядку для запуска скрипта Python в качестве фонового процесса:

    Скрипты Python (файлы с расширением .py ) будут выполняться python.exe по умолчанию. Этот исполняемый файл открывает терминал, который остается открывать, даже если программа использует графический интерфейс. Если вы не хотите случится, используйте расширение .pyw , которое заставит скрипт быть выполняется pythonw.exe по умолчанию (оба исполняемых файла находятся в верхний уровень каталога установки Python). Это подавляет окно терминала при запуске.

    C:ThanosDoddPython3.6pythonw.exe C:\PythonScriptsmoveDLs.py 

    Чтобы ваш скрипт работал непрерывно, вы можете использовать sched для планирования мероприятий:

    Модуль sched определяет класс, который реализует планировщик событий общего назначения.

    import sched import time event_schedule = sched.scheduler(time.time, time.sleep) def do_something(): print("Hello, World!") event_schedule.enter(30, 1, do_something, (sc,)) event_schedule.enter(30, 1, do_something, (s,)) event_schedule.run() 

    Теперь, чтобы убить фоновый процесс в Windows, вам нужно просто запустить:

    taskkill /pid processId /f 

    Где processId — это идентификатор процесса, который вы хотите убить.

    Giorgos Myrianthous
    19 Сен 2020 в 00:05

    Я нашел решение, которое работает:

    import shutil, os, time while True: for filename in os.listdir('folderToMoveFrom'): if filename.endswith((desired file extensions)): shutil.move( (folderToMoveFrom + filename), folderToMoveTo) time.sleep(6) 

    Если вы выполнили приведенный выше код без функции time.sleep (), программа вылетает после того, как новый файл входит в папку из-за ошибки «файл не найден», вложенной в другую ошибку «файл не найден». Не уверен, о чем идет речь, но я доволен тем, что у меня есть до сих пор. Единственное, что вам нужно сделать сейчас, это добавить скрипт в планировщик задач для запуска под Pythonw, чтобы он работал как фоновый процесс. Или вместо запуска сценария вы можете запустить командный файл, если не забываете добавить соответствующую инструкцию для pythonw. Вы должны начать процесс только один раз, конечно.

    Thanos Dodd
    4 Дек 2019 в 16:36

    Одним из вариантов является изменение вашего сценария, чтобы он работал непрерывно, а не многократно. Просто оберните все это во время цикла и добавьте сон.

    import time while True: your_script_here time.sleep(300) 

    Чтобы убедиться, что это запускается на машине, и для обеспечения автоматического перезапуска в случае возникновения исключительной ситуации, я бы рекомендовал сделать это службой Windows с помощью диспетчера службы не-сосания (www.nssm.cc). Для этого есть несколько шагов (см. Документацию), но после этого ваш скрипт станет просто еще одной службой Windows, которую вы можете запускать и останавливать с помощью стандартной утилиты services.msc.

    SimonN
    1 Дек 2019 в 14:01

    Оба захватывают вывод и запускаются в фоновом режиме с threading

    Как упоминалось на этот ответ, если вы захватите вывод с помощью stdout= а затем попробуйте read() , то процесс блокируется.

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

    Компания threading модуль позволяет нам это делать.

    Во-первых, посмотрите, как выполнить только часть перенаправления вывода в этом вопросе: Python Popen: запись в стандартный вывод и файл журнала одновременно

    #!/usr/bin/env python3 import os import subprocess import sys import threading def output_reader(proc, file): while True: byte = proc.stdout.read(1) if byte: sys.stdout.buffer.write(byte) sys.stdout.flush() file.buffer.write(byte) else: break with subprocess.Popen(['./sleep.py', '0'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc1, subprocess.Popen(['./sleep.py', '10'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc2, open('log1.log', 'w') as file1, open('log2.log', 'w') as file2: t1 = threading.Thread(target=output_reader, args=(proc1, file1)) t2 = threading.Thread(target=output_reader, args=(proc2, file2)) t1.start() t2.start() t1.join() t2.join() 
    #!/usr/bin/env python3 import sys import time for i in range(4): print(i + int(sys.argv[1])) sys.stdout.flush() time.sleep(0.5) 
    ./main.py 

    stdout обновляется каждые 0.5 секунды для каждых двух строк, содержащих:

    0 10 1 11 2 12 3 13 

    и каждый файл журнала содержит соответствующий журнал для данного процесса.

    Проверено на Ubuntu 18.04, Python 3.6.7.

    И захватывать вывод и работать на фоне с threading

    Как уже упоминалось в этом ответе, если вы перехватываете вывод с помощью stdout= а затем пытаетесь read() , тогда процесс блокируется.

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

    Модуль threading позволяет нам это делать.

    Во-первых, рассмотрим, как выполнить часть перенаправления вывода в этом вопросе: Python Popen: одновременная запись в стандартный вывод и файл журнала

    #!/usr/bin/env python3 import os import subprocess import sys import threading def output_reader(proc, file): while True: byte = proc.stdout.read(1) if byte: sys.stdout.buffer.write(byte) sys.stdout.flush() file.buffer.write(byte) else: break with subprocess.Popen(['./sleep.py', '0'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc1, subprocess.Popen(['./sleep.py', '10'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc2, open('log1.log', 'w') as file1, open('log2.log', 'w') as file2: t1 = threading.Thread(target=output_reader, args=(proc1, file1)) t2 = threading.Thread(target=output_reader, args=(proc2, file2)) t1.start() t2.start() t1.join() t2.join() 
    #!/usr/bin/env python3 import sys import time for i in range(4): print(i + int(sys.argv[1])) sys.stdout.flush() time.sleep(0.5) 
    ./main.py 

    stdout обновляется каждые 0,5 секунды для каждых двух строк:

    0 10 1 11 2 12 3 13 

    и каждый файл журнала содержит соответствующий журнал для данного процесса.

    Протестировано на Ubuntu 18.04, Python 3.6.7.

    Все библиотеки для разработки приложений работают с главным циклом, который обрабатывает такие события, как отображение окна на экране, его перемещение, изменение размера, реакция на нажатие кнопки. Словом, любое взаимодействие с интерфейсом.

    Некоторые из этих событий могут быть связаны с функцией, которую мы предоставляем.

    Например, метод button1_pressed(), который вызывается этой библиотекой, когда пользователь нажимает на элемент управления button1.

    При работе с Qt способ реагирования на эти события обычно заключается в подключении сигнала к слоту.

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

    Это приводит к тому, что процессор занят выполнением нашей задачи и не может заниматься основным циклом приложения.

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

    Перейдем к конкретному примеру.

    Следующий код дублирует окно с меткой (QLabel) и кнопкой (QPushButton), которая при нажатии загружает файл, используя стандартный модуль urllib.request.

    #!/usr/bin/env python # -*- coding: utf-8 -*- from urllib.request import urlopen from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel, QPushButton class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("Пример загрузки файла") self.resize(400, 300) self.label = QLabel("Нажмите кнопку, чтобы начать загрузку.", self) self.label.setGeometry(20, 20, 200, 25) self.button = QPushButton("Начать скачивание", self) self.button.move(20, 60) self.button.pressed.connect(self.downloadFile) def downloadFile(self): self.label.setText("Загрузка файла. ") # Отключение кнопки во время загрузки файла. self.button.setEnabled(False) url = "https://www.python.org/ftp/python/3.7.2/python-3.7.2.exe" filename = "python-3.7.2.exe" # Открываем URL. with urlopen(url) as r: with open(filename, "wb") as f: # Чтение удаленного файла и запись локального файла. f.write(r.read()) self.label.setText("Файл загружен!") # Сброс кнопки. self.button.setEnabled(True) if __name__ == "__main__": app = QApplication([]) window = MainWindow() window.show() app.exec_()

    Вы заметите, что во время загрузки, которая в моем случае длится около пяти секунд, интерфейс полностью замирает.

    Мы уже упоминали причину такого поведения: в данном конкретном коде строка, блокирующая выполнение, – это строка 32, где вызывается метод r.read(), который читает содержимое удаленного файла.

    Разберем три решения этой же проблемы с их сильными и слабыми сторонами.

    Первое решение: потоки

    Это решение предполагает запуск нового потока для выполнения нашей трудоемкой задачи.

    Поскольку основной цикл Qt выполняется в основном потоке программы, а наша операция выполняется во вторичном потоке, интерфейс остается активным, пока файл загружается в фоновом режиме.

    Для этого мы используем класс QThread, который предоставляет кроссплатформенный API для создания потоков.

    #!/usr/bin/env python # -*- coding: utf-8 -*- from urllib.request import urlopen from PyQt5.QtCore import QThread from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel, QPushButton class Downloader(QThread): def __init__(self, url, filename): super().__init__() self._url = url self._filename = filename def run(self): # Открываем URL. with urlopen(self._url) as r: with open(self._filename, "wb") as f: # Чтение содержимого и запись его в новый файл. f.write(r.read()) class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("Пример загрузки файла") self.resize(400, 300) self.label = QLabel("Нажмите кнопку , чтобы начать загрузку.", self) self.label.setGeometry(20, 20, 200, 25) self.button = QPushButton("Начать скачивание", self) self.button.move(20, 60) self.button.pressed.connect(self.initDownload) def initDownload(self): self.label.setText("Загрузка файла. ") # Отключение кнопки на время загрузки файла. self.button.setEnabled(False) # Выполнение загрузки в новом потоке. self.downloader = Downloader( "https://www.python.org/ftp/python/3.7.2/python-3.7.2.exe", "python-3.7.2.exe" ) # Qt вызовет метод `downloadFinished()`, когда поток завершится. self.downloader.finished.connect(self.downloadFinished) self.downloader.start() def downloadFinished(self): self.label.setText("Файл загружен!") # Сброс кнопки. self.button.setEnabled(True) # Удаление потока после его использования. del self.downloader if __name__ == "__main__": app = QApplication([]) window = MainWindow() window.show() app.exec_()

    Ядром кода является класс Downloader, который наследуется от QThread и повторно реализует метод run() (строка 17), содержимое которого будет выполняться в новом потоке, когда мы создадим экземпляр и вызовем метод start() (строки 43 и 50).

    В строке 49 мы соединяем сигнал finished, который отдает Qt, когда поток завершает выполнение, с нашим методом downloadFinished().

    Хотя в примере речь идет о загрузке файла, этот метод позволяет переместить любую ресурсоемкую задачу в новый поток: просто поместите его внутрь метода run().

    С другой стороны, многопоточное программирование должно выполняться с особой осторожностью.

    Только метод run() выполняется в новом потоке, в то время как все остальные (включая сам Downloader.init()) выполняются в основном.

    Кроме того, важно быть осторожным, чтобы не разделять объекты, к которым могут одновременно обращаться два или более потоков.

    Второе решение: processEvents()

    Альтернативой запуску нового потока является выполнение всей работы в основном потоке, но периодически позволяя Qt обрабатывать события приложения, чтобы интерфейс не перестал отвечать.

    Функция, которая позаботится об этом, – QCoreApplication.processEvents().

    В предыдущих вариантах кода функция, которая выполняет тяжелую работу и блокирует выполнение на несколько секунд, была r.read().

    Поскольку этот метод не возвращает нам контроль над программой, пока файл не будет полностью загружен, мы должны создать собственный цикл, который получает удаленный файл небольшими пакетами (128 байт) и в то же время позволяет Qt обрабатывать его события.

    К счастью для нас, read() опционально принимает в качестве аргумента количество байт данных, которые мы хотим прочитать.

    #!/usr/bin/env python # -*- coding: utf-8 -*- from urllib.request import urlopen from PyQt5.QtCore import QCoreApplication from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel, QPushButton class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("Пример загрузки файла") self.resize(400, 300) self.label = QLabel("Нажмите кнопку , чтобы начать загрузку.", self) self.label.setGeometry(20, 20, 200, 25) self.button = QPushButton("Начать скачивание", self) self.button.move(20, 60) self.button.pressed.connect(self.downloadFile) def downloadFile(self): self.label.setText("Загрузка файла. ") # Отключение кнопки на время загрузки файла. self.button.setEnabled(False) # Открываем URL. url = "https://www.python.org/ftp/python/3.7.2/python-3.7.2.exe" filename = "python-3.7.2.exe" with urlopen(url) as r: with open(filename, "ab") as f: while True: # Qt обрабатывает ваши события, чтобы сохранить отзывчивость окна. QCoreApplication.processEvents() # Чтение части загружаемого файла. chunk = r.read(128) # Если результат равен `None`, это означает, что данные еще не были загружены. Мы просто продолжаем ждать. if chunk is None: continue # Если результатом является пустой экземпляр `bytes`, это означает, что файл готов. elif chunk == b"": break # Запись загруженной части в локальный файл. f.write(chunk) self.label.setText("Файл загружен!") # Сброс кнопки. self.button.setEnabled(True) if __name__ == "__main__": app = QApplication([]) window = MainWindow() window.show() app.exec_()

    Здесь ключ лежит между строками 32-48, где строится цикл, который обрабатывает события Qt, потребляет кусок информации из сети и отправляет его в локальный файл, до тех пор, пока больше нет данных для извлечения.

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

    Однако r.read(128) все равно является вызовом, который блокирует выполнение кода даже на очень короткое время, практически незаметное.

    Если скорость интернет-соединения слишком низкая, даже извлечение этого небольшого количества байтов может привести к зависанию пользовательского интерфейса.

    Третье решение: Twisted

    Модуль qt5reactor позволяет объединить основные циклы Twisted и Qt в одном приложении, предоставляя нам доступ ко всему арсеналу асинхронных функций, предоставляемых сетевой библиотекой.

    Для этого третьего решения мы также будем использовать библиотеку treq (похожую на Requests, но построенную на базе Twisted) для доступа к URL файла и загрузки содержимого.

    Мы устанавливаем эти два инструмента просто с помощью pip:

    pip install qt5reactor treq

    Теперь код выглядит следующим образом:

    #!/usr/bin/env python # -*- coding: utf-8 -*- from PyQt5.QtCore import QCoreApplication from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel, QPushButton from twisted.internet.defer import inlineCallbacks class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("Пример загрузки файла") self.resize(400, 300) self.label = QLabel("Нажмите кнопку , чтобы начать загрузку.", self) self.label.setGeometry(20, 20, 200, 25) self.button = QPushButton("Начать скачивание", self) self.button.move(20, 60) self.button.pressed.connect(self.initDownload) def initDownload(self): self.label.setText("Загрузка файла. ") # Отключение кнопки на время загрузки файла. self.button.setEnabled(False) url = "https://www.python.org/ftp/python/3.7.2/python-3.7.2.exe" # Метод `requestSucceeded()` будет вызван, когда соединение с URL будет успешно установлено. treq.get(url).addCallback(self.requestSucceeded) @inlineCallbacks def requestSucceeded(self, response): # Мы получаем содержимое удаленного файла. Обратите внимание, что эта операция не блокирует выполнение. content = yield response.content() # Записываем его в локальный файл. with open("python-3.7.2.exe", "wb") as f: f.write(content) self.label.setText("Файл загружен!") # Сброс кнопки. self.button.setEnabled(True) def closeEvent(self, event): QCoreApplication.instance().quit() if __name__ == "__main__": app = QApplication([]) import qt5reactor qt5reactor.install() window = MainWindow() window.show() from twisted.internet import reactor import treq import os import certifi # Требуется для соединений HTTPS. os.environ["SSL_CERT_FILE"] = certifi.where() reactor.run()

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

    HTTP-запросы библиотеки treq основаны на логике отложенных запросов, которые похожи на сигналы Qt при вызове определенного события.

    Здесь нам также не придется иметь дело с проблемами совместного использования объектов между потоками, поскольку Twisted всегда работает в главном потоке.

    Те, кто немного разбирается в Twisted, найдут это решение весьма удачным.

    И это действительно так: Qt и Twisted очень хорошо подходят друг другу благодаря своей структуре, философии и даже соглашениям об именах.

    Заключение

    Мы рассмотрели все три решения – какое из них лучше всего подходит для вашей задачи?

    Приведем основные моменты каждого из них.

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

    Если ваше приложение использует много HTTP-запросов или обращается к любым другим ресурсам в Интернете (а также к локальным файлам) и вы разбираетесь в Twisted (или нет, но вам было бы интересно побаловаться с ним), вы будете рады решению с qt5reactor и treq.

    Наконец, если ваш код позволяет это сделать, простое добавление вызова QCoreApplication.processEvents() в нужном месте сделает ваш интерфейс отзывчивым к взаимодействию с пользователем.