Cách thích hợp để khai báo các ngoại lệ tùy chỉnh trong Python hiện đại?


1290

Cách thích hợp để khai báo các lớp ngoại lệ tùy chỉnh trong Python hiện đại là gì? Mục tiêu chính của tôi là tuân theo bất kỳ tiêu chuẩn nào mà các lớp ngoại lệ khác có, do đó (ví dụ) bất kỳ chuỗi bổ sung nào tôi đưa vào ngoại lệ được in ra bởi bất kỳ công cụ nào bắt được ngoại lệ.

Theo "Python hiện đại" Tôi có nghĩa là một cái gì đó sẽ chạy trong Python 2.5 nhưng phải 'chính xác' cho Python 2.6 và Python 3. * cách làm việc. Và theo "tùy chỉnh", ý tôi là một đối tượng Ngoại lệ có thể bao gồm dữ liệu bổ sung về nguyên nhân gây ra lỗi: một chuỗi, cũng có thể là một số đối tượng tùy ý khác có liên quan đến ngoại lệ.

Tôi đã vấp phải cảnh báo phản đối sau đây trong 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

Có vẻ điên rồ BaseExceptioncó một ý nghĩa đặc biệt cho các thuộc tính được đặt tên message. Tôi tập hợp từ PEP-352 , thuộc tính đó có ý nghĩa đặc biệt trong 2.5 họ đang cố gắng từ chối, vì vậy tôi đoán rằng tên đó (và chỉ một mình) đã bị cấm? Ừ

Tôi cũng nhận thức một cách mờ nhạt rằng Exceptioncó một số tham số ma thuật args, nhưng tôi chưa bao giờ biết cách sử dụng nó. Tôi cũng không chắc đó là cách đúng đắn để làm mọi thứ tiến lên; rất nhiều cuộc thảo luận tôi tìm thấy trên mạng cho thấy họ đang cố gắng loại bỏ các đối số trong Python 3.

Cập nhật: hai câu trả lời đã đề xuất ghi đè __init____str__/ __unicode__/ __repr__. Điều đó có vẻ như rất nhiều gõ, nó có cần thiết không?

Câu trả lời:


1322

Có thể tôi đã bỏ lỡ câu hỏi, nhưng tại sao không:

class MyException(Exception):
    pass

Chỉnh sửa: để ghi đè một cái gì đó (hoặc vượt qua các đối số phụ), hãy làm điều này:

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

Bằng cách đó, bạn có thể chuyển thông báo lỗi đến thông số thứ hai và truy cập nó sau với e.errors


Cập nhật Python 3: Trong Python 3+, bạn có thể sử dụng cách sử dụng nhỏ gọn hơn một chút 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

35
Tuy nhiên, một ngoại lệ được xác định như thế này sẽ không thể chọn được; xem cuộc thảo luận tại đây stackoverflow.com/questions/16244923/ từ
jiakai

86
@jiakai có nghĩa là "có thể chọn". :-)
Robino

1
Theo tài liệu về python cho các trường hợp ngoại lệ do người dùng xác định, các tên được đề cập trong hàm __init__ không chính xác. Thay vì (tự, tin nhắn, lỗi) đó là (tự, biểu hiện, tin nhắn). Biểu thức thuộc tính là biểu thức đầu vào trong đó xảy ra lỗi và thông báo là lời giải thích về lỗi.
ddleon

2
Đó là một sự hiểu lầm, @ddleon. Ví dụ trong các tài liệu mà bạn đang đề cập là cho một trường hợp sử dụng cụ thể. Không có ý nghĩa đối với tên của các đối số hàm tạo của lớp con (cũng không phải số của chúng).
asthasr

498

Với ngoại lệ Python hiện đại, bạn không cần phải lạm dụng .message, hoặc ghi đè lên .__str__()hoặc .__repr__()hoặc bất kỳ của nó. Nếu tất cả những gì bạn muốn là một thông báo thông tin khi ngoại lệ của bạn được nêu ra, hãy làm điều này:

class MyException(Exception):
    pass

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

Điều đó sẽ cho một kết thúc truy tìm với MyException: My hovercraft is full of eels.

