ยกข้อยกเว้นด้วยประเภทและข้อความที่แตกต่างกันรักษาข้อมูลที่มีอยู่


139

ฉันกำลังเขียนโมดูลและต้องการมีลำดับชั้นข้อยกเว้นแบบรวมสำหรับข้อยกเว้นที่สามารถยกระดับได้ (เช่นการสืบทอดจากFooErrorคลาสนามธรรมสำหรับfooข้อยกเว้นเฉพาะทั้งหมดของโมดูล) สิ่งนี้ช่วยให้ผู้ใช้โมดูลสามารถตรวจจับข้อยกเว้นเฉพาะเหล่านั้นและจัดการพวกมันอย่างชัดเจนหากจำเป็น แต่มีข้อยกเว้นมากมายที่ยกมาจากโมดูลเนื่องจากข้อยกเว้นอื่น ๆ เช่นความล้มเหลวในบางงานเนื่องจาก OSError บนไฟล์

สิ่งที่ฉันต้องการคือ"ห่อ" ข้อยกเว้นที่จับได้ว่ามันมีประเภทและข้อความที่แตกต่างกันดังนั้นข้อมูลจะมีอยู่ในลำดับชั้นการเผยแพร่ต่อไปโดยสิ่งใดก็ตามที่จับข้อยกเว้น แต่ฉันไม่ต้องการสูญเสียชนิดข้อความและการติดตามสแต็กที่มีอยู่เดิม นั่นคือข้อมูลที่มีประโยชน์ทั้งหมดสำหรับคนที่พยายามแก้ไขปัญหา ตัวจัดการข้อยกเว้นระดับบนสุดไม่ดีนักเนื่องจากฉันพยายามตกแต่งข้อยกเว้นก่อนที่มันจะเพิ่มระดับสแต็คการเผยแพร่ต่อไปและตัวจัดการระดับบนสุดจะสายเกินไป

นี่เป็นส่วนหนึ่งที่ได้รับการแก้ไขโดยการรับfooประเภทข้อยกเว้นเฉพาะของโมดูลจากประเภทที่มีอยู่ (เช่นclass FooPermissionError(OSError, FooError)) แต่นั่นไม่ได้ทำให้ง่ายต่อการห่ออินสแตนซ์ข้อยกเว้นที่มีอยู่ในรูปแบบใหม่หรือแก้ไขข้อความ

PEP 3134 “ข้อยกเว้นผูกมัดและ tracebacks ฝังตัว” กล่าวถึงการเปลี่ยนแปลงที่ยอมรับในหลาม 3.0 สำหรับ“ผูกมัด” วัตถุข้อยกเว้นที่จะแสดงให้เห็นว่าข้อยกเว้นใหม่ถูกยกขึ้นในระหว่างการจัดการของข้อยกเว้นที่มีอยู่

สิ่งที่ฉันพยายามจะทำคือเกี่ยวข้อง: ฉันต้องการมันยังทำงานในรุ่น Python ก่อนหน้านี้และฉันไม่ต้องการสำหรับการผูกมัด แต่สำหรับ polymorphism เท่านั้น วิธีที่ถูกต้องในการทำเช่นนี้คืออะไร?


ข้อยกเว้นนี้เป็น polymorphic โดยสมบูรณ์ - เป็นคลาสย่อยทั้งหมดของข้อยกเว้น คุณพยายามจะทำอะไร? "ข้อความที่แตกต่าง" ค่อนข้างเล็กน้อยกับตัวจัดการข้อยกเว้นระดับบนสุด ทำไมคุณถึงเปลี่ยนชั้นเรียน?
S.Lott

ตามที่อธิบายไว้ในคำถาม (ตอนนี้ขอขอบคุณสำหรับความคิดเห็นของคุณ): ฉันพยายามตกแต่งข้อยกเว้นที่ฉันพบเพื่อให้สามารถเผยแพร่ต่อไปด้วยข้อมูลเพิ่มเติม แต่ไม่สูญเสียใด ๆ ตัวจัดการระดับบนสุดสายเกินไป
bignose

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


สำหรับหลาม-2 ที่ผมทำสิ่งที่คล้ายกับ @DevinJeanpierre แต่ฉันเพียงแค่ต่อท้ายข้อความสตริงใหม่except Exception as e-> raise type(e), type(e)(e.message + custom_message), sys.exc_info()[2]-> การแก้ปัญหานี้คือจากคำถาม SO อีก มันไม่สวย แต่ใช้งานได้
เทรเวอร์บอยด์สมิ ธ

คำตอบ:


197

Python 3นำเสนอการผูกมัดข้อยกเว้น (ตามที่อธิบายไว้ในPEP 3134 ) สิ่งนี้อนุญาตให้ยกข้อยกเว้นเพื่ออ้างถึงข้อยกเว้นที่มีอยู่ว่าเป็น "สาเหตุ":

