“ ข้อยกเว้นภายใน” (พร้อมการสืบค้นกลับ) ใน Python หรือไม่


147

พื้นหลังของฉันอยู่ใน C # และฉันเพิ่งเพิ่งเริ่มเขียนโปรแกรมใน Python เมื่อมีข้อผิดพลาดเกิดขึ้นฉันมักต้องการล้อมรอบด้วยข้อยกเว้นอื่นที่เพิ่มข้อมูลเพิ่มเติมขณะที่ยังคงแสดงการติดตามสแต็กเต็ม มันค่อนข้างง่ายใน C # แต่ฉันจะทำใน Python ได้อย่างไร

เช่น. ใน C # ฉันจะทำสิ่งนี้:

try
{
  ProcessFile(filePath);
}
catch (Exception ex)
{
  throw new ApplicationException("Failed to process file " + filePath, ex);
}

ใน Python ฉันสามารถทำสิ่งที่คล้ายกัน:

try:
  ProcessFile(filePath)
except Exception as e:
  raise Exception('Failed to process file ' + filePath, e)

... แต่สิ่งนี้จะสูญเสียการย้อนกลับของข้อยกเว้นภายใน!

แก้ไข:ฉันต้องการเห็นทั้งข้อความข้อยกเว้นและทั้งร่องรอยสแต็กและสัมพันธ์กันทั้งสอง นั่นคือฉันต้องการที่จะเห็นในผลลัพธ์ที่ข้อยกเว้น X เกิดขึ้นที่นี่และจากนั้นยกเว้น Y มี - เช่นเดียวกับฉันใน C # เป็นไปได้ใน Python 2.6 หรือไม่ ดูเหมือนว่าดีที่สุดที่ฉันสามารถทำได้ (ตามคำตอบของ Glenn Maynard) คือ:

try:
  ProcessFile(filePath)
except Exception as e:
  raise Exception('Failed to process file' + filePath, e), None, sys.exc_info()[2]

ซึ่งรวมถึงข้อความและทั้งสองการสืบค้นกลับ แต่ไม่แสดงข้อยกเว้นใดที่เกิดขึ้นในการติดตามกลับ


3
คำตอบที่ยอมรับนั้นเริ่มล้าสมัยบางทีคุณควรลองตอบคำถามใหม่ดู
Aaron Hall

1
@AaronHall น่าเสียดายที่ OP ไม่ได้เห็นมาตั้งแต่ปี 2015
Antti Haapala

คำตอบ:


137

Python 2

มันง่ายมาก ผ่านการสืบค้นกลับเป็นอาร์กิวเมนต์ที่สามเพื่อยกระดับ

import sys
class MyException(Exception): pass

try:
    raise TypeError("test")
except TypeError, e:
    raise MyException(), None, sys.exc_info()[2]

ทำเช่นนี้เสมอเมื่อมีข้อยกเว้นหนึ่งข้อและเพิ่มอีกครั้ง


4
ขอบคุณ ที่รักษาการย้อนกลับ แต่จะสูญเสียข้อความแสดงข้อผิดพลาดของข้อยกเว้นเดิม ฉันจะดูทั้งข้อความและการสืบค้นกลับทั้งสองได้อย่างไร
EMP

6
raise MyException(str(e)), ...เป็นต้น
Glenn Maynard

23
Python 3 เพิ่มraise E() from tbและ.with_traceback(...)
Dima Tisnek

3
@GlennMaynard เป็นคำถามที่ค่อนข้างเก่า แต่อาร์กิวเมนต์กลางของraiseคือค่าที่ส่งผ่านไปยังข้อยกเว้น (ในกรณีที่อาร์กิวเมนต์แรกคือคลาสยกเว้นและไม่ใช่อินสแตนซ์) ดังนั้นถ้าคุณต้องการที่จะยกเว้นแลกเปลี่ยนแทนการทำมันจะดีกว่าที่จะใช้นี้:raise MyException(str(e)), None, sys.exc_info()[2] raise MyException, e.args, sys.exc_info()[2]
bgusach

