Cara yang tepat untuk mendeklarasikan pengecualian khusus dalam Python modern?

1291

Apa cara yang tepat untuk mendeklarasikan kelas pengecualian khusus dalam Python modern? Tujuan utama saya adalah mengikuti standar apa pun yang dimiliki kelas pengecualian lain, sehingga (misalnya) string tambahan apa pun yang saya sertakan dalam pengecualian dicetak oleh alat apa pun yang menangkap pengecualian.

Dengan "Python modern" Maksud saya sesuatu yang akan berjalan dalam Python 2.5 tetapi menjadi 'benar' untuk Python 2.6 dan Python 3. * cara melakukan sesuatu. Dan dengan "kebiasaan" yang saya maksud adalah objek Pengecualian yang dapat menyertakan data tambahan tentang penyebab kesalahan: string, mungkin juga beberapa objek arbitrer lain yang relevan dengan pengecualian.

Saya tersandung oleh peringatan penghentian berikut di Python 2.6.2:

>>> class MyError(Exception):
...     def __init__(self, message):
...         self.message = message
... 
>>> MyError("foo")
_sandbox.py:3: DeprecationWarning: BaseException.message has been deprecated as of Python 2.6

Tampaknya gila yang BaseExceptionmemiliki makna khusus untuk atribut yang dinamai message. Saya kumpulkan dari PEP-352 bahwa atribut memang memiliki arti khusus dalam 2.5 yang mereka coba hilangkan, jadi saya kira nama itu (dan itu saja) sekarang dilarang? Ugh.

Saya juga tidak sadar yang Exceptionmemiliki beberapa parameter ajaib args, tetapi saya tidak pernah tahu cara menggunakannya. Saya juga tidak yakin itu cara yang tepat untuk melakukan berbagai hal ke depan; banyak diskusi yang saya temukan online menyarankan mereka mencoba untuk menghilangkan args dengan Python 3.

Pembaruan: dua jawaban telah menyarankan penggantian __init__, dan __str__/ __unicode__/ __repr__. Sepertinya banyak mengetik, apakah perlu?

Nelson
sumber

Jawaban:

1323

Mungkin saya melewatkan pertanyaan itu, tetapi mengapa tidak:

class MyException(Exception):
    pass

Edit: untuk mengganti sesuatu (atau memberikan argumen tambahan), lakukan ini:

class ValidationError(Exception):
    def __init__(self, message, errors):

        # Call the base class constructor with the parameters it needs
        super(ValidationError, self).__init__(message)

        # Now for your custom code...
        self.errors = errors

Dengan begitu Anda bisa mengirimkan dikt dari pesan kesalahan ke param kedua, dan sampai nanti dengan e.errors


Pembaruan Python 3: Dalam Python 3+, Anda dapat menggunakan penggunaan yang sedikit lebih ringkas dari super():

class ValidationError(Exception):
    def __init__(self, message, errors):

        # Call the base class constructor with the parameters it needs
        super().__init__(message)

        # Now for your custom code...
        self.errors = errors
gahooa
sumber
35
Namun pengecualian yang ditentukan seperti ini tidak akan dapat dipilih; lihat pembahasannya di sini stackoverflow.com/questions/16244923/…
jiakai
87
@jiakai berarti "picklable". :-)
Robino
1
Mengikuti dokumentasi python untuk pengecualian yang ditentukan pengguna, nama-nama yang disebutkan dalam fungsi __init__ salah. Alih-alih (diri, pesan, kesalahan) itu (diri, ekspresi, pesan). Ekspresi atribut adalah ekspresi input di mana kesalahan terjadi dan pesan adalah penjelasan tentang kesalahan tersebut.
ddleon
2
Itu adalah kesalahpahaman, @ddleon. Contoh dalam dokumen yang Anda maksud adalah untuk kasus penggunaan tertentu. Tidak ada signifikansi untuk nama argumen konstruktor subclass (atau jumlah mereka).
asthasr
498