Nếu bạn muốn linh hoạt hơn từ ngoại lệ, bạn có thể chuyển từ điển làm đối số:

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

Tuy nhiên, để có được những chi tiết đó trong một exceptkhối thì phức tạp hơn một chút. Các chi tiết được lưu trữ trong argsthuộc tính, đó là một danh sách. Bạn sẽ cần phải làm một cái gì đó như thế này:

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

Vẫn có thể chuyển nhiều mục vào ngoại lệ và truy cập chúng thông qua các chỉ mục tuple, nhưng điều này rất nản lòng (và thậm chí còn có ý định khấu hao một thời gian trước). Nếu bạn cần nhiều hơn một mẩu thông tin và phương pháp trên không đủ cho bạn, thì bạn nên phân lớp Exceptionnhư mô tả trong hướng dẫn .

class MyError(Exception):
    def __init__(self, message, animal):
        self.message = message
        self.animal = animal
    def __str__(self):
        return self.message

2
"nhưng điều này sẽ bị phản đối trong tương lai" - điều này có còn được dự định để phản đối không? Python 3.7 dường như vẫn vui vẻ chấp nhận Exception(foo, bar, qux).
mtraceur

Nó đã không thấy bất kỳ công việc gần đây để làm giảm giá trị của nó kể từ lần thử cuối cùng thất bại do quá trình chuyển đổi, nhưng việc sử dụng đó vẫn không được khuyến khích. Tôi sẽ cập nhật câu trả lời của tôi để phản ánh điều đó.
frnknstn

@frnknstn, tại sao nó lại nản lòng? Trông giống như một thành ngữ tốt đẹp cho tôi.
Neves

2
@neves để bắt đầu, sử dụng bộ dữ liệu để lưu trữ thông tin ngoại lệ không có lợi ích gì so với việc sử dụng từ điển để làm điều tương tự. Nếu bạn quan tâm đến lý do đằng sau những thay đổi ngoại lệ, hãy xem PEP352
frnknstn

Phần có liên quan của PEP352 là "Ý tưởng rút lại" .
lực lượng tự do

196

"Cách thích hợp để khai báo các ngoại lệ tùy chỉnh trong Python hiện đại?"

Điều này là tốt, trừ khi ngoại lệ của bạn thực sự là một loại ngoại lệ cụ thể hơn:

class MyException(Exception):
    pass

Hoặc tốt hơn (có thể là hoàn hảo), thay vì passđưa ra một chuỗi:

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

Phân lớp ngoại lệ Phân lớp

Từ các tài liệu

Exception

Tất cả các ngoại lệ tích hợp, không thoát khỏi hệ thống đều có nguồn gốc từ lớp này. Tất cả các ngoại lệ do người dùng định nghĩa cũng nên được bắt nguồn từ lớp này.

Điều đó có nghĩa là nếu ngoại lệ của bạn là một loại ngoại lệ cụ thể hơn, thì lớp con ngoại lệ đó thay vì chung chung Exception(và kết quả sẽ là bạn vẫn xuất phát từ Exceptioncác tài liệu khuyến nghị). Ngoài ra, ít nhất bạn có thể cung cấp một chuỗi doc (và không bị buộc phải sử dụng passtừ khóa):

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

Đặt thuộc tính bạn tự tạo với một tùy chỉnh __init__. Tránh chuyển một lệnh như một đối số vị trí, người dùng mã trong tương lai của bạn sẽ cảm ơn bạn. Nếu bạn sử dụng thuộc tính thông báo không dùng nữa, việc tự gán nó sẽ tránh 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) 

Thực sự không cần phải viết của riêng bạn __str__hay __repr__. Các nội dung dựng sẵn rất đẹp và kế thừa hợp tác của bạn đảm bảo rằng bạn sử dụng nó.

Phê bình câu trả lời hàng đầu

Có thể tôi đã bỏ lỡ câu hỏi, nhưng tại sao không:

class MyException(Exception):
    pass

