Как правильно завершить цикл при нажатии CTRL+C в Python

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

Вот что я попробовал.

а) Попробуйте - кроме блока

x = 1
print "Script started."
while True:
 try:
  print "Processing file #",x,"started...",
  # do something time-cosnuming
  time.sleep(1)
  x += 1
  print " finished."
 except KeyboardInterrupt:
  print "Bye"
  print "x=",x
  sys.exit()

sys.exit()

Выход:

Script started.
Processing file # 1 started...  finished.
Processing file # 2 started...  finished.
Processing file # 3 started... Bye
x= 3

Итерация № 3 не закончена изящно.

б) sys.excepthook

OriginalExceptHook = sys.excepthook
def NewExceptHook(type, value, traceback):
global Terminator
    Terminator = True
    if type == KeyboardInterrupt:
        #exit("\nExiting by CTRL+C.")   # this line was here originally
        print("\n\nExiting by CTRL+C.\n\n")
    else:
        OriginalExceptHook(type, value, traceback)
sys.excepthook = NewExceptHook

global Terminator
Terminator = False

x = 1
while True:
  print "Processing file #",x,"started...",
  # do something time-cosnuming
  time.sleep(1)
  x += 1
  print " finished."
  if Terminator:
   print "I'll be back!"
   break

print "Bye"
print "x=",x
sys.exit()

Выход:

Script started.
Processing file # 1 started...  finished.
Processing file # 2 started...  finished.
Processing file # 3 started...

Exiting by CTRL+C.

Итерация № 3 не закончена изящно.

UPD # 1

@mguijarr, я немного изменил код так:

import time, sys

x = 1
print "Script started."
stored_exception=None

while True:
    try:
        print "Processing file #",x,"started...",
        # do something time-cosnuming
        time.sleep(1)
        print "Processing file #",x,"part two...",
        time.sleep(1)
        print " finished."
        if stored_exception:
            break
        x += 1
    except KeyboardInterrupt:
        print "[CTRL+C detected]",
        stored_exception=sys.exc_info()

print "Bye"
print "x=",x

if stored_exception:
    raise stored_exception[0], stored_exception[1], stored_exception[2]

sys.exit()

Вывод (протестирован с использованием "Python 2.7.6:: Anaconda 2.0.0 (64-bit)" на Win7-64bit):

Script started.
Processing file # 1 started... Processing file # 1 part two...  finished.
Processing file # 2 started... Processing file # 2 part two...  finished.
Processing file # 3 started... [CTRL+C detected] Processing file # 3 started... Processing file # 3 part two...  finished.
Bye
x= 3
Traceback (most recent call last):
  File "test2.py", line 12, in <module>
    time.sleep(1)
KeyboardInterrupt

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

Я удалил запятые в выражениях "print" и добавил еще кое-что, чтобы увидеть, что итерация действительно перезапущена:

import time, sys

x = 1
y = 0
print "Script started."
stored_exception=None

while True:
    try:
        y=x*1000
        y+=1
        print "Processing file #",x,y,"started..."
        y+=1
        # do something time-cosnuming
        y+=1
        time.sleep(1)
        y+=1
        print "Processing file #",x,y,"part two..."
        y+=1
        time.sleep(1)
        y+=1
        print " finished.",x,y
        y+=1
        if stored_exception:
            break
        y+=1
        x += 1
        y+=1
    except KeyboardInterrupt:
        print "[CTRL+C detected]",
        stored_exception=sys.exc_info()

print "Bye"
print "x=",x
print "y=",y

if stored_exception:
    raise stored_exception[0], stored_exception[1], stored_exception[2]

sys.exit()

и вывод:

Script started.
Processing file # 1 1001 started...
Processing file # 1 1004 part two...
 finished. 1 1006
Processing file # 2 2001 started...
Processing file # 2 2004 part two...
[CTRL+C detected] Processing file # 2 2001 started...
Processing file # 2 2004 part two...
 finished. 2 2006
Bye
x= 2
y= 2007
Traceback (most recent call last):
  File "test2.py", line 20, in <module>
    time.sleep(1)
KeyboardInterrupt

4 ответа

Решение

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

Это работает (протестировано с Python 2.7):

x = 1
print "Script started."
stored_exception=None

while True:
    try:
        print "Processing file #",x,"started...",
        # do something time-cosnuming
        time.sleep(1)
        print " finished."
        if stored_exception:
            break
        x += 1
    except KeyboardInterrupt:
        stored_exception=sys.exc_info()

print "Bye"
print "x=",x

if stored_exception:
    raise stored_exception[0], stored_exception[1], stored_exception[2]

sys.exit()

РЕДАКТИРОВАТЬ: как это было замечено в комментариях, этот ответ не удовлетворяет для оригинального плаката, вот решение, основанное на темах:

import time
import sys
import threading

print "Script started."

class MyProcessingThread(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)

    def run(self):
        print "Processing file #",x,"started...",
        # do something time-cosnuming
        time.sleep(1)
        print " finished."

for x in range(1,4):
    task = MyProcessingThread()
    task.start()
    try:
        task.join()
    except KeyboardInterrupt:
        break

print "Bye"
print "x=",x

sys.exit()

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

import signal
import time

class GracefulExiter():

    def __init__(self):
        self.state = False
        signal.signal(signal.SIGINT, self.change_state)

    def change_state(self, signum, frame):
        print("exit flag set to True (repeat to exit now)")
        signal.signal(signal.SIGINT, signal.SIG_DFL)
        self.state = True

    def exit(self):
        return self.state


x = 1
flag = GracefulExiter()
while True:
    print("Processing file #",x,"started...")
    time.sleep(1)
    x+=1
    print(" finished.")
    if flag.exit():
        break

Вы можете написать функцию обработки сигналов

import signal,sys,time                          
terminate = False                            

def signal_handling(signum,frame):           
    global terminate                         
    terminate = True                         

signal.signal(signal.SIGINT,signal_handling) 
x=1                                          
while True:                                  
    print "Processing file #",x,"started..." 
    time.sleep(1)                            
    x+=1                                     
    if terminate:                            
        print "I'll be back"                 
        break                                
print "bye"                                  
print x

нажатие Ctrl+c отправляет прерывание SIGINT, которое выдает:

Processing file # 1 started...
Processing file # 2 started...
^CI'll be back
bye
3

Поздно на вечеринку, но это было мое решение:

      #!/usr/bin/env python
# -*- coding: utf8 -*-

import sys
import signal
import time

interrupted = False

def main():
    global interrupted
    oldHandler = signal.signal(signal.SIGINT, handle_interrupt)

    x = 1
    print "Script started"
    while True:
      print "Processing file #",x,"started...",
      # do something time-cosnuming
      time.sleep(1)
      x += 1
      print " finished."
      if interrupted:
        break

    signal.signal(signal.SIGINT, oldHandler)

def handle_interrupt(sig, frame):
    global interrupted
    interrupted = True

if __name__ == '__main__':
    sys.exit(main())
Другие вопросы по тегам