Dengan Pengecualian Python modern, Anda tidak perlu penyalahgunaan .message, atau menimpa .__str__()atau .__repr__()atau apapun itu. Jika semua yang Anda inginkan adalah pesan informatif ketika pengecualian Anda muncul, lakukan ini:

class MyException(Exception):
    pass

raise MyException("My hovercraft is full of eels")

Itu akan memberikan traceback yang diakhiri dengan MyException: My hovercraft is full of eels.

Jika Anda menginginkan lebih banyak fleksibilitas dari pengecualian, Anda bisa meneruskan kamus sebagai argumen:

raise MyException({"message":"My hovercraft is full of animals", "animal":"eels"})

Namun, untuk mendapatkan rincian itu dalam sebuah exceptblok sedikit lebih rumit. Rinciannya disimpan dalam argsatribut, yang merupakan daftar. Anda perlu melakukan sesuatu seperti ini:

try:
    raise MyException({"message":"My hovercraft is full of animals", "animal":"eels"})
except MyException as e:
    details = e.args[0]
    print(details["animal"])

Masih dimungkinkan untuk mengirimkan beberapa item ke pengecualian dan mengaksesnya melalui indeks tuple, tetapi ini sangat tidak disarankan (dan bahkan dimaksudkan untuk penghentian beberapa waktu yang lalu). Jika Anda membutuhkan lebih dari satu informasi dan metode di atas tidak cukup untuk Anda, maka Anda harus subkelas Exceptionseperti yang dijelaskan dalam tutorial .

class MyError(Exception):
    def __init__(self, message, animal):
        self.message = message
        self.animal = animal
    def __str__(self):
        return self.message
frnknstn
sumber
2
"tetapi ini akan ditinggalkan di masa depan" - apakah ini masih dimaksudkan untuk penghentian? Python 3.7 tampaknya masih dengan senang hati menerima Exception(foo, bar, qux).
mtraceur
Belum terlihat adanya pekerjaan baru-baru ini untuk menghilangkannya sejak upaya terakhir gagal karena sakitnya transisi, tetapi penggunaannya masih tidak dianjurkan. Saya akan memperbarui jawaban saya untuk mencerminkan hal itu.
frnknstn
@ frnknstn, mengapa itu tidak disarankan? Sepertinya idiom yang bagus untukku.
neves
2
@neves sebagai permulaan, menggunakan tuple untuk menyimpan informasi pengecualian tidak memiliki manfaat dibandingkan menggunakan kamus untuk melakukan hal yang sama. Jika Anda tertarik pada alasan di balik perubahan pengecualian, lihat PEP352
frnknstn
Bagian yang relevan dari PEP352 adalah "Gagasan yang Ditarik" .
liberforce
196

"Cara yang tepat untuk mendeklarasikan pengecualian khusus dengan Python modern?"

Ini baik-baik saja, kecuali pengecualian Anda benar-benar jenis pengecualian yang lebih spesifik:

class MyException(Exception):
    pass

Atau lebih baik (mungkin sempurna), daripada passmemberikan docstring:

class MyException(Exception):
    """Raise for my specific kind of exception"""

Subkelas Pengecualian Subkelas

Dari dokumen

Exception

Semua pengecualian built-in, non-sistem keluar dari kelas ini. Semua pengecualian yang ditentukan pengguna juga harus diturunkan dari kelas ini.

Itu berarti bahwa jika pengecualian Anda adalah jenis pengecualian yang lebih spesifik, subkelas pengecualian itu daripada generik Exception(dan hasilnya adalah Anda masih berasal dari Exceptionseperti yang direkomendasikan oleh dokumen). Juga, Anda setidaknya dapat memberikan dokumen (dan tidak dipaksa untuk menggunakanpass kata kunci):

class MyAppValueError(ValueError):
    '''Raise when my specific value is wrong'''