Một lần nữa, vấn đề ở trên là để bắt được nó, bạn sẽ phải đặt tên cụ thể (nhập nó nếu được tạo ở nơi khác) hoặc bắt Ngoại lệ, (nhưng có lẽ bạn chưa sẵn sàng để xử lý tất cả các loại Ngoại lệ, và bạn chỉ nên bắt ngoại lệ bạn chuẩn bị xử lý). Những lời chỉ trích tương tự như bên dưới, nhưng ngoài ra, đó không phải là cách để khởi tạo thông qua supervà bạn sẽ nhận được DeprecationWarningnếu bạn truy cập thuộc tính thư:

Chỉnh sửa: để ghi đè một cái gì đó (hoặc vượt qua các đối số phụ), hãy làm điều này:

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

Bằng cách đó, bạn có thể chuyển thông báo lỗi cho thông số lỗi thứ hai và truy cập nó sau với e.errors

Nó cũng đòi hỏi chính xác hai đối số được truyền vào (ngoài self.) Không hơn, không kém. Đó là một hạn chế thú vị mà người dùng trong tương lai có thể không đánh giá cao.

Để được trực tiếp - nó vi phạm khả năng thay thế Liskov .

Tôi sẽ chứng minh cả hai lỗi:

>>> 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'

So với:

>>> MyAppValueError('foo', 'FOO', 'bar').message
'foo'

2
Xin chào từ năm 2018! BaseException.messageđã biến mất trong Python 3, vì vậy bài phê bình chỉ giữ cho các phiên bản cũ, phải không?
Kos

5
@Kos Bài phê bình về Liskov Thay thế vẫn còn hiệu lực. Các ngữ nghĩa của đối số đầu tiên là một "thông điệp" cũng có thể gây tranh cãi, nhưng tôi không nghĩ rằng tôi sẽ tranh luận về vấn đề này. Tôi sẽ cho cái nhìn này nhiều hơn khi tôi có nhiều thời gian rảnh hơn.
Aaron Hall

1
FWIW, đối với Python 3 (ít nhất là 3.6+), người ta sẽ xác định lại __str__phương thức MyAppValueErrorthay vì dựa vào messagethuộc tính
Jacquot

1
@AaronHall Bạn có thể mở rộng về lợi ích của phân loại ValueError thay vì Exception không? Bạn nói rằng đây là ý nghĩa của các tài liệu nhưng việc đọc trực tiếp không hỗ trợ cho việc giải thích đó và trong Hướng dẫn Python trong Ngoại lệ do người dùng xác định rõ ràng khiến nó trở thành lựa chọn của người dùng: "Các ngoại lệ thường được lấy từ lớp Ngoại lệ, trực tiếp hoặc gián tiếp. " Do đó muốn hiểu nếu quan điểm của bạn là chính đáng, xin vui lòng.
Ostergaard

1
@ostergaard Không thể trả lời đầy đủ ngay bây giờ, nhưng trong ngắn hạn, người dùng có thêm tùy chọn bắt ValueError. Điều này có ý nghĩa nếu nó thuộc danh mục Lỗi giá trị. Nếu nó không thuộc danh mục Lỗi giá trị, tôi sẽ tranh luận về vấn đề ngữ nghĩa. Có chỗ cho một số sắc thái và lý luận về phía lập trình viên, nhưng tôi thích tính cụ thể hơn khi áp dụng. Tôi sẽ cập nhật câu trả lời của mình để sớm giải quyết vấn đề tốt hơn.
Aaron Hall

50

xem cách các ngoại lệ hoạt động theo mặc định nếu một so với nhiều thuộc tính được sử dụng (bỏ qua tracebacks):

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

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

vì vậy bạn có thể muốn có một loại " mẫu ngoại lệ ", hoạt động như một ngoại lệ, theo cách tương thích:

>>> 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')

điều này có thể được thực hiện dễ dàng với lớp con này

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

và nếu bạn không thích cách biểu diễn giống như tuple mặc định đó, chỉ cần thêm __str__phương thức vào ExceptionTemplatelớp, như:

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

và bạn sẽ có

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

32

