ฉันจะจับ SIGINT ใน Python ได้อย่างไร


535

ฉันกำลังทำงานกับสคริปต์หลามที่เริ่มต้นกระบวนการและการเชื่อมต่อฐานข้อมูลหลายอย่าง ทุก ๆ ครั้งที่ฉันต้องการฆ่าสคริปต์ด้วยสัญญาณCtrl+ Cและฉันต้องการจะล้างข้อมูล

ใน Perl ฉันจะทำสิ่งนี้:

$SIG{'INT'} = 'exit_gracefully';

sub exit_gracefully {
    print "Caught ^C \n";
    exit (0);
}

ฉันจะทำอะนาล็อกของสิ่งนี้ใน Python ได้อย่างไร

คำตอบ:


787

ลงทะเบียนตัวจัดการของคุณด้วยsignal.signalสิ่งนี้:

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

def signal_handler(sig, frame):
    print('You pressed Ctrl+C!')
    sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
print('Press Ctrl+C')
signal.pause()

รหัสดัดแปลงมาจากที่นี่

เอกสารเพิ่มเติมเกี่ยวกับsignalสามารถพบได้ที่นี่  


13
คุณช่วยบอกฉันได้ไหมว่าทำไมต้องใช้สิ่งนี้แทนข้อยกเว้น KeyboardInterrupt มันใช้งานง่ายกว่าใช่มั้ย
noio

35
Noio: 2 เหตุผล ก่อนอื่น SIGINT สามารถส่งไปยังกระบวนการของคุณได้หลายวิธี (เช่น 'kill -s INT <pid>') ฉันไม่แน่ใจว่า KeyboardInterruptException มีการใช้งานเป็นตัวจัดการ SIGINT หรือหากกดปุ่ม Ctrl + C เท่านั้น แต่การใช้ตัวจัดการสัญญาณจะทำให้เจตนาของคุณชัดเจน (อย่างน้อยถ้าเจตนาของคุณเหมือนกับ OP) ที่สำคัญกว่านั้นด้วยสัญญาณที่คุณไม่จำเป็นต้องใช้ในการพยายามจับทุกสิ่งเพื่อให้มันทำงานได้ซึ่งอาจจะทำให้คอมโพเน้นท์และการชนะวิศวกรรมซอฟต์แวร์ทั่วไปขึ้นอยู่กับโครงสร้างของแอปพลิเคชัน
Matt J

35
ตัวอย่างของสาเหตุที่คุณต้องการดักจับสัญญาณแทนที่จะจับข้อยกเว้น สมมติว่าคุณรันโปรแกรมและเปลี่ยนเส้นทางผลลัพธ์ไปยังไฟล์บันทึก, ./program.py > output.log. เมื่อคุณกดCtrl-Cคุณต้องการให้โปรแกรมของคุณออกอย่างสง่างามโดยให้มันบันทึกว่าไฟล์ข้อมูลทั้งหมดได้รับการฟลัชและทำเครื่องหมาย clean เพื่อยืนยันว่าไฟล์นั้นอยู่ในสถานะดีที่รู้จัก แต่ Ctrl-C ส่ง SIGINT ไปยังกระบวนการทั้งหมดในไปป์ไลน์ดังนั้นเชลล์อาจปิด STDOUT (ตอนนี้ "output.log") ก่อนที่ program.py จะเสร็จสิ้นการพิมพ์บันทึกสุดท้าย Python จะบ่นว่า "การปิดไม่สำเร็จในตัวทำลายวัตถุไฟล์: ข้อผิดพลาดใน sys.excepthook:"
Noah Spurrier

24
โปรดทราบว่า signal.pause () ไม่พร้อมใช้งานบน Windows docs.python.org/dev/library/signal.html
อาจดำเนินการ

10
-1 ยูนิคอร์นสำหรับการใช้สัญญาณ. หยุดชั่วคราว (), แนะนำว่าฉันจะต้องรอที่การบล็อคการโทรแทนการใช้งานจริง ;)
นิค T

177

คุณสามารถปฏิบัติต่อมันเหมือนมีข้อยกเว้น (KeyboardInterrupt) เหมือนกับอื่น ๆ สร้างไฟล์ใหม่และเรียกใช้จากเชลล์ด้วยเนื้อหาต่อไปนี้เพื่อดูว่าฉันหมายถึงอะไร:

import time, sys

x = 1
while True:
    try:
        print x
        time.sleep(.3)
        x += 1
    except KeyboardInterrupt:
        print "Bye"
        sys.exit()

