Как закончить цикл изящно, когда CTRL + C был нажат на питоне

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

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

a) Блок try-except

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 не выполнена изящно.

b) 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-разрядный)" на 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

Ответ 1

Я бы просто использовал обработчик исключений, который поймал бы 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()

Ответ 2

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

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

Ответ 3

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

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