8
Python2 และ 3 วิธีที่สอดคล้องกับความเป็นไปได้โดยใช้แพคเกจในอนาคต: python-future.org/compatible_idioms.html#raising-exceptionsเช่นและfrom future.utils import raise_ raise_(ValueError, None, sys.exc_info()[2])
jtpereyda

239

Python 3

ใน python 3 คุณสามารถทำสิ่งต่อไปนี้:

try:
    raise MyExceptionToBeWrapped("I have twisted my ankle")

except MyExceptionToBeWrapped as e:

    raise MyWrapperException("I'm not in a good shape") from e

สิ่งนี้จะสร้างสิ่งนี้:

   Traceback (most recent call last):
   ...
   MyExceptionToBeWrapped: ("I have twisted my ankle")

The above exception was the direct cause of the following exception:

   Traceback (most recent call last):
   ...
   MyWrapperException: ("I'm not in a good shape")

17
raise ... from ...แน่นอนเป็นวิธีที่ถูกต้องในการทำเช่นนี้ใน Python 3 นี้ต้องการ upvotes มากขึ้น
Nakedible

Nakedibleฉันคิดว่าเป็นเพราะน่าเสียดายที่คนส่วนใหญ่ยังไม่ได้ใช้ Python 3
ทิม Ludwinski

สิ่งนี้ดูเหมือนจะเกิดขึ้นแม้ว่าจะใช้ 'จาก' ใน python 3
Steve Vermeulen

อาจจะย้อนไปที่ Python 2 หวังว่าสักวันหนึ่ง
Marcin Wojnarski

4
@ogrisel คุณสามารถใช้futureแพคเกจเพื่อให้บรรลุนี้: python-future.org/compatible_idioms.html#raising-exceptionsเช่นและfrom future.utils import raise_ raise_(ValueError, None, sys.exc_info()[2])
jtpereyda

19

งูหลาม 3 มีraise... fromข้อข้อยกเว้นโซ่ คำตอบของ Glennนั้นยอดเยี่ยมสำหรับ Python 2.7 แต่ใช้การย้อนกลับของข้อยกเว้นดั้งเดิมเท่านั้นและทิ้งข้อความแสดงข้อผิดพลาดและรายละเอียดอื่น ๆ นี่คือตัวอย่างบางส่วนใน Python 2.7 ที่เพิ่มข้อมูลบริบทจากขอบเขตปัจจุบันลงในข้อความแสดงข้อผิดพลาดของข้อยกเว้นเดิม แต่เก็บรายละเอียดอื่นไว้ไม่เปลี่ยนแปลง

ประเภทข้อยกเว้นที่ทราบ

try:
    sock_common = xmlrpclib.ServerProxy(rpc_url+'/common')
    self.user_id = sock_common.login(self.dbname, username, self.pwd)
except IOError:
    _, ex, traceback = sys.exc_info()
    message = "Connecting to '%s': %s." % (config['connection'],
                                           ex.strerror)
    raise IOError, (ex.errno, message), traceback

รสชาติของraiseข้อความสั่งนั้นใช้ประเภทการยกเว้นเป็นนิพจน์แรกอาร์กิวเมนต์ตัวสร้างคลาสข้อยกเว้นในทูเปิลเป็นนิพจน์ที่สองและการย้อนกลับเป็นนิพจน์ที่สาม หากคุณกำลังใช้ก่อนหน้านี้กว่า 2.2 sys.exc_info()หลามดูคำเตือนบน

ประเภทข้อยกเว้นใด ๆ

นี่คืออีกตัวอย่างหนึ่งที่มีวัตถุประสงค์ทั่วไปมากขึ้นหากคุณไม่ทราบว่ามีข้อยกเว้นประเภทใดที่รหัสของคุณอาจต้องตรวจจับ ข้อเสียคือมันเสียประเภทข้อยกเว้นและเพียงแค่เพิ่ม RuntimeError คุณต้องนำเข้าtracebackโมดูล

except Exception:
    extype, ex, tb = sys.exc_info()
    formatted = traceback.format_exception_only(extype, ex)[-1]
    message = "Importing row %d, %s" % (rownum, formatted)
    raise RuntimeError, message, tb

