Menambahkan informasi ke pengecualian?

150

Saya ingin mencapai sesuatu seperti ini:

def foo():
   try:
       raise IOError('Stuff ')
   except:
       raise

def bar(arg1):
    try:
       foo()
    except Exception as e:
       e.message = e.message + 'happens at %s' % arg1
       raise

bar('arg1')
Traceback...
  IOError('Stuff Happens at arg1')

Tapi yang saya dapatkan adalah:

Traceback..
  IOError('Stuff')

Ada petunjuk tentang bagaimana mencapai ini? Bagaimana cara melakukannya dengan Python 2 dan 3?

anijhaw
sumber
1
Saat mencari dokumentasi untuk messageatribut Exception, saya menemukan pertanyaan SO ini, BaseException.message tidak digunakan lagi di Python 2.6 , yang tampaknya menunjukkan penggunaannya sekarang tidak disarankan (dan mengapa tidak ada di dokumen).
martineau
Sayangnya, tautan itu sepertinya tidak berfungsi lagi.
Michael Scott Cuthbert
1
@MichaelScottCuthbert inilah alternatif yang baik: itmaybeahack.com/book/python-2.6/html/p02/…
Niels Keurentjes
Berikut adalah penjelasan yang sangat bagus tentang apa status atribut pesan dan hubungannya dengan atribut args dan PEP 352 . Ini dari buku gratis Building Skills in Python oleh Steven F. Lott.
martineau

Jawaban:

125

Saya akan melakukannya seperti ini sehingga mengubah jenisnya foo()tidak perlu juga mengubahnya bar().

def foo():
    try:
        raise IOError('Stuff')
    except:
        raise

def bar(arg1):
    try:
        foo()
    except Exception as e:
        raise type(e)(e.message + ' happens at %s' % arg1)

bar('arg1')

Traceback (most recent call last):
  File "test.py", line 13, in <module>
    bar('arg1')
  File "test.py", line 11, in bar
    raise type(e)(e.message + ' happens at %s' % arg1)
IOError: Stuff happens at arg1

Perbarui 1

Berikut ini sedikit modifikasi yang mempertahankan traceback asli:

...
def bar(arg1):
    try:
        foo()
    except Exception as e:
        import sys
        raise type(e), type(e)(e.message +
                               ' happens at %s' % arg1), sys.exc_info()[2]

bar('arg1')

Traceback (most recent call last):
  File "test.py", line 16, in <module>
    bar('arg1')
  File "test.py", line 11, in bar
    foo()
  File "test.py", line 5, in foo
    raise IOError('Stuff')
IOError: Stuff happens at arg1

Perbarui 2

Untuk Python 3.x, kode update pertama saya adalah sintaksis benar ditambah gagasan memiliki messageatribut pada BaseExceptionitu ditarik dalam perubahan ke PEP 352 pada 2012/05/16 (update pertama saya telah diposting pada 2012-03-12) . Jadi saat ini, dengan Python 3.5.2, Anda perlu melakukan sesuatu di sepanjang baris ini untuk mempertahankan traceback dan bukan hardcode jenis pengecualian dalam fungsi bar(). Perhatikan juga bahwa akan ada baris:

During handling of the above exception, another exception occurred:

di pesan traceback ditampilkan.

# for Python 3.x
...
def bar(arg1):
    try:
        foo()
    except Exception as e:
        import sys
        raise type(e)(str(e) +
                      ' happens at %s' % arg1).with_traceback(sys.exc_info()[2])

bar('arg1')

Perbarui 3

Sebuah commenter bertanya apakah ada cara yang akan bekerja di kedua Python 2 dan 3. Meskipun jawabannya mungkin tampaknya menjadi "Tidak" karena perbedaan sintaks, ada adalah cara sekitar bahwa dengan menggunakan fungsi pembantu seperti reraise()di sixadd pada modul. Jadi, jika Anda lebih suka tidak menggunakan pustaka karena alasan tertentu, di bawah ini adalah versi mandiri yang disederhanakan.

Perhatikan juga, bahwa karena pengecualian dijalankan ulang dalam reraise()fungsi, itu akan muncul di traceback apa pun yang dimunculkan, tetapi hasil akhirnya adalah yang Anda inginkan.

