Ekstrak info pelacakan balik dari objek pengecualian

111

Diberikan objek Pengecualian (asal tidak diketahui) adakah cara untuk mendapatkan pelacakannya? Saya punya kode seperti ini:

def stuff():
   try:
       .....
       return useful
   except Exception as e:
       return e

result = stuff()
if isinstance(result, Exception):
    result.traceback <-- How?

Bagaimana cara mengekstrak traceback dari objek Exception setelah saya memilikinya?

georg
sumber

Jawaban:

92

Jawaban atas pertanyaan ini bergantung pada versi Python yang Anda gunakan.

Dengan Python 3

Sederhana: pengecualian dilengkapi dengan __traceback__atribut yang berisi traceback. Atribut ini juga bisa ditulis, dan bisa diatur dengan mudah menggunakan with_tracebackmetode pengecualian:

raise Exception("foo occurred").with_traceback(tracebackobj)

Fitur-fitur ini secara minimal dijelaskan sebagai bagian dari raisedokumentasi.

Semua kredit untuk bagian jawaban ini harus diberikan kepada Vyctor, yang pertama kali memposting informasi ini . Saya memasukkannya di sini hanya karena jawaban ini tertahan di atas, dan Python 3 menjadi lebih umum.

Dengan Python 2

Ini sangat rumit. Masalah dengan tracebacks adalah bahwa mereka memiliki referensi ke frame stack, dan frame stack memiliki referensi ke traceback yang memiliki referensi ke frame stack yang memiliki referensi ke ... Anda mendapatkan idenya. Ini menyebabkan masalah bagi pengumpul sampah. (Terima kasih kepada ecatmur untuk pertama kali menunjukkan ini.)

Cara yang bagus untuk menyelesaikan ini adalah dengan memutus siklus setelah meninggalkan exceptklausa, yang dilakukan Python 3. Solusi Python 2 jauh lebih buruk: Anda diberikan fungsi ad-hoc sys.exc_info(), yang hanya bekerja di dalam except klausa . Ini mengembalikan tupel yang berisi pengecualian, jenis pengecualian, dan traceback untuk pengecualian apa pun yang saat ini ditangani.

Jadi jika Anda berada di dalam exceptklausa, Anda dapat menggunakan output sys.exc_info()bersama tracebackmodul untuk melakukan berbagai hal berguna:

>>> import sys, traceback
>>> def raise_exception():
...     try:
...         raise Exception
...     except Exception:
...         ex_type, ex, tb = sys.exc_info()
...         traceback.print_tb(tb)
...     finally:
...         del tb
... 
>>> raise_exception()
  File "<stdin>", line 3, in raise_exception

Tapi seperti mengedit Anda menunjukkan, Anda mencoba untuk mendapatkan traceback yang akan telah dicetak jika pengecualian Anda belum ditangani, setelah itu telah sudah ditangani. Itu pertanyaan yang jauh lebih sulit. Sayangnya, sys.exc_infokembali (None, None, None)jika tidak ada pengecualian yang ditangani. sysAtribut terkait lainnya juga tidak membantu. sys.exc_tracebacktidak berlaku lagi dan tidak ditentukan saat tidak ada pengecualian yang ditangani; sys.last_tracebacktampak sempurna, tetapi tampaknya hanya ditentukan selama sesi interaktif.

Jika Anda dapat mengontrol bagaimana pengecualian dimunculkan, Anda mungkin dapat menggunakan inspectdan pengecualian khusus untuk menyimpan beberapa informasi. Tapi saya tidak sepenuhnya yakin bagaimana itu akan berhasil.

Sejujurnya, menangkap dan mengembalikan pengecualian adalah hal yang tidak biasa untuk dilakukan. Ini mungkin pertanda bahwa Anda perlu melakukan refactor.

pengirim
sumber
Saya setuju bahwa mengembalikan pengecualian entah bagaimana tidak konvensional, tetapi lihat pertanyaan saya yang lain untuk mengetahui alasan di balik ini.
georg
@ thg435, ok, ini lebih masuk akal. Pertimbangkan solusi saya di atas menggunakan sys.exc_infohubungannya dengan pendekatan panggilan balik yang saya sarankan untuk pertanyaan Anda yang lain.
pengirim
Info selengkapnya (hanya ada sedikit) tentang objek pelacakan balik
0xfede7c8
69

Sejak Python 3.0 [PEP 3109] kelas bawaan Exceptionmemiliki __traceback__atribut yang berisi traceback object(dengan Python 3.2.3):

>>> try:
...     raise Exception()
... except Exception as e:
...     tb = e.__traceback__
...
>>> tb
<traceback object at 0x00000000022A9208>

Masalahnya adalah setelah Googling__traceback__ untuk beberapa saat saya hanya menemukan beberapa artikel tetapi tidak ada yang menjelaskan apakah atau mengapa Anda harus (tidak) menggunakan __traceback__.