ปรับเปลี่ยนข้อความ

นี่คือตัวเลือกอื่นถ้าประเภทข้อยกเว้นจะช่วยให้คุณเพิ่มบริบท คุณสามารถแก้ไขข้อความของข้อยกเว้นจากนั้นทำการอ่านซ้ำ

import subprocess

try:
    final_args = ['lsx', '/home']
    s = subprocess.check_output(final_args)
except OSError as ex:
    ex.strerror += ' for command {}'.format(final_args)
    raise

ที่สร้างการติดตามสแต็กต่อไปนี้:

Traceback (most recent call last):
  File "/mnt/data/don/workspace/scratch/scratch.py", line 5, in <module>
    s = subprocess.check_output(final_args)
  File "/usr/lib/python2.7/subprocess.py", line 566, in check_output
    process = Popen(stdout=PIPE, *popenargs, **kwargs)
  File "/usr/lib/python2.7/subprocess.py", line 710, in __init__
    errread, errwrite)
  File "/usr/lib/python2.7/subprocess.py", line 1327, in _execute_child
    raise child_exception
OSError: [Errno 2] No such file or directory for command ['lsx', '/home']

คุณสามารถเห็นว่ามันแสดงบรรทัดที่check_output()ถูกเรียก แต่ตอนนี้ข้อความยกเว้นมีบรรทัดคำสั่ง


1
อยู่ที่ไหนex.strerrorมาจากไหน ฉันไม่พบการเข้าชมที่เกี่ยวข้องใด ๆ สำหรับสิ่งนั้นใน Python docs ไม่ควรstr(ex)หรือ
Henrik Heimbuerger

1
IOErrorมาจากEnvironmentError@hheimbuerger ซึ่งให้บริการerrornoและstrerrorคุณลักษณะ
Don Kirkby

ฉันจะห่อคำสั่งตามอำเภอใจErrorเช่น ValueError ลงไปRuntimeErrorโดยจับได้Exceptionอย่างไร หากฉันทำซ้ำคำตอบของคุณสำหรับกรณีนี้สแต็คเทรซจะหายไป
Karl Richter

ฉันไม่แน่ใจว่าสิ่งที่คุณถาม @karl คุณสามารถโพสต์ตัวอย่างในคำถามใหม่แล้วเชื่อมโยงกับมันได้จากที่นี่
Don Kirkby

ฉันแก้ไขคำถาม OP ของฉันซ้ำที่stackoverflow.com/questions/23157766/…โดยมีการชี้แจงโดยคำนึงถึงคำตอบของคุณโดยตรง เราควรพูดคุยกันที่นั่น :)
Karl Richter

12

ในPython 3.x :

raise Exception('Failed to process file ' + filePath).with_traceback(e.__traceback__)

หรือเพียงแค่

except Exception:
    raise MyException()

ซึ่งจะเผยแพร่MyExceptionแต่พิมพ์ทั้งสองอย่างข้อยกเว้นถ้ามันจะไม่ได้รับการจัดการ

ในPython 2.x :

raise Exception, 'Failed to process file ' + filePath, e

