Bagaimana cara memunculkan kembali pengecualian di blok percobaan / pengecualian bersarang?

109

Saya tahu bahwa jika saya ingin memunculkan kembali pengecualian, saya menggunakan sederhana raisetanpa argumen di exceptblok masing-masing . Tapi diberi ekspresi bersarang seperti

try:
    something()
except SomeError as e:
    try:
        plan_B()
    except AlsoFailsError:
        raise e  # I'd like to raise the SomeError as if plan_B()
                 # didn't raise the AlsoFailsError

bagaimana cara menaikkan kembali SomeErrortanpa merusak jejak tumpukan? raisesendiri akan dalam hal ini mengangkat kembali yang lebih baru AlsoFailsError. Atau bagaimana saya dapat mengubah kode saya untuk menghindari masalah ini?

Tobias Kienzler
sumber
2
Sudahkah Anda mencoba memasukkan plan_Bfungsi lain yang mengembalikan Truekesuksesan, dan Falsepengecualian? Maka exceptblok luar mungkin sajaif not try_plan_B(): raise
Drew McGowen
@DrewcGowen Sayangnya kasus yang lebih realistis adalah bahwa ini ada di dalam fungsi yang menerima objek sewenang-wenang argdan saya akan mencoba menelepon arg.plan_B()yang mungkin menimbulkan AttributeErrorkarena argtidak menyediakan rencana B
Tobias Kienzler
@Paco Terima kasih, saya akan (Meskipun jawaban sudah menunjukkan cara yang lebih sederhana)
Tobias Kienzler
@DrewMcGowen Saya menulis jawaban berdasarkan komentar Anda , yang terlihat kurang pythonic daripada jawaban pengguna4815162342 . Tapi itu karena keinginan saya untuk juga memiliki nilai pengembalian dan memungkinkan plan_Buntuk meningkatkan pengecualian
Tobias Kienzler

Jawaban:

131

Pada Python 3, traceback disimpan dalam pengecualian, jadi simple raise eakan melakukan (sebagian besar) hal yang benar:

try:
    something()
except SomeError as e:
    try:
        plan_B()
    except AlsoFailsError:
        raise e  # or raise e from None - see below

Traceback yang dihasilkan akan menyertakan pemberitahuan tambahan yang SomeErrorterjadi saat menangani AlsoFailsError(karena raise eberada di dalam except AlsoFailsError). Ini menyesatkan karena yang sebenarnya terjadi adalah sebaliknya - kami bertemu AlsoFailsError, dan menanganinya, sambil mencoba memulihkan SomeError. Untuk mendapatkan traceback yang tidak termasuk AlsoFailsError, ganti raise edengan raise e from None.

Di Python 2 Anda akan menyimpan jenis pengecualian, nilai, dan pelacakan balik di variabel lokal dan menggunakan bentuk tiga argumen dariraise :

try:
    something()
except SomeError:
    t, v, tb = sys.exc_info()
    try:
        plan_B()
    except AlsoFailsError:
        raise t, v, tb
pengguna4815162342
sumber
Sempurna, itulah yang baru saja saya temukan di sini , terima kasih! Meskipun ada saran raise self.exc_info[1], None, self.exc_info[2]setelah self.exc_info = sys.exc_info()- menempatkan [1]ke posisi pertama karena beberapa alasan
Tobias Kienzler
3
@TobiasKienzler raise t, None, tbakan kehilangan nilai pengecualian dan akan memaksa raiseuntuk membuat instance ulang dari jenisnya, memberi Anda nilai pengecualian yang kurang spesifik (atau salah). Misalnya, jika pengecualian yang dimunculkan adalah KeyError("some-key"), itu hanya akan memunculkan kembali KeyError()dan menghilangkan kunci yang benar-benar hilang dari traceback.
pengguna4815162342
3
@TobiasKienzler Seharusnya masih mungkin untuk mengekspresikannya dengan Python 3 sebagai raise v.with_traceback(tb). (Komentar Anda bahkan mengatakan sebanyak itu, kecuali itu mengusulkan untuk mengembalikan nilai.)
pengguna4815162342
2
Juga, peringatan merah untuk tidak menyimpan sys.exc_info()dalam variabel lokal masuk akal sebelum Python 2.0 (dirilis 13 tahun yang lalu), tetapi berbatasan dengan konyol hari ini. Python modern hampir tidak berguna tanpa cycle collector, karena setiap pustaka Python non-sepele membuat siklus tanpa jeda dan bergantung pada pembersihan yang benar.
pengguna4815162342
1
@ user4815162342 Anda dapat menghentikan "terjadi kesalahan lain" kesalahan bersarang dengan menulis "naikkan e dari Tidak ada".
Matthias Urlichs
19