22
ให้ความสนใจเมื่อใช้โซลูชันนี้ คุณควรใช้รหัสนี้ก่อน KeyboardInterrupt catch block: signal.signal(signal.SIGINT, signal.default_int_handler)หรือคุณกำลังจะล้มเหลวเนื่องจาก KeyboardInterrupt ไม่ทำงานในทุกสถานการณ์ที่ควรทำงาน! โดยมีรายละเอียดที่นี่
Velda

67

และในฐานะผู้จัดการบริบท:

import signal

class GracefulInterruptHandler(object):

    def __init__(self, sig=signal.SIGINT):
        self.sig = sig

    def __enter__(self):

        self.interrupted = False
        self.released = False

        self.original_handler = signal.getsignal(self.sig)

        def handler(signum, frame):
            self.release()
            self.interrupted = True

        signal.signal(self.sig, handler)

        return self

    def __exit__(self, type, value, tb):
        self.release()

    def release(self):

        if self.released:
            return False

        signal.signal(self.sig, self.original_handler)

        self.released = True

        return True

ใช้:

with GracefulInterruptHandler() as h:
    for i in xrange(1000):
        print "..."
        time.sleep(1)
        if h.interrupted:
            print "interrupted!"
            time.sleep(2)
            break

ตัวจัดการซ้อน:

with GracefulInterruptHandler() as h1:
    while True:
        print "(1)..."
        time.sleep(1)
        with GracefulInterruptHandler() as h2:
            while True:
                print "\t(2)..."
                time.sleep(1)
                if h2.interrupted:
                    print "\t(2) interrupted!"
                    time.sleep(2)
                    break
        if h1.interrupted:
            print "(1) interrupted!"
            time.sleep(2)
            break

จากที่นี่: https://gist.github.com/2907502


นอกจากนี้ยังสามารถโยน a StopIterationเพื่อตัดลูปด้านในสุดเมื่อกด ctrl-C ใช่ไหม?
Theo Belaire

@ TheoBelaire แทนที่จะเพียงแค่หยุด StopIteration ฉันจะสร้างตัวสร้างที่ยอมรับ iterable เป็นพารามิเตอร์และลงทะเบียน / เผยแพร่ตัวจัดการสัญญาณ
Udi

28

คุณสามารถจัดการCTRL+ CโดยจับKeyboardInterruptข้อยกเว้น คุณสามารถใช้โค้ดการล้างข้อมูลใด ๆ ในตัวจัดการข้อยกเว้น



18

อีกตัวอย่างหนึ่ง

เรียกว่าmainเป็นฟังก์ชั่นหลักและexit_gracefullyเป็นตัวจัดการCTRL+c

if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        pass
    finally:
        exit_gracefully()

4
คุณควรใช้ยกเว้นสิ่งที่ไม่ควรเกิดขึ้น ในกรณีนี้ KeyboardInterrupt น่าจะเกิดขึ้น ดังนั้นนี่ไม่ใช่การก่อสร้างที่ดี
อุโมงค์

15
@TristanT ในภาษาอื่น ๆ ใช่ แต่ในข้อยกเว้น Python ไม่เพียง แต่สำหรับสิ่งที่ไม่ควรเกิดขึ้น จริงๆแล้วถือว่าเป็นสไตล์ที่ดีใน Python ที่จะใช้ข้อยกเว้นสำหรับการควบคุมการไหล (ตามความเหมาะสม)
Ian Goldby

8

ฉันดัดแปลงรหัสจาก @udi เพื่อรองรับสัญญาณหลายสัญญาณ (ไม่มีอะไรแฟนซี):

class GracefulInterruptHandler(object):
    def __init__(self, signals=(signal.SIGINT, signal.SIGTERM)):
        self.signals = signals
        self.original_handlers = {}

    def __enter__(self):
        self.interrupted = False
        self.released = False

        for sig in self.signals:
            self.original_handlers[sig] = signal.getsignal(sig)
            signal.signal(sig, self.handler)

        return self

    def handler(self, signum, frame):
        self.release()
        self.interrupted = True

    def __exit__(self, type, value, tb):
        self.release()

    def release(self):
        if self.released:
            return False

        for sig in self.signals:
            signal.signal(sig, self.original_handlers[sig])

        self.released = True
        return True

รหัสนี้รองรับการใช้อินเตอร์รัปต์คีย์บอร์ด ( SIGINT) และSIGTERM( kill <process>)


