--- tags: Python --- # Handling Exceptions [TOC] `try-except` 可以決定當錯誤發生時,相對應的處理是什麼。有了錯誤處理,就可以印出錯誤,可以將錯誤拋出,或是將錯誤訊息存入 log ,都是不錯的處理方法。 ```flow try=>operation: try exception=>condition: Exception handle=>operation: except else=>operation: else finally=>operation: finally try->exception exception(no)->else->finally exception(yes)->handle->finally ``` ## Re-raising Exceptions ```python try: a = 1 / 0 except Exception as e: print('something wrong') raise e ``` ``` Traceback (most recent call last): File "main.py", line 5, in <module> raise e ZeroDivisionError: integer division or modulo by zero ``` :arrow_down: Better,traceback 所指的 exception 會在真正發生的地方 ```python try: a = 1 / 0 except Exception as e: print('something wrong') raise ``` ``` Traceback (most recent call last): File "main.py", line 2, in <module> a = 1 / 0 ZeroDivisionError: integer division or modulo by zero ``` ## Exception Order 只要是 same class 或有相同的 base class ,都會算在同一個異常處理,因此涵蓋越多的 Exception 要擺越後面: ```python class B(Exception): pass class C(B): pass class D(C): pass for cls in [B, C, D]: try: raise cls() except D: print("D") except C: print("C") except B: print("B") ``` ``` Output: B C D ``` ```python for cls in [B, C, D]: try: raise cls() except B: print("B") except D: print("D") except C: print("C") ``` ``` Output: B B B ``` ## Raising Custom Exceptions ```python def average(x, y): if x <= 0 or y <= 0: raise Exception('Both x and y should be positive') return (x + y) / 2 ``` ``` Exception: Both x and y should be positive ``` :arrow_down: Better,這樣在別的地方 try-catch 時才好抓取特定的 Exception 。 ```python class NonPositiveError(Exception): pass def average(x, y): if x <= 0 or y <= 0: raise NonPositiveError('Both x and y should be positive') return (x + y) / 2 try: avg = average(1, 0) except NonPositiveError as err: avg = 0 print(err) finally: print(avg) ``` :arrow_down: More better ,當一個套件專案下的 Exception 越多,用一個最基礎的去涵蓋下面的 Exceptions 是常見的做法。 > When creating a module that can raise several distinct errors, a common practice is to create a base class for exceptions defined by that module, and subclass that to create specific exception classes for different error conditions. ```python class Error(Exception): pass class NonPositiveError(IntegerError): def __init__(self, x, y): msg = 'Both x and y should be positive' super().__init__(msg) self.x = x self.y = y class TooBigError(IntegerError): def __init__(self, x, y): msg = 'Both x and y should be less than 10' super().__init__(msg) self.x = x self.y = y def average(x, y): if x <= 0 or y <= 0: raise NonPositiveError('Both x and y should be positive') if x > 10 or y > 10: raise TooBigError('Both x and y should be less than 10') return (x + y) / 2 try: avg = average(11, 11) except (NonPositiveError, TooBigError) as err: avg = 0 print(err) finally: print(avg) ``` ## Exception Chaining 捕獲一個異常後跑出另一個異常,並且把異常的原始訊息保留下來。在不使用 `from` 時更傾向於新異常與正在處理的異常沒有關係。 Exception 裡的 `__cause__` 預設都是 `None` ,可以用來揭露原始的原因。 ```python raise EXCEPTION from CAUSE ``` :arrow_down: ```python exc = EXCEPTION exc.__cause__ = CAUSE raise exc ``` ### Example ```python try: v = {}['a'] except KeyError as e: raise ValueError('failed') ``` ``` Traceback (most recent call last): File "main.py", line 2, in <module> v = {}['a'] KeyError: 'a' During handling of the above exception, another exception occurred: Traceback (most recent call last): File "main.py", line 4, in <module> raise ValueError('failed') ValueError: failed ``` :vs: ```python try: v = {}['a'] except KeyError as e: raise ValueError('failed') from e ``` ``` Traceback (most recent call last): File "main.py", line 2, in <module> v = {}['a'] KeyError: 'a' The above exception was the direct cause of the following exception: Traceback (most recent call last): File "main.py", line 4, in <module> raise ValueError('failed') from e ValueError: failed ``` :vs: ```python try: v = {}['a'] except KeyError as e: a = 1 / 0 # Unexcpted error raise ValueError('failed') from e ``` ``` Traceback (most recent call last): File "main.py", line 2, in <module> v = {}['a'] KeyError: 'a' During handling of the above exception, another exception occurred: Traceback (most recent call last): File "main.py", line 4, in <module> a = 1 / 0 ZeroDivisionError: division by zero ``` 可以更知道這個 Exception 發生的原因: 1. 因為另一個 Exception 而造成,是我們所預期的情況 2. 在處理 Exception 時額外又產生的 Exception ,是我們不預期的情況 ## Google Python Style Guide 1. Make use of built-in exception classes when it makes sense. For example, raise a ValueError if you were passed a negative number but were expecting a positive one. Do not use assert statements for validating argument values of a public API. assert is used to ensure internal correctness, not to enforce correct usage nor to indicate that some unexpected event occurred. If an exception is desired in the latter cases, use a raise statement. ### built-in exception: ```python if value < 1024: raise ValueError('Must be at least 1024, not %d.' % value) ``` ```shell traceback (most recent call last): File "main.py", line 4, in <module> raise ValueError('Must be at least 1024, not %d.' % value) ValueError: Must be at least 1024, not 200. ``` ### assert: ```python assert value >= 1024, 'Must be at least 1024, not %d.' % value ``` ```shell Traceback (most recent call last): File "main.py", line 6, in <module> assert value >= 1024, 'Must be at least 1024, not %d.' % value AssertionError: Must be at least 1024, not 200. ``` ### 我覺得 1. 從 exception class 就可以清楚知道原因。 2. 從程式碼邏輯上也比較直覺。 2. Never use catch-all `except:` statements, or catch `Exception` or `StandardError` . except: will really catch everything including misspelled names, sys.exit() calls, Ctrl+C interrupts, unittest failures and all kinds of other exceptions that you simply don’t want to catch. **處理例外不要什麼都不做,也不要人人好**。不推薦使用 `except:` 或 `except Exception:` 這種涵蓋太多其他 Exception 的方式,因為捕捉到的異常太多,要找原因不好找。涵蓋的範圍可以看 [Exception hierarchy](https://docs.python.org/3/library/exceptions.html#exception-hierarchy) 。 以下就是個無法 Ctrl+C 的悲劇,最慘的是他還沒有任何處理: ```python while True: try: s = input() except: pass ``` **Catching Specific Exceptions**,只處理已知的情況 ```python try: file = open('my_file.dat') # FileNotFoundError data = file.readfile() # AttributeError print('Data Loaded') except: file = open('my_file.dat', 'w') print('File created') ``` :arrow_down: ```python try: file = open('my_file.dat') # FileNotFoundError data = file.readfile() # AttributeError print('Data Loaded') except FileNotFoundError: file = open('my_file.dat', 'w') print('File created') ``` 如果沒有指定好 Exception ,就會很難發現我們預想之外的錯誤,給予預期的 Exception 可以避免這個問題,像是下方在第二次執行時,才會正確地跳出錯誤: ``` AttributeError: '_io.TextIOWrapper' object has no attribute 'readfile' ``` 3. Minimize the amount of code in a `try` / `except` block. The larger the body of the `try`, the more likely that an exception will be raised by a line of code that you didn’t expect to raise an exception. In those cases, the `try` / `except` block hides a real error. 以下是要去開啟一的檔案抓取另一個檔案名稱,再去開啟新檔案的程式。如果 `try` 的範圍太大,遇到相同的 Exception 時處理上會遇到問題。 ```python try: file = open(filename) new_filename = file.readline() new_file = open(new_filename) data = new_file.read() except FileNotFoundError: print('File not found, creating one') file = open(filename, 'w') ``` ## 資料來源 1. [Errors and Exceptions / Python](https://docs.python.org/3/tutorial/errors.html) 1. [Built-in Exceptions / Python](https://docs.python.org/3/library/exceptions.html) 1. [Google Python Style Guide / Google](http://google.github.io/styleguide/pyguide.html#24-exceptions) 2. [Fail fast / Jim Shore](https://martinfowler.com/ieeeSoftware/failFast.pdf)