Python Exceptions Handling

Trong python cũng như các ngôn ngữ khác, những trường hợp lỗi luôn có thể xảy ra gây ra lỗi không đáng có trong chương trình.
Các lỗi này ảnh hưởng đến hiệu suất của chương trình, đôi khi còn có thể gây nên crash hoặc server downtime. Có ít nhất hai loại lỗi khác nhau: lỗi cú pháp (Syntax Errors) và ngoại lệ (Exception).

Syntax Errors là gì ?

Lỗi cú pháp, hay còn được biết tới là lỗi phân tích cú pháp (parsing error), có lẽ là lỗi phổ biến nhất đối với lập trình viên Python.
>>> while True print 'Hello world'
  File "<stdin>", line 1, in ?
    while True print 'Hello world'
                   ^
SyntaxError: invalid syntax
Bộ phân tích cú pháp lặp lại dòng gây lỗi, và hiển thị một mũi tên hình con trỏ vào điểm đầu tiên lỗi được phát hiện. Lỗi được phát hiện nằm ở phía trước dấu mũi tên. 
Ở ví dụ trên, lỗi được phát hiện tại hàm print vì bị thiếu dấu : trước đó. Tên file và số dòng cũng được thông báo để bạn có thể xác định được nơi cần sửa.

Exception là gì?

Exception có thể hiểu là các lỗi, biến cố khi thực thi chương trình khiến cho luồng thực thi bị phá vỡ ngay cả khi câu lệnh hoặc biểu thức có cú pháp chính xác. Lỗi được phát hiện trong quá trình thực thi. Ví dụ:
>>> print(a)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'a' is not defined
>>> '2' + 2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: cannot concatenate 'str' and 'int' objects
Ngoại lệ có nhiều loại khác nhau và được in như một phần của thông báo lỗi, ngoại lệ trong ví dụ trên là NameError và TypeError
Cấu trúc chung của chuỗi thông báo lỗi được in ra có dạng tên của ngoại lệ được tính hợp sẵn của Python và lời giải thích nguyên nhân gây ra lỗi, trừ ngoại lệ do người dùng tự định nghĩa. Phần trước của thông báo lỗi cho thấy nơi ngoại lệ xảy ra, dưới dạng một truy xuất stack
Ở ví dụ thứ nhất, chúng ta đã dùng hàm print() để in ra giá trị biến a, tuy nhiên biến a chưa được khai báo nên Python trả về cho chúng ta lỗi NameError.

Xử lý Exceptions

Exceptions handling là một phương thức hết sức đơn giản để các bạn quản lí những lỗi có thể xảy ra trong chương trình của mình. Khi bạn nghĩ đoạn code của bạn có thể gây ra lỗi, bạn có thể sử dụng exceptions handling để phát hiện và xử lí chúng.

Câu lệnh raise

Cấu trúc của câu lệnh raise được hiểu như sau:
raise_stmt ::=  "raise" [expression ["from" expression]]
  • raise expression được sử dụng để gọi một ngoại lệ (có thể là ngoại lệ được xây dựng sẵn của python hoặc ngoại lệ do người dùng tự định nghĩa) tùy thuộc vào một điều kiện nhất định, nó được xem như là một phần của Exceptions handling.
>>> x = 10
>>> if x > 5:
>>>    raise Exception('x không nên bé hơn 5. Giá trị của x là: {}'.format(x))
Exception: x không nên bé hơn 5. Giá trị của x là: 10
  • Câu lệnh raise mà không có bất kỳ đối số nào là một cú pháp python đặc biệt được gọi là reraise. Nó có nghĩa là được ngoại lệ lại và tái nâng cao nó. Trình thông dịch sẽ tìm ngoại lệ cuối cùng và xử lý (chú ý ở đây sẽ là ngoại lệ cuối cùng được xử lí chứ không phải ngoại lệ cuối cùng được nêu ra trong mã nguồn). Sau đó, nó hoạt động giống như khi bạn sử dụng cú pháp raise expression, giá trị và truy xuất lại gần đây nhất được in ra thông báo lỗi. Điều này thực sự hữu ích khi trình xử lí lỗi phát hiện ra nó không thể xử lí một ngoại lệ mà nó nhận được, bằng câu lệnh raise nó sẽ quay ngược lại và tái nâng cao ngoại lệ.
  • Vì lí do trên, nếu reraise được thực hiện trong trường hợp không phải là một trường hợp ngoại lệ của một ngoại lệ khác, lỗi sau được hiển thị: RuntimeError: No active exception to reraise