Kể từ Python 3.8 (2018, https://docs.python.org/dev/whatsnew/3.8.html ), phương thức được đề xuất vẫn là:

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

Xin đừng quên tài liệu, tại sao một ngoại lệ tùy chỉnh là không cần thiết!

Nếu bạn cần, đây là cách để có ngoại lệ với nhiều dữ liệu hơn:

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

và lấy chúng như:

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=Nonelà quan trọng để làm cho nó dưa chua có thể. Trước khi bán nó, bạn phải gọi error.__reduce__(). Đang tải sẽ hoạt động như mong đợi.

Bạn có thể nên điều tra trong việc tìm kiếm một giải pháp bằng cách sử dụng returncâu lệnh pythons nếu bạn cần nhiều dữ liệu được chuyển đến một số cấu trúc bên ngoài. Điều này dường như là rõ ràng hơn / nhiều pythonic với tôi. Các ngoại lệ nâng cao được sử dụng nhiều trong Java, đôi khi có thể gây khó chịu, khi sử dụng một khung công tác và phải bắt tất cả các lỗi có thể.


1
Ít nhất, các tài liệu hiện tại chỉ ra rằng đây là cách để làm điều đó (ít nhất là không có __str__) thay vì các câu trả lời khác sử dụng super().__init__(...).. Chỉ là một sự xấu hổ ghi đè __str____repr__có lẽ là cần thiết để tuần tự hóa "mặc định" tốt hơn.
kevlarr

2
Câu hỏi trung thực: Tại sao điều quan trọng là ngoại lệ có thể được chọn? Các trường hợp sử dụng để bán phá giá và tải ngoại lệ là gì?
Roel Schroeven

1
@RoelSchroeven: Tôi đã phải song song mã một lần. Chạy tốt quá trình đơn, nhưng các khía cạnh của một số lớp của nó không được tuần tự hóa (hàm lambda được truyền dưới dạng đối tượng). Mất một thời gian để tôi tìm ra nó và sửa chữa nó. Có nghĩa là ai đó sau này có thể cần mã của bạn để được tuần tự hóa, không thể thực hiện được và phải tìm hiểu lý do tại sao ... Vấn đề của tôi không phải là lỗi không thể khắc phục được, nhưng tôi có thể thấy nó gây ra vấn đề tương tự.
logicOn sát thương

17

Bạn nên ghi đè __repr__hoặc __unicode__phương thức thay vì sử dụng thông báo, các đối số bạn cung cấp khi bạn xây dựng ngoại lệ sẽ nằm trong argsthuộc tính của đối tượng ngoại lệ.


7

Không, "tin nhắn" không bị cấm. Nó chỉ bị phản đối. Ứng dụng của bạn sẽ hoạt động tốt với việc sử dụng tin nhắn. Nhưng bạn có thể muốn thoát khỏi lỗi khấu hao, tất nhiên.

Khi bạn tạo các lớp Exception tùy chỉnh cho ứng dụng của mình, nhiều trong số chúng không phân lớp chỉ từ Exception, mà từ các lớp khác, như ValueError hoặc tương tự. Sau đó, bạn phải thích ứng với việc sử dụng các biến của họ.

Và nếu bạn có nhiều ngoại lệ trong ứng dụng của mình, thì thường nên có một lớp cơ sở tùy chỉnh chung cho tất cả chúng, để người dùng mô-đun của bạn có thể làm

try:
    ...
except NelsonsExceptions:
    ...

Và trong trường hợp đó, bạn có thể làm điều __init__ and __str__cần thiết ở đó, vì vậy bạn không phải lặp lại nó cho mọi ngoại lệ. Nhưng chỉ đơn giản là gọi biến tin nhắn một cái gì đó khác hơn là tin nhắn lừa.

Trong mọi trường hợp, bạn chỉ cần __init__ or __str__nếu bạn làm một cái gì đó khác với những gì Exception tự làm. Và bởi vì nếu không dùng nữa, thì bạn cần cả hai hoặc bạn gặp lỗi. Đó không phải là toàn bộ nhiều mã bạn cần cho mỗi lớp. ;)


Thật thú vị khi ngoại lệ Django không được thừa hưởng từ một cơ sở chung. docs.djangoproject.com/en/2.2/_modules/django/core/exceptions Bạn có một ví dụ hay khi bắt tất cả các ngoại lệ từ một ứng dụng cụ thể là cần thiết không? (có lẽ nó chỉ hữu ích cho một số loại ứng dụng cụ thể).
Yaroslav Nikitenko

