Kembalikan pengecualian dengan jenis dan pesan yang berbeda, menjaga informasi yang ada


139

Saya sedang menulis modul dan ingin memiliki hierarki eksepsi terpadu untuk pengecualian yang dapat dimunculkan (misalnya mewarisi dari FooErrorkelas abstrak untuk semua foopengecualian spesifik modul). Ini memungkinkan pengguna modul untuk menangkap pengecualian khusus tersebut dan menanganinya dengan jelas, jika perlu. Tetapi banyak pengecualian yang diangkat dari modul dinaikkan karena beberapa pengecualian lain; misalnya gagal pada beberapa tugas karena OSError pada file.

Yang saya butuhkan adalah untuk "membungkus" pengecualian yang ditangkap sehingga memiliki jenis dan pesan yang berbeda , sehingga informasi tersedia lebih lanjut atas hierarki propagasi oleh apa pun yang menangkap pengecualian. Tapi saya tidak ingin kehilangan jenis, pesan, dan jejak stack yang ada; itu semua informasi yang berguna untuk seseorang yang mencoba men-debug masalah. Penangan pengecualian tingkat atas tidak baik, karena saya mencoba untuk menghias pengecualian sebelum membuat jalannya lebih jauh ke tumpukan propagasi, dan penangan tingkat atas sudah terlambat.

Ini sebagian diselesaikan dengan menurunkan foojenis pengecualian spesifik modul saya dari jenis yang ada (misalnya class FooPermissionError(OSError, FooError)), tetapi itu tidak membuatnya lebih mudah untuk membungkus contoh pengecualian yang ada dalam jenis baru, atau memodifikasi pesan.

PEP 3134 dari Python 3134 " Chains Pengecualian dan Embedded Tracebacks" membahas perubahan yang diterima dalam Python 3.0 untuk objek pengecualian "chaining", untuk menunjukkan bahwa pengecualian baru dimunculkan selama penanganan pengecualian yang ada.

Apa yang saya coba lakukan terkait: Saya membutuhkannya juga bekerja di versi Python sebelumnya, dan saya membutuhkannya bukan untuk chaining, tetapi hanya untuk polimorfisme. Apa cara yang tepat untuk melakukan ini?


Pengecualian sudah sepenuhnya polimorfik - mereka semua adalah subkelas Pengecualian. Apa yang sedang Anda coba lakukan? "Pesan berbeda" cukup sepele dengan pengendali pengecualian tingkat atas. Mengapa Anda mengubah kelas?
S.Lott

Seperti yang dijelaskan dalam pertanyaan (sekarang, terima kasih atas komentar Anda): Saya mencoba untuk menghias pengecualian yang saya tangkap, sehingga dapat menyebar lebih lanjut dengan informasi lebih lanjut tetapi tidak kehilangan apapun. Handler tingkat atas sudah terlambat.
bignose

Silakan lihat di kelas CausedException saya yang dapat melakukan apa yang Anda inginkan dengan Python 2.x. Juga di Python 3 dapat digunakan jika Anda ingin memberikan lebih dari satu pengecualian asli sebagai penyebab pengecualian Anda. Mungkin itu sesuai dengan kebutuhan Anda.
Alfe


Untuk python-2 saya melakukan sesuatu yang mirip dengan @DevinJeanpierre tapi saya hanya menambahkan pesan string baru: except Exception as e-> raise type(e), type(e)(e.message + custom_message), sys.exc_info()[2]-> solusi ini dari pertanyaan SO lainnya . Ini tidak cantik tapi fungsional.
Trevor Boyd Smith

Jawaban:


197

Python 3 memperkenalkan chaining pengecualian (seperti yang dijelaskan dalam PEP 3134 ). Ini memungkinkan, saat mengajukan pengecualian, mengutip pengecualian yang ada sebagai "penyebab":

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

Pengecualian yang tertangkap dengan demikian menjadi bagian dari (adalah "penyebab") pengecualian baru, dan tersedia untuk kode apa pun yang menangkap pengecualian baru.

Dengan menggunakan fitur ini, __cause__atribut ditetapkan. Penangan pengecualian bawaan juga tahu bagaimana melaporkan "penyebab" dan "konteks" pengecualian bersama dengan traceback.


Dalam Python 2 , tampaknya use case ini tidak memiliki jawaban yang baik (seperti yang dijelaskan oleh Ian Bicking dan Ned Batchelder ). Kekecewaan.


4
Bukankah Ian Bicking menggambarkan solusi saya? Saya menyesal telah memberikan jawaban yang luar biasa, tetapi aneh bahwa yang ini diterima.
Devin Jeanpierre

1
@ Bignose Anda mendapatkan poin saya bukan hanya dari menjadi benar, tetapi untuk penggunaan "frobnicate" :)
David M.

5
Chaining pengecualian sebenarnya adalah perilaku default sekarang, pada kenyataannya itu adalah kebalikan dari masalah, menekan pengecualian pertama yang membutuhkan pekerjaan, lihat PEP 409 python.org/dev/peps/pep-0409
Chris_Rands