>>> x = 10
>>> if x > 5:
>>> raise 
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
RuntimeError: No active exception to reraise

Sự khác nhau giữa raiseraise expressionraise expression from expression

Ví dụ 1
try:
    raise ValueError
except:
    raise Exception 
Kết quả:
Nhập vào một số: `                                                       
Traceback (most recent call last):
  File "C:\Users\Finn\Desktop\python.py", line 2, in <module>
    raise ValueError
ValueError

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Users\Finn\Desktop\python.py", line 4, in <module>
    raise Exception
Exception                                                 
Ví dụ 2
try:
    raise ValueError
except:
    raise
Kết quả:
Traceback (most recent call last):
  File "C:\Users\Finn\Desktop\python.py", line 2, in <module>
    raise ValueError
ValueError
Ví dụ 3
try:
    raise ValueError
except Exception as e:
    raise NameError from e
Kết quả:

Traceback (most recent call last):
  File "C:\Users\Finn\Desktop\python.py", line 2, in <module>
    raise ValueError
ValueError

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "C:\Users\Finn\Desktop\python.py", line 4, in <module>
    raise NameError from e
NameError
Ví dụ 4
try:
    raise ValueError
except Exception as e:
    raise NameError from None
Kết quả:
Traceback (most recent call last):
  File "C:\Users\Finn\Desktop\python.py", line 4, in <module>
    raise NameError from None
NameError
Python sử dụng một cách xây dựng traceback rất đặc biệt. Thay vì xây dựng toàn bộ dấu vết ngăn xếp (stack) trong việc tạo ngoại lệ( như Java) hoặc khi một ngoại lệ được tạo ra, Python đã xây dựng một khung traceback từng phần tại từng thời điểm khi ngoại lệ bật lên. 
Mỗi khi một ngoại lệ mới phát sinh thì một khung ngăn xếp mới được tạo ra với lệnh raise có đối số , vòng lặp trình thông dịch bytecode Python thực hiện PyTraceback_Here để thêm mới đến danh sách liên kết của các đối tượng traceback đại diện cho stack
Python duy trì một chồng các ngoại lệ (và traceback) cho mỗi luồng và bị treo khi các khối except và finally chưa được thực hiện xong. 
Câu lệnh raise không có đối số sẽ khôi phục ngoại lệ (và traceback) được đại diện bởi mục nhập đầu tiên trên stack này, ngay cả khi except hoặc finally đang ở trong một hàm khác.
Khi thực hiện lệnh ở ví dụ 1:
raise ValueError
Python xây dựng một traceback tương ứng với dòng đó:
Traceback (most recent call last):
  File "C:\Users\Finn\Desktop\python.py", line 2, in <module>
    raise ValueError
ValueError
Khi thực hiện raise không có đối số ở ví dụ, traceback này được khôi phục, nhưng không có đối số nào đi với lệnh raise. Cho nên, khi ngoại lệ tạo ra, sẽ không có trackback được thêm vào theo dõi ngăn xếp, dẫn đến dấu vết ngăn xếp cuối cùng được hiển thị:
Traceback (most recent call last):
  File "C:\Users\Finn\Desktop\python.py", line 2, in <module>
    raise ValueError
ValueError
Còn raise expression from expression là một cách xây dựng python mới từ PEP 3134 nó như là một kiểu mở rộng của raise expression. Khi bạn sử dụng from, thuộc tính __cause__ được thiết lập và thông báo cho biết lí do trực tiếp gây ra ngoại lệ. Nếu bạn bỏ qua from thì thuộc tính __cause__ cũng sẽ không được thiết lập, nhưng thuộc tính __context__ vẫn thể được thiết lập, và traceback sau đó in ra màn hình The above exception was the direct cause of the following exception: (chính là cú pháp raise expression) .
Khi tạo một trình xử lý ngoại lệ mà bạn không muốn hiển thị ngữ cảnh (không muốn trong quá trình xử lý một ngoại lệ khác tạo ra thông báo), thì hãy sử dụng raise ... from None để đặt cờ hiệu __suppress_context__ thành True khi đó __context__ sẽ bị bỏ qua khi in ra màn hình một lần traceback.
Nói cách khác, Python đặt mộtcontext về các ngoại lệ để bạn có thể quan sát một ngoại lệ được tạo ra ở đâu, cho phép bạn xem liệu một ngoại lệ khác có đang được diễn ra hay không. Bạn cũng có thể thêm cause vào một ngoại lệ, làm cho traceback rõ ràng về những ngoại lệ có thể đang xảy ra đồng thời, và ngữ cảnh bị bỏ qua (nhưng vẫn có thể quan sát khi ở chế độ gỡ lỗi-debugging). Sử dụng raise ... from None để bỏ qua ngữ cảnh khi xử lí mọt ngoại lệ khác diễn ra đồng thời .

Câu lệnh assert

assert:
 # Câu lệnh điều kiện
Câu lệnh trong assert dùng để kiểm tra điều kiện đó và ngay lập tức kích hoạt lỗi nếu điều kiện là False. Câu lệnh assert <expression> có 2 dạng, tương đương với: 
Dạng thứ 1:
if __debug__:
    if not expression: raise AssertionError
Dạng thứ 2:
if __debug__:
    if not expression1: raise AssertionError(expression2)
Vì sao ở đây chúng ta đặt điều kiện if __debug__. Bở vì Khi chạy python trong chế độ tối ưu hóa, nơi __debug__ là Falseassert sẽ bị bỏ qua. Chỉ cần thêm tùy chọn -O làm cờ:
python -O script.py
Ví dụ:
>>> assert True # nothing happens
>>> assert False
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError
Để tạo một thông báo tùy chọn ta có thể viết:
a = 5
>>> assert(a==6), "sai"
AssertionError: Sai
# Tương đương với
>>> assert False, "sai"

Khối lệnh try...except

Câu hỏi được đặt ra ở đây là làm sao phát hiện được các ngoại lệ có thể phát sinh trong đoạn code của bạn. Đừng lo, trong python bạn có thể sử dụng từ khoá try và except để bắt toàn bộ ngoại lệ có thể xảy ra trong một khối code của bạn. Các câu lệnh nằm trong khối lệnh try nếu xảy ra lỗi nó sẽ gọi ra exception handling trong khối lệnh nằm sau except và xử lí chúng. Cú pháp ta sử dụng ở đây là:
try:
    # mã nguồn ở đây
except exceptionName:
    # mã xử lí lỗi
Python sẽ xử lý tất cả các câu lệnh trong khối lệnh try một cách bình thường, khi lỗi xảy ra các câu lệnh trong khối exceptsẽ được thực hiện.
while True:
    try:
        number = int(input("Nhập vào một số: "))
        print(number)
    except Exception as e:
        # Xử lý khi dữ liệu đầu vào không chính xác
        print('Dữ liệu đầu vào không chính xác: ' + str(e))
Ở đây khi người dùng nhập vào một kí tự không chính xác, thay vì trả về lỗi và dừng chương trình, chương trình sẽ thông báo lỗi được khai báo ở khối lệnh except và tiếp tục chạy các vòng lặp while.
  • Xử lí lỗi xác định
while True:
    try:
        number = int(input("Nhập vào một số: "))
        print(number)
    except NameError:
        # Xử lý khi dữ liệu đầu vào không chính xác
        print('Dữ liệu đầu vào không chính xác: ' + str(e))
Khi chúng ta biết rõ chương trình của mình sẽ xảy ra lỗi gì (ở ví dụ trên là lỗi NameError), chúng ta có thể bắt ngay lỗi đó.
  • Xử lí lỗi không xác định
try:
    try:
     number = int(input("Nhập vào một số: "))
     print(number)
    except NameError as e:
        print("Dữ liệu đầu vào không chính xác: " + str(e))
except Exception as e:
    print(str(e))

Nhập vào một số: a # ta nhập chữ cái a từ bàn phím thay vì 1 chữ số
#invalid literal for int() with base 10: 'a'
Ở ví dụ trên, ta định nghĩa một except NameError để bắt khi không tìm thấy tên biến và một except Exception để xử lí lỗi không xác định ngoài lỗi đó mà ta chưa biết rõ.
  • Một mệnh đề try có thể đi kèm 1 hay nhiều except:
try:
 number = int(input("Nhập vào một số: "))
 print(number)
except NameError as e:
    print("Dữ liệu đầu vào không chính xác: " + str(e))
except ValueError:
    print("Không thể chuyển đổi dữ liệu về kiểu int.")
except:
    print("Lỗi không xác định:")
    raise
Ở đây, chúng ta khai báo 3 except là NameError với lỗi tên biến không xác định, ValueError với lỗi giá trị không chính xác và except cuối cùng sẽ được gọi đến nếu như không có ngoại lệ nào được xử lí ở trên.
  • Ta cũng có thể bắt nhiều exception cùng một lúc cách nhau bởi dấu phẩy.
try:
 number = int(input("Nhập vào một số: "))
 print(number)
except (NameError, ValueError) as e:
    print(str(e))
  • Bạn cũng có thể lồng các khối try...except lại với nhau:
try :
    # code
except ZeroDivisionError:
    try :
        # code
    except StandardError:
        # code
except RuntimeError:
    # code
  • Câu lệnh else 
Câu lệnhelse  được gọi trong trường hợp lệnh không có exception được sinh ra khi thực hiện khối lệnh trong try

Cú pháp:
try:
 #code
except:
 #code
else:
 #code
Ví dụ:
try:
 number = int(input("Nhập vào một số: "))
 print(number)
except (NameError, ValueError) as e:
    print(str(e))
else:
 print("Không có lỗi xảy ra")
  • Câu lệnh finally
Sử dụng lệnh finally Trong trường hợp lệnh trong mệnh đề try đang được thực hiện thì xảy ra lỗi, chương trình khi đó sẽ nhảy tới thực hiện lệnh trong mệnh đề except, có thể việc một lệnh nào đó nằm mệnh đề try chưa được thực hiện hết khiến cho phần mềm có thể bị lỗi sau đó. Khi đó bạn cần dùng đến finally.
try:
    # code
except:
    # code
else:
 #code
finally:
    # code
Ví dụ:
try:
 number = int(input("Nhập vào một số: "))
 print(number)
except (NameError, ValueError) as e:
    print(str(e))
finally:
 print("Chương trình đã được chạy")
Hoặc
try:
    f = open('filename','r')
    try:
        f_content = f.readline()
        i = int(f_content.strip())
    finally:
        f.close()
except IOError as e:
    print str(e)
Trong trường hợp đọc lỗi, file f vẫn sẽ được đóng lại trong finally. Chú ý sự khác biệt khi sử dụng của else và finally: lệnh trong else chỉ được gọi khi không có lỗi xảy ra, lệnh trong finally được gọi khi có lỗi xảy ra.

Tự định nghĩa Exceptions

Ngoài các exceptions được cung cấp sẵn, Python cũng cho phép lập trình viên tự định nghĩa các exceptions để sử dụng Để tạo một exception trong Python thì bắt buộc exception này phải kế thừa lớp Exception trong Python, và còn lại ta có thể tùy biến cách xử lí xử lý như thế nào cũng được. 
Ví dụ:
class ExceptionDemo(Exception):
    def __init__(self, value):
        print("Lỗi: " + value)
Sau khi đã tạo ra được exception cho riêng mình rồi, thì khi thực hiện mà bạn muốn gọi ra gọi exception ra bạn chỉ cần sử dụng keyword raise để gọi exception theo cú pháp sau:
raise exceptionName
Trong đó, exceptionName là tên của exception bạn muốn gọi. 
Ví dụ:
class ExceptionDemo(Exception):
    def __init__(self, value):
        print("Loi: " + value)

def sum(a, b):
    if (b == 0):
        raise ExceptionDemo('b phải khác 0')
    return a / b
sum(6, 0)

Các exception có sẵn trong Python

Tên exceptionmiêu tả
ExceptionĐây là lớp cơ sở cho tất cả các exception, nó sẽ xuất hiện khi có bất cứ một lỗi nào xảy ra.
StopIterationXuất hiện khi phương thức next() của iterator không trỏ đến một đối tượng nào.
SystemExitXuất hiện khi dùng phương thức sys.exit()
StandardErrorLớp cơ sở cho tất cả các exception ngoại trừ StopIteration và SystemExit.
ArithmeticErrorXuất hiện khi có lỗi tính toán giữa các số với nhau
OverflowErrorXuất hiện khi thực hiện tính toán và giá trị của nó vượt quá ngưỡng giới hạn cho phép của kiểu dữ liệu.
FloatingPointErrorXuất hiện khi tính toán float thất bại.
ZeroDivisonErrorXuất hiện khi thực hiện phép chia cho 0.
AssertionErrorXuất hiện trong trường hợp lệnh assert thất bại.
AttributeErrorXuất hiện khi không tồn tại thuộc tính này, hoặc thiếu tham số truyền vào nó.
EOFErrorXuất hiện khi không có dữ liệu từ hàm input() hoặc cuối file.
ImportErrorXuất hiện khi lệnh import thất bại.
KeyboardInterruptXuất hiện khi ngắt trình biên dịch.
LookupErrorLớp cơ sở cho tất cả các lỗi về lookup.
IndexErrorXuất hiện khi index không tồn tại trong liststring,...
KeyErrorXuất hiện khi key không tồn tại trong dictionary.
NameErrorXuất hiện khi một biến không tồn tại trong phạm vi bạn gọi nó.
EnvironmentErrorXuất hiện khi có bất kỳ một lỗi nào ngoài phạm vị của Python.
IOErrorXuất hiện khi xử dụng input/output thất bại, hoặc mở file không thành công.
OSErrorXuất hiện khi có lỗi từ hệ điều hành.
SyntaxErrorXuất hiện khi chương trình có lỗi cú pháp.
IndentationErrorXuất hiện khi bạn thụt dòng không đúng.
SystemErrorXuất hiện khi trình biên dịch có vấn đề nhưng mà nó lại không tự động exit.
SystemExitXuất hiện khi trình biên dịch được thoát bởi sys.exit().
TypeErrorXuất hiện khi thực thi toán tử hoặc hàm mà kiểu dữ liệu bị sai so với kiểu dữ liệu đã định nghĩa ban đầu.
ValueErrorXuất hiện khi chúng ta xây dựng 1 function mà kiểu dữ liệu đúng nhưng khi chúng ta thiết lập ở tham số là khác so với khi truyền vào.
RuntimeErrorXuất hiện khi lỗi được sinh ra không thuộc một danh mục nào.
NotImplementedErrorXuất hiện khi một phương thức trừu tượng cần được thực hiện trong lớp kế thừa chứ không phải là lớp thực thi
UnboundLocalErrorXuất hiện khi chúng ta cố tình truy cập vào một biến trong hàm hoặc phương thức, nhưng không thiết lập giá trị cho nó.

Ngoài ra bạn có thể tham khảo thêm các exceptions được xây dựng sẵn ở 
Built-in Exceptions và phân cấp của nó Exception hierarchy
Python Exceptions Handling Reviewed by Finn on November 14, 2018 Rating: 5

No comments:

Copyright © 0xpan's Blog 2018
Online:
Development by Nguyễn Nam Hồng + Finn