Tetapkan atribut yang Anda buat sendiri dengan kustom __init__. Hindari melewatkan dikt sebagai argumen posisional, pengguna masa depan kode Anda akan berterima kasih. Jika Anda menggunakan atribut pesan yang tidak digunakan lagi, menetapkannya sendiri akan menghindari DeprecationWarning:

class MyAppValueError(ValueError):
    '''Raise when a specific subset of values in context of app is wrong'''
    def __init__(self, message, foo, *args):
        self.message = message # without this you may get DeprecationWarning
        # Special attribute you desire with your Error, 
        # perhaps the value that caused the error?:
        self.foo = foo         
        # allow users initialize misc. arguments as any other builtin Error
        super(MyAppValueError, self).__init__(message, foo, *args) 

Benar-benar tidak perlu menulis sendiri __str__atau__repr__ . Yang builtin sangat bagus, dan warisan koperasi Anda memastikan bahwa Anda menggunakannya.

Kritik atas jawaban teratas

Mungkin saya melewatkan pertanyaan itu, tetapi mengapa tidak:

class MyException(Exception):
    pass

Sekali lagi, masalah dengan hal di atas adalah bahwa untuk menangkapnya, Anda harus menyebutkannya secara khusus (mengimpornya jika dibuat di tempat lain) atau menangkap Pengecualian, (tetapi Anda mungkin tidak siap untuk menangani semua jenis Pengecualian, dan Anda seharusnya hanya menangkap pengecualian yang siap Anda tangani). Kritik serupa dengan yang di bawah ini, tetapi juga bukan cara untuk menginisialisasi melalui super, dan Anda akan mendapatkan DeprecationWarningjika Anda mengakses atribut pesan:

Edit: untuk mengganti sesuatu (atau memberikan argumen tambahan), lakukan ini:

class ValidationError(Exception):
    def __init__(self, message, errors):

        # Call the base class constructor with the parameters it needs
        super(ValidationError, self).__init__(message)

        # Now for your custom code...
        self.errors = errors

Dengan begitu Anda bisa mengirimkan dikt dari pesan kesalahan ke param kedua, dan sampai nanti dengan e.errors

Ini juga membutuhkan tepat dua argumen untuk diteruskan (selain dari self .) Tidak lebih, tidak kurang. Itu adalah kendala yang menarik yang mungkin tidak dihargai oleh pengguna di masa depan.

Untuk menjadi langsung - itu melanggar substitusi Liskov .

Saya akan menunjukkan kedua kesalahan:

>>> ValidationError('foo', 'bar', 'baz').message

Traceback (most recent call last):
  File "<pyshell#10>", line 1, in <module>
    ValidationError('foo', 'bar', 'baz').message
TypeError: __init__() takes exactly 3 arguments (4 given)

>>> ValidationError('foo', 'bar').message
__main__:1: DeprecationWarning: BaseException.message has been deprecated as of Python 2.6
'foo'

Dibandingkan dengan:

>>> MyAppValueError('foo', 'FOO', 'bar').message
'foo'
Aaron Hall
sumber
2
Halo dari 2018! BaseException.messagehilang dalam Python 3, jadi kritiknya hanya berlaku untuk versi lama, kan?
Kos
5
@Kos Kritik tentang Substitutabilitas Liskov masih valid. Semantik argumen pertama sebagai "pesan" juga bisa dipertanyakan, tapi saya rasa saya tidak akan memperdebatkan intinya. Saya akan memberikan ini lebih banyak melihat ketika saya memiliki lebih banyak waktu luang.
Aaron Hall
1
FWIW, untuk Python 3 (setidaknya untuk 3,6+), orang akan mendefinisikan kembali __str__metode MyAppValueErroralih-alih mengandalkan messageatribut
Jacquot
1
@AaronHall Bisakah Anda memperluas manfaat sub-classing ValueError daripada Pengecualian? Anda menyatakan bahwa ini adalah apa yang dimaksud dengan dokumen tetapi bacaan langsung tidak mendukung interpretasi itu dan dalam Tutorial Python di bawah Pengecualian yang ditentukan pengguna dengan jelas menjadikannya pilihan pengguna: "Pengecualian biasanya berasal dari kelas Pengecualian, baik secara langsung atau tidak langsung. " Oleh karena itu ingin memahami jika pandangan Anda dapat dibenarkan.
ostergaard
1
@ostergaard Tidak bisa menjawab secara penuh sekarang, tetapi singkatnya, pengguna mendapatkan opsi tambahan untuk menangkap ValueError. Ini masuk akal jika itu dalam kategori Kesalahan Nilai. Jika tidak dalam kategori Kesalahan Nilai, saya akan membantahnya pada semantik. Ada ruang untuk beberapa nuansa dan alasan di bagian programmer, tapi saya lebih suka kekhususan bila berlaku. Saya akan memperbarui jawaban saya untuk lebih baik menangani masalah ini dalam waktu dekat.
Aaron Hall
50