คุณสามารถป้องกันการพิมพ์ทั้งสองข้อยกเว้นโดยการฆ่า__context__แอตทริบิวต์ ที่นี่ฉันเขียนผู้จัดการบริบทโดยใช้สิ่งนั้นเพื่อจับและเปลี่ยนข้อยกเว้นของคุณได้ทันที: (ดูhttp://docs.python.org/3.1/library/stdtypes.htmlเพื่อดูว่าพวกเขาทำงานอย่างไร)

try: # Wrap the whole program into the block that will kill __context__.

    class Catcher(Exception):
        '''This context manager reraises an exception under a different name.'''

        def __init__(self, name):
            super().__init__('Failed to process code in {!r}'.format(name))

        def __enter__(self):
            return self

        def __exit__(self, exc_type, exc_val, exc_tb):
            if exc_type is not None:
                self.__traceback__ = exc_tb
                raise self

    ...


    with Catcher('class definition'):
        class a:
            def spam(self):
                # not really pass, but you get the idea
                pass

            lut = [1,
                   3,
                   17,
                   [12,34],
                   5,
                   _spam]


        assert a().lut[-1] == a.spam

    ...


except Catcher as e:
    e.__context__ = None
    raise

4
TypeError: ยก: หาเรื่อง 3 จะต้องเป็น traceback หรือไม่
เกล็นเมย์นาร์

ขออภัยฉันทำผิดพลาดอย่างใดฉันคิดว่ามันยังยอมรับข้อยกเว้นและรับคุณลักษณะการติดตามกลับของพวกเขาโดยอัตโนมัติ ตามdocs.python.org/3.1/reference/ ......นี่ควรเป็น e .__ traceback__
ilya n

1
@ilyan: Python 2 ไม่มีe.__traceback__แอตทริบิวต์!
Jan Hudec

5

ฉันไม่คิดว่าคุณสามารถทำได้ใน Python 2.x แต่สิ่งที่คล้ายกับฟังก์ชันนี้เป็นส่วนหนึ่งของ Python 3 จากPEP 3134 :

ในการใช้ Python วันนี้ข้อยกเว้นประกอบด้วยสามส่วน ได้แก่ ชนิดค่าและการติดตามย้อนกลับ โมดูล 'sys' แสดงข้อยกเว้นปัจจุบันในสามตัวแปรแบบขนาน, exc_type, exc_value และ exc_traceback, sys.exc_info () ฟังก์ชันส่งคืน tuple ของทั้งสามส่วนและคำสั่ง 'เพิ่ม' มีรูปแบบสามอาร์กิวเมนต์ที่ยอมรับ สามส่วนนี้ การจัดการข้อยกเว้นมักจะต้องผ่านสามสิ่งนี้ในแบบคู่ขนานซึ่งอาจน่าเบื่อและผิดพลาดได้ง่าย นอกจากนี้คำสั่ง 'ยกเว้น' สามารถให้การเข้าถึงค่าเท่านั้นไม่ใช่การย้อนกลับ การเพิ่มแอตทริบิวต์ ' traceback ' ให้กับค่าข้อยกเว้นทำให้สามารถเข้าถึงข้อมูลข้อยกเว้นทั้งหมดได้จากที่เดียว

เปรียบเทียบกับ C #:

ข้อยกเว้นใน C # มีคุณสมบัติ 'InnerException' แบบอ่านอย่างเดียวซึ่งอาจชี้ไปที่ข้อยกเว้นอื่น เอกสารอธิบาย [10] บอกว่า "เมื่อมีการโยนข้อยกเว้น X เนื่องจากผลลัพธ์โดยตรงจากข้อยกเว้นก่อนหน้านี้ Y คุณสมบัติ InnerException ของ X ควรมีการอ้างอิงถึง Y" คุณสมบัตินี้ไม่ได้ถูกตั้งค่าโดย VM โดยอัตโนมัติ ค่อนข้างตัวสร้างข้อยกเว้นทั้งหมดใช้อาร์กิวเมนต์ 'innerException' ที่เป็นตัวเลือกเพื่อตั้งค่าอย่างชัดเจน 'การสาเหตุแอตทริบิวต์' ตอบสนองวัตถุประสงค์เช่นเดียวกับ InnerException แต่ PEP นี้นำเสนอรูปแบบใหม่ของ 'ยก' มากกว่าการขยายการก่อสร้างยกเว้นทั้งหมด C # ยังมีวิธี GetBaseException ที่ข้ามโดยตรงไปยังจุดสิ้นสุดของห่วงโซ่ InnerException

โปรดทราบว่า Java, Ruby และ Perl 5 ไม่รองรับสิ่งประเภทนี้เช่นกัน อ้างอีกครั้ง:

สำหรับภาษาอื่น ๆ ทั้ง Java และ Ruby ต่างละทิ้งข้อยกเว้นเดิมเมื่อมีข้อยกเว้นอื่นเกิดขึ้นในประโยค 'catch' / 'rescue' หรือ 'สุดท้าย' / 'sure' Perl 5 ไม่มีการจัดการข้อยกเว้นที่มีโครงสร้างในตัว สำหรับ Perl 6 หมายเลข RFC 88 [9] เสนอกลไกการยกเว้นที่เก็บข้อยกเว้นที่ถูกผูกมัดไว้ในอาร์เรย์ชื่อ @@ โดยปริยาย


แต่แน่นอนใน Perl5 คุณสามารถพูดว่า "ยอมรับ qq {OH NOES! $ @}" และจะไม่สูญเสียการติดตามสแต็กของข้อยกเว้นอื่น ๆ หรือคุณสามารถใช้ประเภทของคุณเองซึ่งยังคงมีข้อยกเว้น
jrockway

4

เพื่อความเข้ากันได้สูงสุดระหว่าง Python 2 และ 3 คุณสามารถใช้raise_fromในsixไลบรารี https://six.readthedocs.io/#six.raise_from นี่คือตัวอย่างของคุณ (แก้ไขเล็กน้อยเพื่อความชัดเจน):

import six

try:
  ProcessFile(filePath)
except Exception as e:
  six.raise_from(IOError('Failed to process file ' + repr(filePath)), e)

3

คุณสามารถใช้คลาส CausedExceptionของฉันกับข้อยกเว้นโซ่ใน Python 2.x (และแม้แต่ใน Python 3 มันจะมีประโยชน์ในกรณีที่คุณต้องการให้ข้อยกเว้นที่ถูกดักจับมากกว่าหนึ่งข้อเป็นสาเหตุของข้อยกเว้นที่เพิ่งยกใหม่) อาจช่วยคุณได้


2

บางทีคุณสามารถคว้าข้อมูลที่เกี่ยวข้องและส่งต่อได้ ฉันกำลังคิดอะไรบางอย่างเช่น:

import traceback
import sys
import StringIO

class ApplicationError:
    def __init__(self, value, e):
        s = StringIO.StringIO()
        traceback.print_exc(file=s)
        self.value = (value, s.getvalue())

    def __str__(self):
        return repr(self.value)

try:
    try:
        a = 1/0
    except Exception, e:
        raise ApplicationError("Failed to process file", e)
except Exception, e:
    print e

2

สมมติว่า:

  • คุณต้องการวิธีแก้ปัญหาซึ่งใช้งานได้กับ Python 2 (สำหรับ Python 3 ดูraise ... fromโซลูชัน)
  • เพียงต้องการเพิ่มข้อความแสดงข้อผิดพลาดเช่นจัดเตรียมบริบทเพิ่มเติม
  • ต้องการการติดตามสแต็กเต็ม

คุณสามารถใช้วิธีแก้ปัญหาง่ายๆจากเอกสารhttps://docs.python.org/3/tutorial/errors.html#raising-exceptions :

try:
    raise NameError('HiThere')
except NameError:
    print 'An exception flew by!' # print or log, provide details about context
    raise # reraise the original exception, keeping full stack trace

ผลลัพธ์:

An exception flew by!
Traceback (most recent call last):
  File "<stdin>", line 2, in ?
NameError: HiThere

ดูเหมือนว่าชิ้นส่วนที่สำคัญคือคำหลัก 'เพิ่ม' ที่เรียบง่ายซึ่งยืนอยู่คนเดียว ที่จะเพิ่มข้อยกเว้นในบล็อกยกเว้นอีกครั้ง


นี่เป็นโซลูชันที่รองรับ Python 2 และ 3! ขอบคุณ!
Andy Chase

ฉันคิดว่าความคิดคือการยกข้อยกเว้นประเภทอื่น
Tim Ludwinski

2
นี่ไม่ใช่ห่วงโซ่ของข้อยกเว้นแบบซ้อนเพียงแค่อ่านข้อยกเว้นหนึ่งข้อ
Karl Richter

นี่คือทางออกที่ดีที่สุดของ python 2 หากคุณต้องการเพิ่มข้อความแสดงข้อยกเว้นและมีการติดตามสแต็กเต็ม!
geekQ

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