Namun, dokumentasi Python 3 untukraise mengatakan bahwa:

Objek traceback biasanya dibuat secara otomatis saat pengecualian dimunculkan dan dilampirkan sebagai __traceback__atribut, yang dapat ditulis.

Jadi saya berasumsi itu dimaksudkan untuk digunakan.

Vyktor
sumber
4
Ya, ini dimaksudkan untuk digunakan. Dari Apa yang Baru Dalam Python 3.0 "PEP 3134: Objek Exception sekarang menyimpan traceback sebagai atribut traceback . Ini berarti objek pengecualian sekarang berisi semua informasi yang berkaitan dengan pengecualian, dan ada lebih sedikit alasan untuk menggunakan sys.exc_info () ( meskipun yang terakhir tidak dihapus). "
Maciej Szpakowski
Saya tidak begitu mengerti mengapa jawaban ini begitu ragu-ragu dan tidak jelas. Ini adalah properti yang didokumentasikan; kenapa tidak "dimaksudkan untuk digunakan"?
Mark Amery
2
@MarkAmery Mungkin __dalam nama yang menunjukkan bahwa ini adalah detail implementasi, bukan milik publik?
Dasar
4
@Dasar bukan itu yang ditunjukkan di sini. Secara konvensional di Python __fooadalah metode pribadi tetapi __foo__(dengan garis bawah juga) adalah metode "ajaib" (dan bukan pribadi).
Mark Amery
1
FYI, __traceback__atribut ini 100% aman digunakan sesuka Anda, tanpa implikasi GC. Sulit untuk mengatakannya dari dokumentasinya, tapi ecatmur menemukan bukti kuat .
pengirim
38

Cara untuk mendapatkan traceback sebagai string dari objek pengecualian di Python 3:

import traceback

# `e` is an exception object that you get from somewhere
traceback_str = ''.join(traceback.format_tb(e.__traceback__))

traceback.format_tb(...)mengembalikan daftar string. ''.join(...)menggabungkan mereka bersama. Untuk referensi lebih lanjut, silakan kunjungi: https://docs.python.org/3/library/traceback.html#traceback.format_tb

Hieu
sumber
21

Sebagai tambahan, jika Anda benar-benar ingin mendapatkan pelacakan balik penuh seperti yang Anda lihat dicetak ke terminal Anda, Anda menginginkan ini:

>>> try:
...     print(1/0)
... except Exception as e:
...     exc = e
...
>>> exc
ZeroDivisionError('division by zero')
>>> tb_str = traceback.format_exception(etype=type(exc), value=exc, tb=exc.__traceback__)
>>> tb_str
['Traceback (most recent call last):\n', '  File "<stdin>", line 2, in <module>\n', 'ZeroDivisionError: division by zero\n']
>>> print("".join(tb_str))
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
ZeroDivisionError: division by zero

Jika Anda menggunakan format_tbseperti di atas jawaban menyarankan Anda akan mendapatkan lebih sedikit informasi:

>>> tb_str = "".join(traceback.format_tb(exc.__traceback__))
>>> print("".join(tb_str))
  File "<stdin>", line 2, in <module>
Daniel Porteous
sumber
4
Akhirnya! Ini harus menjadi jawaban teratas. Terima kasih, Daniel!
Dany
3
Argh, saya telah menghabiskan 20 menit terakhir mencoba mencari tahu sebelum saya menemukan ini :-) etype=type(exc)dapat dihilangkan sekarang btw: "Berubah di versi 3.5: Argumen etype diabaikan dan disimpulkan dari jenis nilainya." docs.python.org/3.7/library/… Diuji dengan Python 3.7.3.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
8

Ada alasan yang sangat bagus mengapa pelacakan tidak disimpan dalam pengecualian; karena traceback menyimpan referensi ke stack lokal-nya, hal ini akan mengakibatkan referensi melingkar dan memori (sementara) bocor hingga GC melingkar masuk. (Inilah mengapa Anda tidak boleh menyimpan traceback dalam variabel lokal .)

Tentang satu-satunya hal yang dapat saya pikirkan adalah bagi Anda untuk monkeypatch stuff's global sehingga ketika mengira itu menangkapnya Exceptionsebenarnya menangkap tipe khusus dan pengecualian menyebar ke Anda sebagai pemanggil:

module_containing_stuff.Exception = type("BogusException", (Exception,), {})
try:
    stuff()
except Exception:
    import sys
    print sys.exc_info()
ecatmur.dll
sumber
7
Ini salah. Python 3 menempatkan objek traceback dalam pengecualian, sebagai e.__traceback__.
Glenn Maynard
6
@GlennMaynard Python 3 menyelesaikan masalah dengan menghapus target pengecualian saat keluar dari exceptblok, sesuai PEP 3110 .
ecatmur