import sys

if sys.version_info.major < 3:  # Python 2?
    # Using exec avoids a SyntaxError in Python 3.
    exec("""def reraise(exc_type, exc_value, exc_traceback=None):
                raise exc_type, exc_value, exc_traceback""")
else:
    def reraise(exc_type, exc_value, exc_traceback=None):
        if exc_value is None:
            exc_value = exc_type()
        if exc_value.__traceback__ is not exc_traceback:
            raise exc_value.with_traceback(exc_traceback)
        raise exc_value

def foo():
    try:
        raise IOError('Stuff')
    except:
        raise

def bar(arg1):
    try:
       foo()
    except Exception as e:
        reraise(type(e), type(e)(str(e) +
                                 ' happens at %s' % arg1), sys.exc_info()[2])

bar('arg1')
martineau.dll
sumber
3
Itu kehilangan backtrace, seperti mengalahkan titik menambahkan informasi ke pengecualian yang ada. Juga, ini tidak bekerja dengan pengecualian dengan ctor yang mengambil> 1 argumen (tipe adalah sesuatu yang tidak dapat Anda kendalikan dari tempat Anda menangkap pengecualian).
Václav Slavík
1
@ Václav: Sangat mudah untuk mencegah kehilangan backtrace - seperti yang ditunjukkan dalam pembaruan yang saya tambahkan. Meskipun ini masih tidak menangani setiap pengecualian yang mungkin, ini berfungsi untuk kasus-kasus yang mirip dengan apa yang ditunjukkan dalam pertanyaan OP.
martineau
1
Ini tidak cukup benar. Jika tipe (e) menimpa __str__, Anda mungkin mendapatkan hasil yang tidak diinginkan. Perhatikan juga bahwa argumen kedua diteruskan ke konstruktor yang diberikan oleh argumen pertama, yang menghasilkan argumen yang agak tidak masuk akal type(e)(type(e)(e.message). Ketiga, e.message tidak digunakan lagi dan mendukung e.args [0].
bukzor
1
jadi, tidak ada cara portabel yang berfungsi di Python 2 dan 3?
Elias Dorneles
1
@martineau Apa tujuan mengimpor di dalam blok kecuali? Apakah ini untuk menghemat memori dengan hanya mengimpor bila perlu?
AllTradesJack
132

Jika Anda datang ke sini mencari solusi untuk Python 3 , manualnya mengatakan:

Saat memunculkan pengecualian baru (daripada menggunakan telanjang raiseuntuk memunculkan kembali pengecualian yang saat ini sedang ditangani), konteks pengecualian implisit dapat dilengkapi dengan penyebab eksplisit dengan menggunakan fromwith raise:

raise new_exc from original_exc

Contoh:

try:
    return [permission() for permission in self.permission_classes]
except TypeError as e:
    raise TypeError("Make sure your view's 'permission_classes' are iterable. "
                    "If you use '()' to generate a set with a single element "
                    "make sure that there is a comma behind the one (element,).") from e

Yang terlihat seperti ini pada akhirnya:

2017-09-06 16:50:14,797 [ERROR] django.request: Internal Server Error: /v1/sendEmail/
Traceback (most recent call last):
File "venv/lib/python3.4/site-packages/rest_framework/views.py", line 275, in get_permissions
    return [permission() for permission in self.permission_classes]
TypeError: 'type' object is not iterable 

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

Traceback (most recent call last):
    # Traceback removed...
TypeError: Make sure your view's Permission_classes are iterable. If 
     you use parens () to generate a set with a single element make 
     sure that there is a (comma,) behind the one element.

Mengubah sebuah pesan yang benar-benar tidak mendeskripsikan TypeErrormenjadi pesan yang bagus dengan petunjuk menuju solusi tanpa mengacaukan Pengecualian asli.

Chris
sumber
16
Ini adalah solusi terbaik, karena hasil pengecualian menunjuk kembali ke penyebab awal, berikan lebih banyak detail.
JT
apakah ada solusi yang bisa kita tambahkan beberapa pesan tetapi tetap tidak memunculkan pengecualian baru? Maksud saya, hanya perluas pesan dari contoh pengecualian.
edcSam
Yaa ~~ itu berhasil, tapi rasanya seperti sesuatu yang seharusnya tidak dilakukan padaku. Pesan disimpan di e.args, tapi itu Tuple, jadi tidak bisa diubah. Jadi pertama-tama salin argske dalam daftar, lalu modifikasi, lalu salin kembali sebagai Tuple:args = list(e.args) args[0] = 'bar' e.args = tuple(args)
Chris
30

Dengan asumsi Anda tidak ingin atau tidak dapat memodifikasi foo (), Anda dapat melakukan ini:

try:
    raise IOError('stuff')
except Exception as e:
    if len(e.args) >= 1:
        e.args = (e.args[0] + ' happens',) + e.args[1:]
    raise

Ini memang satu-satunya solusi di sini yang memecahkan masalah dengan Python 3 tanpa pesan "Selama menangani pengecualian di atas, terjadi pengecualian lain" yang jelek dan membingungkan.

Dalam hal garis re penggalangan harus ditambahkan ke stack trace, menulis raise ebukan raiseakan melakukan trik.

Steve Howard
sumber
tetapi dalam kasus ini jika pengecualian berubah di foo, saya harus mengubah bilah juga kan.?
anijhaw
1
Jika Anda menangkap Exception (diedit di atas), Anda dapat menangkap pengecualian library standar apa pun (serta yang mewarisi dari Exception dan memanggil Exception .__ init__).
Steve Howard
6
agar lebih lengkap / kooperatif, sertakan bagian lain dari tupel asli:e.args = ('mynewstr' + e.args[0],) + e.args[1:]
Dubslow
1
@ nmz787 Ini sebenarnya adalah solusi terbaik untuk Python 3. Apa sebenarnya kesalahan Anda?
Kristen
1
@Dubslow dan martineau Saya memasukkan saran Anda ke dalam pengeditan.
Kristen
9

Sejauh ini saya tidak menyukai semua jawaban yang diberikan. Mereka masih terlalu bertele-tele. Baik dalam kode dan keluaran pesan.

Yang ingin saya miliki hanyalah stacktrace yang menunjuk ke pengecualian sumber, tidak ada pengecualian di antaranya, jadi tidak ada pembuatan pengecualian baru, hanya menaikkan yang asli dengan semua status bingkai tumpukan yang relevan di dalamnya, yang mengarah ke sana.

Steve Howard memberikan jawaban yang bagus yang ingin saya perpanjang, tidak, kurangi ... hanya menjadi python 3.

except Exception as e:
    e.args = ("Some failure state", *e.args)
    raise

Satu-satunya hal baru adalah parameter expansion / unpacking yang membuatnya kecil dan cukup mudah untuk saya gunakan.

Cobalah:

foo = None

try:
    try:
        state = "bar"
        foo.append(state)

    except Exception as e:
        e.args = ("Appending '"+state+"' failed", *e.args)
        raise

    print(foo[0]) # would raise too

except Exception as e:
    e.args = ("print(foo) failed: " + str(foo), *e.args)
    raise

Ini akan memberi Anda:

Traceback (most recent call last):
  File "test.py", line 6, in <module>
    foo.append(state)
AttributeError: ('print(foo) failed: None', "Appending 'bar' failed", "'NoneType' object has no attribute 'append'")

Sebuah cetakan cantik yang sederhana bisa seperti itu

print("\n".join( "-"*i+" "+j for i,j in enumerate(e.args)))
6EQUJ5
sumber
Ini memiliki sisi negatif bahwa bingkai luar bukan bagian dari jejak tumpukan.
moi
5

Salah satu pendekatan praktis yang saya gunakan adalah menggunakan atribut class sebagai penyimpanan untuk detail, karena atribut class dapat diakses dari objek kelas dan instance kelas:

class CustomError(Exception):
    def __init__(self, details: Dict):
        self.details = details

Kemudian di kode Anda:

raise CustomError({'data': 5})

Dan saat menemukan kesalahan:

except CustomError as e:
    # Do whatever you want with the exception instance
    print(e.details)
Kee
sumber
Tidak terlalu berguna karena OP meminta rincian dicetak sebagai bagian dari jejak tumpukan ketika pengecualian asli dilempar dan tidak tertangkap.
cowbert
Saya pikir solusinya bagus. Namun uraian tersebut tidak benar. Atribut kelas disalin ke instance saat Anda membuat instance. Jadi, ketika Anda memodifikasi atribut "details", dari instance tersebut, atribut class tetap akan menjadi None. Bagaimanapun kami ingin perilaku ini di sini.
Adam Wallner
3

Saya akan memberikan potongan kode yang sering saya gunakan setiap kali saya ingin menambahkan info tambahan ke pengecualian. Saya bekerja dengan Python 2.7 dan 3.6.

import sys
import traceback

try:
    a = 1
    b = 1j

    # The line below raises an exception because
    # we cannot compare int to complex.
    m = max(a, b)  

except Exception as ex:
    # I create my  informational message for debugging:
    msg = "a=%r, b=%r" % (a, b)

    # Gather the information from the original exception:
    exc_type, exc_value, exc_traceback = sys.exc_info()

    # Format the original exception for a nice printout:
    traceback_string = ''.join(traceback.format_exception(
        exc_type, exc_value, exc_traceback))

    # Re-raise a new exception of the same class as the original one, 
    # using my custom message and the original traceback:
    raise type(ex)("%s\n\nORIGINAL TRACEBACK:\n\n%s\n" % (msg, traceback_string))

Kode di atas menghasilkan keluaran sebagai berikut:

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-6-09b74752c60d> in <module>()
     14     raise type(ex)(
     15         "%s\n\nORIGINAL TRACEBACK:\n\n%s\n" %
---> 16         (msg, traceback_string))

TypeError: a=1, b=1j

ORIGINAL TRACEBACK:

Traceback (most recent call last):
  File "<ipython-input-6-09b74752c60d>", line 7, in <module>
    m = max(a, b)  # Cannot compare int to complex
TypeError: no ordering relation is defined for complex numbers


Saya tahu ini sedikit menyimpang dari contoh yang diberikan dalam pertanyaan, tetapi saya berharap seseorang menganggapnya berguna.

Pedro M Duarte
sumber
2

Tidak seperti jawaban sebelumnya, ini berfungsi saat menghadapi pengecualian dengan sangat buruk __str__. Itu memang memodifikasi jenisnya, untuk memfaktorkan __str__implementasi yang tidak membantu .

Saya masih ingin menemukan peningkatan tambahan yang tidak mengubah jenisnya.

from contextlib import contextmanager
@contextmanager
def helpful_info():
    try:
        yield
    except Exception as e:
        class CloneException(Exception): pass
        CloneException.__name__ = type(e).__name__
        CloneException.__module___ = type(e).__module__
        helpful_message = '%s\n\nhelpful info!' % e
        import sys
        raise CloneException, helpful_message, sys.exc_traceback


class BadException(Exception):
    def __str__(self):
        return 'wat.'

with helpful_info():
    raise BadException('fooooo')

Traceback asli dan jenis (nama) dipertahankan.

Traceback (most recent call last):
  File "re_raise.py", line 20, in <module>
    raise BadException('fooooo')
  File "/usr/lib64/python2.6/contextlib.py", line 34, in __exit__
    self.gen.throw(type, value, traceback)
  File "re_raise.py", line 5, in helpful_info
    yield
  File "re_raise.py", line 20, in <module>
    raise BadException('fooooo')
__main__.BadException: wat.

helpful info!
bukzor.dll
sumber
1

Anda dapat menentukan pengecualian Anda sendiri yang mewarisi dari yang lain dan membuat konstruktornya sendiri untuk menetapkan nilai.

Sebagai contoh:

class MyError(Exception):
   def __init__(self, value):
     self.value = value
     Exception.__init__(self)

   def __str__(self):
     return repr(self.value)
Alexander Kiselev
sumber
2
Tidak membahas perlu mengubah / menambahkan sesuatu ke messagepengecualian asli (tapi bisa diperbaiki, saya pikir).
martineau
-6

Mungkin

except Exception as e:
    raise IOError(e.message + 'happens at %s'%arg1)
Malvolio
sumber