Tôi tìm thấy một bài viết hay về chủ đề này, julien.danjou.info/python-exceptions-guide . Tôi nghĩ rằng Ngoại lệ nên được phân lớp chủ yếu dựa trên miền, không dựa trên ứng dụng. Khi ứng dụng của bạn về giao thức HTTP, bạn lấy từ HTTPError. Khi một phần của ứng dụng của bạn là TCP, bạn sẽ rút ra được ngoại lệ của phần đó từ TCPError. Nhưng nếu ứng dụng của bạn trải dài trên nhiều tên miền (tệp, quyền, v.v.), lý do để có MyBaseException giảm đi. Hay là để bảo vệ khỏi 'vi phạm lớp'?
Yaroslav Nikitenko

6

Xem một bài viết rất hay " Hướng dẫn dứt khoát về ngoại lệ Python ". Các nguyên tắc cơ bản là:

  • Luôn kế thừa từ (ít nhất) Ngoại lệ.
  • Luôn luôn gọi BaseException.__init__với chỉ một đối số.
  • Khi xây dựng thư viện, hãy định nghĩa một lớp cơ sở kế thừa từ Ngoại lệ.
  • Cung cấp chi tiết về lỗi.
  • Kế thừa từ các loại ngoại lệ dựng sẵn khi nó có ý nghĩa.

Ngoài ra còn có thông tin về cách tổ chức (trong các mô-đun) và gói ngoại lệ, tôi khuyên bạn nên đọc hướng dẫn.


1
Đây là một ví dụ tốt về lý do tại sao trên SO tôi thường kiểm tra câu trả lời được đánh giá cao nhất, nhưng cũng là câu trả lời gần đây nhất. Bổ sung hữu ích, cảm ơn.
logicOn sát thương

1
Always call BaseException.__init__ with only one argument.Có vẻ như ràng buộc không cần thiết, vì nó thực sự chấp nhận bất kỳ số lượng đối số.
Eugene Yarmash

@EugeneYarmash Tôi đồng ý, bây giờ tôi không hiểu điều đó. Dù sao tôi cũng không dùng. Có lẽ tôi nên đọc lại bài viết và mở rộng câu trả lời của mình.
Yar Tư Nikitenko

@EugeneYarmash Mình đọc lại bài viết. Nó được tuyên bố rằng trong trường hợp có một số đối số, việc triển khai C gọi "return PyObject_Str (self-> args);" Nó có nghĩa là một chuỗi nên hoạt động tốt hơn nhiều chuỗi. Bạn đã kiểm tra chưa?
Yar Tư Nikitenko ngày

3

Hãy thử ví dụ này

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")

1

Để xác định chính xác các trường hợp ngoại lệ của bạn, có một vài thực tiễn tốt nhất mà bạn cần tuân theo:

  • Xác định một lớp cơ sở kế thừa từ Exception. Điều này sẽ cho phép bắt bất kỳ trường hợp ngoại lệ nào liên quan đến dự án (ngoại lệ cụ thể hơn nên kế thừa từ nó):

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

    Tổ chức các lớp ngoại lệ này trong một mô-đun riêng biệt (ví dụ exceptions.py) nói chung là một ý tưởng tốt.

  • Để tạo một ngoại lệ tùy chỉnh, hãy phân lớp lớp ngoại lệ cơ sở.

  • Để thêm hỗ trợ cho (các) đối số bổ sung cho ngoại lệ tùy chỉnh, hãy xác định __init__()phương thức tùy chỉnh với số lượng đối số thay đổi. Gọi lớp cơ sở __init__(), chuyển bất kỳ đối số vị trí nào cho nó (hãy nhớ rằng BaseException/Exception mong đợi bất kỳ số lượng đối số vị trí nào ):

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

    Để đưa ra ngoại lệ như vậy với một đối số phụ, bạn có thể sử dụng:

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

Thiết kế này tuân thủ nguyên tắc thay thế Liskov , vì bạn có thể thay thế một thể hiện của một lớp ngoại lệ cơ sở bằng một thể hiện của một lớp ngoại lệ dẫn xuất. Ngoài ra, nó cho phép bạn tạo một thể hiện của một lớp dẫn xuất có cùng tham số với lớp cha.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.