try:
    frobnicate()
except KeyError as exc:
    raise ValueError("Bad grape") from exc

ข้อยกเว้นที่จับได้จึงกลายเป็นส่วนหนึ่งของ (เป็น“ สาเหตุของ”) ข้อยกเว้นใหม่และสามารถใช้ได้กับรหัสใดก็ตามที่จับข้อยกเว้นใหม่ได้

ด้วยการใช้คุณสมบัตินี้จะมีการ__cause__ตั้งค่าแอตทริบิวต์ ตัวจัดการข้อยกเว้นในตัวยังรู้วิธีการรายงาน“ สาเหตุ” และ“ บริบท” ของข้อยกเว้นพร้อมกับการสืบค้นกลับ


ในPython 2ดูเหมือนว่ากรณีการใช้งานนี้ไม่มีคำตอบที่ดี (ตามที่อธิบายโดยIan BickingและNed Batchelder ) คนเกียจคร้าน


4
Ian Bicking ไม่ได้อธิบายวิธีการแก้ปัญหาของฉัน? ฉันเสียใจที่ฉันได้รับคำตอบที่หนักหนาสาหัส แต่มันแปลกที่คนนี้ยอมรับ
Devin Jeanpierre

1
@bignose คุณมีจุดของฉันไม่เพียง แต่จากการที่ถูกต้อง แต่สำหรับการใช้งานของ "frobnicate" :)
เดวิดเมตร

5
ข้อยกเว้นการผูกมัดเป็นพฤติกรรมเริ่มต้นจริง ๆ แล้วในความเป็นจริงมันตรงกันข้ามกับปัญหาระงับข้อยกเว้นแรกที่ต้องใช้งานดู PEP 409 python.org/dev/peps/pep-0409
Chris_Rands

1
คุณจะทำให้สำเร็จใน python 2 ได้อย่างไร
selotape

1
ดูเหมือนว่าจะทำงานได้ดี (python 2.7)try: return 2 / 0 except ZeroDivisionError as e: raise ValueError(e)
alex

37

คุณสามารถใช้ sys.exc_info () เพื่อรับการติดตามย้อนกลับและเพิ่มข้อยกเว้นใหม่ของคุณด้วยการติดตามย้อนกลับดังกล่าว (ตามที่ PEP กล่าวถึง) หากคุณต้องการรักษาชนิดและข้อความเก่าไว้คุณสามารถทำได้ยกเว้น แต่จะมีประโยชน์เฉพาะเมื่อมีสิ่งใดก็ตามที่จับข้อยกเว้นของคุณได้

ตัวอย่างเช่น

import sys

def failure():
    try: 1/0
    except ZeroDivisionError, e:
        type, value, traceback = sys.exc_info()
        raise ValueError, ("You did something wrong!", type, value), traceback

แน่นอนว่ามันไม่มีประโยชน์จริงๆ ถ้าเป็นเช่นนั้นเราไม่ต้องการ PEP นั้น ฉันไม่แนะนำให้ทำ


Devin คุณเก็บการอ้างอิงถึงการย้อนกลับที่นั่นคุณไม่ควรลบการอ้างอิงนั้นอย่างชัดเจนหรือ
Arafangion

2
ฉันไม่ได้เก็บอะไรเลยฉันเหลือร่องรอยการติดตามไว้เป็นตัวแปรในตัวเครื่องซึ่งน่าจะอยู่นอกขอบเขต ใช่เป็นไปได้ว่ามันไม่ได้ แต่ถ้าคุณเพิ่มข้อยกเว้นเช่นนั้นในขอบเขตทั่วโลกมากกว่าภายในฟังก์ชั่นคุณจะมีปัญหาที่ใหญ่กว่า หากการร้องเรียนของคุณเป็นเพียงการดำเนินการในขอบเขตทั่วโลกการแก้ปัญหาที่เหมาะสมคือไม่ต้องเพิ่มสำเร็จรูปสำเร็จรูปที่ไม่เกี่ยวข้องซึ่งจะต้องอธิบายและไม่เกี่ยวข้องกับการใช้งาน 99% แต่เพื่อเขียนโซลูชันใหม่เพื่อไม่ให้สิ่งนั้นเกิดขึ้น มีความจำเป็นในขณะที่ทำให้ดูเหมือนว่าไม่มีอะไรแตกต่าง - ที่ฉันได้ทำตอนนี้
Devin Jeanpierre

4
Arafangion อาจอ้างถึงคำเตือนในเอกสาร Python สำหรับsys.exc_info() @Devin มันบอกว่า "การกำหนดค่าส่งคืน traceback ให้กับตัวแปรโลคัลในฟังก์ชันที่จัดการข้อยกเว้นจะทำให้เกิดการอ้างอิงแบบวงกลม" อย่างไรก็ตามข้อความต่อไปนี้บอกว่าตั้งแต่ Python 2.2 สามารถล้างวัฏจักรได้ แต่มีประสิทธิภาพมากกว่าที่จะหลีกเลี่ยง
Don Kirkby