lihat bagaimana pengecualian berfungsi secara default jika satu vs lebih banyak atribut digunakan (tracebacks dihilangkan):

>>> raise Exception('bad thing happened')
Exception: bad thing happened

>>> raise Exception('bad thing happened', 'code is broken')
Exception: ('bad thing happened', 'code is broken')

jadi Anda mungkin ingin memiliki semacam " template pengecualian ", yang berfungsi sebagai pengecualian itu sendiri, dengan cara yang kompatibel:

>>> nastyerr = NastyError('bad thing happened')
>>> raise nastyerr
NastyError: bad thing happened

>>> raise nastyerr()
NastyError: bad thing happened

>>> raise nastyerr('code is broken')
NastyError: ('bad thing happened', 'code is broken')

ini dapat dilakukan dengan mudah dengan subkelas ini

class ExceptionTemplate(Exception):
    def __call__(self, *args):
        return self.__class__(*(self.args + args))
# ...
class NastyError(ExceptionTemplate): pass

dan jika Anda tidak menyukai representasi standar seperti tuple, cukup tambahkan __str__metode ke ExceptionTemplatekelas, seperti:

    # ...
    def __str__(self):
        return ': '.join(self.args)

dan kamu akan punya

>>> raise nastyerr('code is broken')
NastyError: bad thing happened: code is broken
mykhal
sumber
32