5

ตรงกันข้ามกับคำตอบของMatt Jเขาใช้วัตถุง่าย ๆ สิ่งนี้ทำให้ฉันมีความเป็นไปได้ที่จะแยกวิเคราะห์ตัวจัดการนี้ไปยังกระทู้ทั้งหมดที่จำเป็นต้องหยุดความลับ

class SIGINT_handler():
    def __init__(self):
        self.SIGINT = False

    def signal_handler(self, signal, frame):
        print('You pressed Ctrl+C!')
        self.SIGINT = True


handler = SIGINT_handler()
signal.signal(signal.SIGINT, handler.signal_handler)

ที่อื่น ๆ

while True:
    # task
    if handler.SIGINT:
        break

คุณควรใช้เหตุการณ์หรือtime.sleep()แทนที่จะทำวนรอบที่ยุ่งกับตัวแปร
OlivierM

@OlivierM นี่เป็นแอปพลิเคชั่นที่เฉพาะเจาะจงจริงๆและไม่ใช่จุดของตัวอย่างนี้ ตัวอย่างเช่นการปิดกั้นการโทรหรือรอฟังก์ชั่นจะไม่ทำให้ CPU ไม่ว่าง นอกจากนี้นี่เป็นเพียงตัวอย่างของสิ่งที่สามารถทำได้ KeyboardInterrupts มักจะเพียงพอเช่นที่กล่าวไว้ในคำตอบอื่น ๆ
โทมัส Devoogdt

4

คุณสามารถใช้ฟังก์ชั่นในโมดูลสัญญาณในตัวของ Python เพื่อตั้งค่าตัวจัดการสัญญาณในไพ ธ อน โดยเฉพาะsignal.signal(signalnum, handler)ฟังก์ชั่นที่ใช้ในการลงทะเบียนฟังก์ชั่นการส่งสัญญาณhandlersignalnum


3

ขอบคุณสำหรับคำตอบที่มีอยู่ แต่เพิ่ม signal.getsignal()

import signal

# store default handler of signal.SIGINT
default_handler = signal.getsignal(signal.SIGINT)
catch_count = 0

def handler(signum, frame):
    global default_handler, catch_count
    catch_count += 1
    print ('wait:', catch_count)
    if catch_count > 3:
        # recover handler for signal.SIGINT
        signal.signal(signal.SIGINT, default_handler)
        print('expecting KeyboardInterrupt')

signal.signal(signal.SIGINT, handler)
print('Press Ctrl+c here')

while True:
    pass

3

หากคุณต้องการให้แน่ใจว่ากระบวนการล้างข้อมูลของคุณเสร็จสิ้นฉันจะเพิ่มคำตอบของ Matt Jโดยใช้SIG_IGNเพื่อที่SIGINTจะไม่สนใจสิ่งต่อไปซึ่งจะป้องกันไม่ให้การขัดจังหวะการล้างข้อมูลของคุณ

import signal
import sys

def signal_handler(signum, frame):
    signal.signal(signum, signal.SIG_IGN) # ignore additional signals
    cleanup() # give your process a chance to clean up
    sys.exit(0)

signal.signal(signal.SIGINT, signal_handler) # register the signal with the signal handler first
do_stuff()

0

โดยส่วนตัวฉันไม่สามารถใช้ลอง / ยกเว้น KeyboardInterrupt เพราะฉันใช้โหมดซ็อกเก็ตมาตรฐาน (IPC) ซึ่งกำลังบล็อกอยู่ ดังนั้น SIGINT จึงเข้ามา แต่มาหลังจากได้รับข้อมูลบนซ็อกเก็ตเท่านั้น

การตั้งค่าตัวจัดการสัญญาณทำงานเหมือนกัน

ในทางกลับกันสิ่งนี้ใช้ได้กับเทอร์มินัลจริงเท่านั้น สภาพแวดล้อมที่เริ่มต้นอื่น ๆ อาจไม่ยอมรับCtrl+ Cหรือจัดการสัญญาณล่วงหน้า

นอกจากนี้ยังมี "ข้อยกเว้น" และ "BaseExceptions" ใน Python ซึ่งแตกต่างในแง่ที่ล่ามจำเป็นต้องออกจากตัวเองอย่างหมดจดดังนั้นข้อยกเว้นบางอย่างมีลำดับความสำคัญสูงกว่าคนอื่น ๆ (ข้อยกเว้นมาจาก BaseException)

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.