1
Bagaimana Anda melakukannya dengan python 2?
selotape

1
Tampaknya berfungsi dengan baik (python 2.7)try: return 2 / 0 except ZeroDivisionError as e: raise ValueError(e)
alex

37

Anda dapat menggunakan sys.exc_info () untuk mendapatkan traceback, dan meningkatkan pengecualian baru Anda dengan traceback tersebut (seperti yang disebutkan oleh PEP). Jika Anda ingin mempertahankan jenis dan pesan yang lama, Anda dapat melakukannya dengan pengecualian, tetapi itu hanya berguna jika apa pun yang menangkap pengecualian Anda mencarinya.

Sebagai contoh

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

Tentu saja, ini benar-benar tidak berguna. Jika ya, kita tidak membutuhkan PEP itu. Saya tidak akan merekomendasikan melakukannya.


Devin, Anda menyimpan referensi ke traceback di sana, tidakkah Anda harus secara eksplisit menghapus referensi itu?
Arafangion

2
Saya tidak menyimpan apa pun, saya meninggalkan traceback sebagai variabel lokal yang mungkin berada di luar jangkauan. Ya, bisa dibayangkan tidak, tetapi jika Anda mengajukan pengecualian seperti itu dalam lingkup global alih-alih dalam fungsi, Anda memiliki masalah yang lebih besar. Jika keluhan Anda hanya dapat dieksekusi dalam lingkup global, solusi yang tepat bukanlah menambahkan boilerplate yang tidak relevan yang harus dijelaskan dan tidak relevan untuk 99% penggunaan, tetapi untuk menulis ulang solusi sehingga tidak ada hal seperti itu. diperlukan sementara membuatnya seolah-olah tidak ada yang berbeda - seperti yang saya lakukan sekarang.
Devin Jeanpierre

4
Arafangion mungkin merujuk pada peringatan dalam dokumentasi Python untuksys.exc_info() , @Devin. Dikatakan, "Menetapkan nilai kembali traceback ke variabel lokal dalam fungsi yang menangani pengecualian akan menyebabkan referensi melingkar." Namun, catatan berikut mengatakan bahwa sejak Python 2.2, siklus dapat dibersihkan, tetapi lebih efisien untuk menghindarinya.
Don Kirkby

5
Detail lebih lanjut tentang berbagai cara untuk meningkatkan kembali pengecualian di Python dari dua pythonista yang tercerahkan: Ian Bicking dan Ned Batchelder
Rodrigue

11

Anda bisa membuat jenis pengecualian Anda sendiri yang meluas ke mana pun pengecualian yang Anda tangkap.

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

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

Tetapi sebagian besar waktu, saya pikir akan lebih mudah untuk menangkap pengecualian, menanganinya, dan apakah raisepengecualian asli (dan menjaga traceback) atau raise NewException(). Jika saya memanggil kode Anda, dan saya menerima salah satu pengecualian khusus Anda, saya berharap bahwa kode Anda sudah menangani pengecualian apa pun yang harus Anda tangkap. Jadi saya tidak perlu mengaksesnya sendiri.

Sunting: Saya menemukan analisis ini tentang cara melempar pengecualian Anda sendiri dan menyimpan pengecualian asli. Tidak ada solusi cantik.


1
Kasus penggunaan yang saya jelaskan bukan untuk menangani pengecualian; ini khusus tentang tidak menanganinya, tetapi menambahkan beberapa informasi tambahan (kelas tambahan dan pesan baru) sehingga dapat ditangani lebih lanjut di tumpukan panggilan.
bignose

2

Saya juga menemukan bahwa berkali-kali saya perlu beberapa "pembungkus" untuk kesalahan yang diajukan.

Ini termasuk dalam lingkup fungsi dan terkadang hanya membungkus beberapa baris di dalam fungsi.

Dibuat pembungkus untuk digunakan decoratordan context manager:


Penerapan

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

Contoh penggunaan

penghias

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

saat memanggil dometode, jangan khawatir IndexError, cukupMyError

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

manajer konteks

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

di dalam do2, di context manager, jika IndexErrordinaikkan, itu akan dibungkus dan dinaikkanMyError


1
Tolong jelaskan apa yang akan "dibungkus" dengan pengecualian asli. Apa tujuan kode Anda, dan perilaku apa yang dimungkinkannya?
Alex

@alexis - menambahkan beberapa contoh, semoga membantu
Aaron_ab

-2

Solusi paling tegas untuk kebutuhan Anda adalah:

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

Dengan cara ini Anda nantinya dapat mencetak pesan Anda dan kesalahan spesifik dilemparkan oleh fungsi unggah


1
Itu menangkap dan kemudian membuang objek pengecualian, jadi tidak, itu tidak memenuhi kebutuhan pertanyaan. Pertanyaan itu menanyakan bagaimana cara menyimpan pengecualian yang ada dan mengizinkan pengecualian yang sama, dengan semua informasi bermanfaat yang dikandungnya, untuk terus menyebar ke atas tumpukan.
bignose
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.