Pada Python 3.8 (2018, https://docs.python.org/dev/whatsnew/3.8.html ), metode yang disarankan masih:

class CustomExceptionName(Exception):
    """Exception raised when very uncommon things happen"""
    pass

Tolong jangan lupa untuk mendokumentasikan, mengapa pengecualian khusus diperlukan?

Jika perlu, inilah cara untuk pengecualian dengan lebih banyak data:

class CustomExceptionName(Exception):
    """Still an exception raised when uncommon things happen"""
    def __init__(self, message, payload=None):
        self.message = message
        self.payload = payload # you could add more args
    def __str__(self):
        return str(self.message) # __str__() obviously expects a string to be returned, so make sure not to send any other data types

dan ambil mereka seperti:

try:
    raise CustomExceptionName("Very bad mistake.", "Forgot upgrading from Python 1")
except CustomExceptionName as error:
    print(str(error)) # Very bad mistake
    print("Detail: {}".format(error.payload)) # Detail: Forgot upgrading from Python 1

payload=NonePenting untuk membuatnya menjadi acar. Sebelum membuangnya, Anda harus menelepon error.__reduce__(). Memuat akan berfungsi seperti yang diharapkan.

Anda mungkin harus menyelidiki dalam menemukan solusi menggunakan returnpernyataan ular sanca jika Anda memerlukan banyak data untuk ditransfer ke beberapa struktur luar. Ini tampaknya lebih jelas / lebih pythonic bagi saya. Pengecualian tingkat lanjut banyak digunakan di Jawa, yang kadang-kadang bisa menjengkelkan, saat menggunakan kerangka kerja dan harus menangkap semua kemungkinan kesalahan.

keluarga
sumber
1
Paling tidak, dokumen saat ini menunjukkan ini adalah cara untuk melakukannya (setidaknya tanpa __str__) daripada jawaban lain yang menggunakan super().__init__(...).. Hanya rasa malu yang menimpa __str__dan __repr__mungkin diperlukan hanya untuk serialisasi "default" yang lebih baik.
kevlarr
2
Pertanyaan jujur: Mengapa penting untuk pengecualian acar? Apa kasus penggunaan untuk pembuangan dan pemuatan pengecualian?
Roel Schroeven
1
@RoelSchroeven: Saya harus memparalelkan kode sekali. Menjalankan proses tunggal yang baik, tetapi aspek-aspek dari beberapa kelasnya tidak bersambung (fungsi lambda diteruskan sebagai objek). Butuh waktu untuk mencari tahu & memperbaikinya. Berarti seseorang di kemudian hari mungkin membutuhkan kode Anda untuk diserialisasi, tidak dapat melakukannya, dan harus menggali alasannya ... Masalah saya bukanlah kesalahan yang tidak dapat dihapus, tetapi saya dapat melihatnya menyebabkan masalah serupa.
logicOnAbstractions
17

Anda harus mengganti __repr__atau __unicode__metode alih-alih menggunakan pesan, argumen yang Anda berikan saat Anda membuat pengecualian akan berada dalam argsatribut objek pengecualian.

M. Utku ALTINKAYA
sumber
7

Tidak, "pesan" tidak dilarang. Itu sudah usang. Aplikasi Anda akan berfungsi dengan baik dengan menggunakan pesan. Tapi Anda mungkin ingin menyingkirkan kesalahan penghinaan, tentu saja.

Saat Anda membuat kelas pengecualian khusus untuk aplikasi Anda, banyak dari mereka tidak subkelas hanya dari pengecualian, tetapi dari yang lain, seperti ValueError atau serupa. Maka Anda harus beradaptasi dengan penggunaan variabel mereka.

Dan jika Anda memiliki banyak pengecualian dalam aplikasi Anda, biasanya ide yang baik untuk memiliki kelas dasar kustom umum untuk semuanya, sehingga pengguna modul Anda dapat melakukan

try:
    ...
except NelsonsExceptions:
    ...

Dan dalam hal ini Anda dapat melakukan __init__ and __str__ dibutuhkan di sana, sehingga Anda tidak perlu mengulanginya untuk setiap pengecualian. Tetapi hanya memanggil variabel pesan itu sesuatu yang lain dari pada pesanlah yang berhasil.

Bagaimanapun, Anda hanya perlu __init__ or __str__jika Anda melakukan sesuatu yang berbeda dari apa yang dilakukan oleh Pengecualian itu sendiri. Dan karena jika penghentian, Anda kemudian membutuhkan keduanya, atau Anda mendapatkan kesalahan. Itu tidak banyak kode tambahan yang Anda butuhkan per kelas. ;)

Lennart Regebro
sumber
Sangat menarik bahwa pengecualian Django tidak mewarisi dari pangkalan bersama. docs.djangoproject.com/en/2.2/_modules/django/core/exceptions Apakah Anda memiliki contoh yang baik ketika menangkap semua pengecualian dari aplikasi tertentu diperlukan? (mungkin bermanfaat hanya untuk beberapa jenis aplikasi tertentu).
Yaroslav Nikitenko
Saya menemukan artikel yang bagus tentang topik ini, julien.danjou.info/python-exceptions-guide . Saya pikir Pengecualian harus disubklasifikasikan terutama berbasis domain, bukan berbasis aplikasi. Saat aplikasi Anda tentang protokol HTTP, Anda berasal dari HTTPError. Ketika bagian dari aplikasi Anda adalah TCP, Anda mendapatkan pengecualian bagian itu dari TCPError. Tetapi jika aplikasi Anda mencakup banyak domain (file, izin, dll), alasan untuk memiliki MyBaseException berkurang. Atau untuk melindungi dari 'pelanggaran lapisan'?
Yaroslav Nikitenko
6