5
รายละเอียดเพิ่มเติมเกี่ยวกับวิธีการที่แตกต่างกันในการเพิ่มข้อยกเว้นใน Python จาก pythonistas ที่รู้แจ้งอีกสองคน: Ian BickingและNed Batchelder
Rodrigue

11

คุณสามารถสร้างประเภทข้อยกเว้นของคุณเองที่ขยายข้อยกเว้นใด ๆ ที่คุณจับได้

class NewException(CaughtException):
    def __init__(self, caught):
        self.caught = caught

try:
    ...
except CaughtException as e:
    ...
    raise NewException(e)

แต่ส่วนมากของเวลาที่ฉันคิดว่ามันจะง่ายต่อการจับข้อยกเว้นจัดการกับมันและทั้งraiseข้อยกเว้นเดิม (และรักษา traceback) raise NewException()หรือ หากฉันกำลังเรียกรหัสของคุณและฉันได้รับหนึ่งในข้อยกเว้นที่กำหนดเองของคุณฉันคาดหวังว่ารหัสของคุณจะจัดการกับข้อยกเว้นใดก็ตามที่คุณต้องตรวจจับ ดังนั้นฉันไม่จำเป็นต้องเข้าถึงด้วยตนเอง

แก้ไข: ฉันพบการวิเคราะห์วิธีนี้เพื่อทำให้เกิดข้อยกเว้นของคุณเองและรักษาข้อยกเว้นดั้งเดิมไว้ ไม่มีทางออกที่ดี


1
กรณีการใช้งานที่ฉันอธิบายไม่ได้มีไว้สำหรับจัดการข้อยกเว้น; มันเกี่ยวกับการไม่จัดการมันโดยเฉพาะ แต่เพิ่มข้อมูลพิเศษบางอย่าง (คลาสเพิ่มเติมและข้อความใหม่) เพื่อให้สามารถจัดการกับสแต็คการโทรเพิ่มเติมได้
bignose

2

ฉันยังพบว่าหลายครั้งที่ฉันต้องการ "การตัด" เพื่อเพิ่มข้อผิดพลาด

สิ่งนี้รวมอยู่ในขอบเขตของฟังก์ชันและบางครั้งก็มีเพียงบางบรรทัดในฟังก์ชัน

สร้างเสื้อคลุมที่จะใช้decoratorและcontext manager:


การดำเนินงาน

import inspect
from contextlib import contextmanager, ContextDecorator
import functools    

class wrap_exceptions(ContextDecorator):
    def __init__(self, wrapper_exc, *wrapped_exc):
        self.wrapper_exc = wrapper_exc
        self.wrapped_exc = wrapped_exc

    def __enter__(self):
        pass

    def __exit__(self, exc_type, exc_val, exc_tb):
        if not exc_type:
            return
        try:
            raise exc_val
        except self.wrapped_exc:
            raise self.wrapper_exc from exc_val

    def __gen_wrapper(self, f, *args, **kwargs):
        with self:
            for res in f(*args, **kwargs):
                yield res

    def __call__(self, f):
        @functools.wraps(f)
        def wrapper(*args, **kw):
            with self:
                if inspect.isgeneratorfunction(f):
                    return self.__gen_wrapper(f, *args, **kw)
                else:
                    return f(*args, **kw)
        return wrapper

ตัวอย่างการใช้งาน

มัณฑนากร

@wrap_exceptions(MyError, IndexError)
def do():
   pass

เมื่อโทรdoวิธีไม่ต้องกังวลIndexErrorเพียงแค่MyError

try:
   do()
except MyError as my_err:
   pass # handle error 

ผู้จัดการบริบท

def do2():
   print('do2')
   with wrap_exceptions(MyError, IndexError):
       do()

ภายในdo2ในcontext managerถ้าIndexErrorจะเพิ่มขึ้นก็จะถูกห่อและเติบโตMyError


1
โปรดอธิบายสิ่งที่ "ห่อ" จะทำกับข้อยกเว้นเดิม จุดประสงค์ของรหัสของคุณคืออะไรและพฤติกรรมแบบใดที่เปิดใช้งาน
alexis

@alexis - เพิ่มตัวอย่างหวังว่าจะช่วยได้
Aaron_ab

-2

ทางออกที่ตรงตามความต้องการของคุณมากที่สุดคือ:

try:
     upload(file_id)
except Exception as upload_error:
     error_msg = "Your upload failed! File: " + file_id
     raise RuntimeError(error_msg, upload_error)

ด้วยวิธีนี้คุณสามารถพิมพ์ข้อความของคุณในภายหลังและข้อผิดพลาดเฉพาะที่เกิดขึ้นจากฟังก์ชั่นอัพโหลด


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