Bahkan jika solusi yang diterima benar, ada baiknya untuk menunjuk ke perpustakaan Six yang memiliki solusi Python 2 + 3, menggunakan six.reraise.

enam. reraise ( exc_type , exc_value , exc_traceback = None)

Kembalikan pengecualian, mungkin dengan pelacakan balik yang berbeda. [...]

Jadi, Anda bisa menulis:

import six


try:
    something()
except SomeError:
    t, v, tb = sys.exc_info()
    try:
        plan_B()
    except AlsoFailsError:
        six.reraise(t, v, tb)
Laurent LAPORTE
sumber
1
Poin bagus - berbicara tentang Enam, Anda juga dapat menggunakan six.raise_fromjika Anda ingin memasukkan informasi yang plan_B()juga gagal.
Tobias Kienzler
1
@TobiasKienzler: Saya pikir ini adalah penggunaan yang berbeda: dengan six.raise_fromAnda membuat pengecualian baru yang ditautkan ke yang sebelumnya, Anda tidak perlu menaikkan kembali , jadi penelusuran kembali berbeda.
Laurent LAPORTE
1
Maksud saya persis - jika Anda reraisemendapatkan kesan hanya something()melemparkan SomeError, jika raise_fromAnda juga tahu bahwa ini menyebabkan plan_B()untuk dieksekusi tetapi membuang AlsoFailsError. Jadi itu tergantung pada usecase. Saya pikir raise_fromakan membuat debugging lebih mudah
Tobias Kienzler
9

Sesuai saran Drew McGowen , tetapi menangani kasus umum (di mana nilai kembali sada), berikut adalah alternatif jawaban pengguna4815162342 :

try:
    s = something()
except SomeError as e:
    def wrapped_plan_B():
        try:
            return False, plan_B()
        except:
            return True, None
    failed, s = wrapped_plan_B()
    if failed:
        raise
Tobias Kienzler
sumber
1
Hal yang menyenangkan tentang pendekatan ini adalah ia bekerja tidak berubah di Python 2 dan 3.
user4815162342
2
@ user4815162342 Poin yang bagus :) Meskipun sementara itu di Python3 saya akan mempertimbangkan raise from, jadi pelacakan tumpukan juga akan membiarkan saya melihat rencana B gagal. Yang bisa ditiru dengan Python 2 .
Tobias Kienzler
5

Python 3.5+ tetap melampirkan informasi pelacakan ke kesalahan, jadi tidak perlu lagi menyimpannya secara terpisah.

>>> def f():
...   try:
...     raise SyntaxError
...   except Exception as e:
...     err = e
...     try:
...       raise AttributeError
...     except Exception as e1:
...       raise err from None
>>> f()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 9, in f
  File "<stdin>", line 3, in f
SyntaxError: None
>>> 
Matthias Urlichs
sumber
2
Pertanyaannya adalah tentang pengecualian lain yang terjadi selama except. Tapi Anda benar, ketika saya mengganti err = edengan, katakanlah raise AttributeError, Anda mendapatkan SyntaxErrorjejak tumpukan terlebih dahulu , diikuti oleh a During handling of the above exception, another exception occurred:dan AttributeErrorjejak tumpukan. Sebaiknya Anda tahu, meskipun sayangnya tidak dapat mengandalkan 3.5+ yang diinstal. PS: ff verstehen nicht-Deutsche vermutlich nicht;)
Tobias Kienzler
Oke, jadi saya mengubah contoh untuk memunculkan pengecualian lain, yang (seperti pertanyaan awal yang ditanyakan) akan diabaikan saat saya memunculkan kembali yang pertama.
Matthias Urlichs
3
@TobiasKienzler 3.5+ (yang saya ubah menjadi) tampaknya merupakan format yang diakui secara global. Apakah denkst du? ;)
linusg
@linusg Setuju :)
Tobias Kienzler