Lihat artikel yang sangat bagus " Panduan definitif untuk pengecualian Python ". Prinsip dasarnya adalah:

  • Selalu mewarisi dari (setidaknya) Pengecualian.
  • Selalu panggil BaseException.__init__dengan hanya satu argumen.
  • Saat membangun perpustakaan, tentukan kelas dasar yang mewarisi dari Pengecualian.
  • Berikan detail tentang kesalahan.
  • Mewarisi dari builtin pengecualian ketika masuk akal.

Ada juga informasi tentang pengorganisasian (dalam modul) dan pengecualian pembungkus, saya sarankan untuk membaca panduan ini.

Yaroslav Nikitenko
sumber
1
Ini adalah contoh yang baik mengapa pada SO saya biasanya memeriksa jawaban yang paling banyak dipilih, tetapi yang paling baru juga. Selain bermanfaat, terima kasih.
logicOnAbstractions
1
Always call BaseException.__init__ with only one argument.Sepertinya kendala yang tidak dibutuhkan, karena sebenarnya menerima sejumlah argumen.
Eugene Yarmash
@EugeneYarmash Saya setuju, sekarang saya tidak mengerti itu. Saya tidak menggunakannya. Mungkin saya harus membaca ulang artikel dan memperluas jawaban saya.
Yaroslav Nikitenko
@EugeneYarmash Saya membaca artikel itu lagi. Dinyatakan bahwa dalam beberapa argumen, implementasi C memanggil "return PyObject_Str (self-> args);" Ini berarti bahwa satu string harus bekerja lebih baik daripada beberapa string. Apakah Anda memeriksa itu?
Yaroslav Nikitenko
3

Coba Contoh ini

class InvalidInputError(Exception):
    def __init__(self, msg):
        self.msg = msg
    def __str__(self):
        return repr(self.msg)

inp = int(input("Enter a number between 1 to 10:"))
try:
    if type(inp) != int or inp not in list(range(1,11)):
        raise InvalidInputError
except InvalidInputError:
    print("Invalid input entered")
Omkaar.K
sumber
1

Untuk menetapkan pengecualian Anda dengan benar, ada beberapa praktik terbaik yang perlu Anda ikuti:

  • Tentukan kelas dasar yang diwarisi dari Exception. Ini akan memungkinkan untuk menangkap pengecualian yang terkait dengan proyek (pengecualian yang lebih spesifik harus diwarisi darinya):

    class MyProjectError(Exception):
        """A base class for MyProject exceptions."""

    Mengatur kelas pengecualian ini dalam modul terpisah (mis. exceptions.py) Umumnya merupakan ide yang bagus.

  • Untuk membuat pengecualian khusus, subkelas kelas pengecualian dasar.

  • Untuk menambahkan dukungan untuk argumen tambahan ke pengecualian khusus, tentukan __init__()metode kustom dengan sejumlah variabel argumen. Panggil kelas dasar __init__(), serahkan argumen posisi apa pun ke sana (ingat bahwa BaseException/Exception harapkan sejumlah argumen posisional ):

    class CustomError(MyProjectError):
        def __init__(self, *args, **kwargs):
            super(CustomError, self).__init__(*args)
            self.foo = kwargs.get('foo')

    Untuk mengajukan pengecualian dengan argumen tambahan, Anda dapat menggunakan:

    raise CustomError('Something bad happened', foo='foo')

Desain ini mematuhi prinsip substitusi Liskov , karena Anda dapat mengganti instance kelas pengecualian dasar dengan instance kelas pengecualian turunan. Juga, ini memungkinkan Anda untuk membuat turunan dari kelas turunan dengan parameter yang sama dengan induknya.

Eugene Yarmash
sumber