Bài 10.3. Python - Ngoại lệ

Bài 10.3. Python - Ngoại lệ
Python sử dụng các đối tượng đặc biệt gọi là ngoại lệ để quản lý các lỗi phát sinh trong quá trình thực thi chương trình. Bất cứ khi nào xảy ra lỗi khiến Python không biết phải làm gì tiếp theo, nó tạo ra một đối tượng ngoại lệ. Nếu bạn viết mã xử lý ngoại lệ, chương trình sẽ tiếp tục chạy. Nếu bạn không xử lý ngoại lệ, chương trình sẽ dừng và hiển thị một traceback, bao gồm báo cáo về ngoại lệ đã được tạo ra.
Ngoại lệ được xử lý bằng các khối try-except. Một khối try-except yêu cầu Python thực hiện một số mã, nhưng cũng cho Python biết phải làm gì nếu xảy ra ngoại lệ. Khi bạn sử dụng các khối try-except, chương trình của bạn sẽ tiếp tục chạy ngay cả khi có sự cố xảy ra. Thay vì các traceback, người dùng sẽ thấy các thông báo lỗi thân thiện mà bạn đã viết.
Xử lý ngoại lệ ZeroDivisionError
Hãy xem xét một lỗi đơn giản khiến Python tạo ra một ngoại lệ. Bạn có thể biết rằng không thể chia một số cho 0, nhưng hãy yêu cầu Python thực hiện điều đó:
print(5/0)
Python không thể thực hiện điều này, vì vậy chúng ta nhận được một traceback:
Traceback (most recent call last):
File "division_calculator.py", line 1, in <module>
print(5/0)
ZeroDivisionError: division by zero
Lỗi được báo cáo trong traceback, ZeroDivisionError, là một đối tượng ngoại lệ. Python tạo ra loại đối tượng này để phản hồi lại tình huống mà nó không thể thực hiện yêu cầu của chúng ta. Khi điều này xảy ra, Python dừng chương trình và cho chúng ta biết loại ngoại lệ đã được tạo ra. Chúng ta có thể sử dụng thông tin này để sửa đổi chương trình của mình. Chúng ta sẽ cho Python biết phải làm gì khi xảy ra loại ngoại lệ này; bằng cách đó, nếu nó xảy ra lần nữa, chúng ta sẽ chuẩn bị sẵn sàng.
Sử dụng các khối try-except
Khi bạn nghĩ rằng có thể xảy ra lỗi, bạn có thể viết một khối try-except để xử lý ngoại lệ có thể được tạo ra. Bạn yêu cầu Python thử chạy một số mã, và bạn cho nó biết phải làm gì nếu mã đó dẫn đến một loại ngoại lệ cụ thể.
Dưới đây là một khối try-except để xử lý ngoại lệ ZeroDivisionError:
try:
print(5/0)
except ZeroDivisionError:
print("You can't divide by zero!")
Chúng ta đặt print(5/0)
, dòng mã gây ra lỗi, bên trong một khối try. Nếu mã trong khối try hoạt động, Python bỏ qua khối except. Nếu mã trong khối try gây ra lỗi, Python tìm kiếm một khối except có lỗi khớp với lỗi đã được tạo ra và chạy mã trong khối đó.
Trong ví dụ này, mã trong khối try tạo ra một ngoại lệ ZeroDivisionError, vì vậy Python tìm kiếm một khối except cho biết cách phản hồi. Python sau đó chạy mã trong khối đó, và người dùng thấy một thông báo lỗi thân thiện thay vì một traceback:
You can't divide by zero!
Nếu có thêm mã sau khối try-except, chương trình sẽ tiếp tục chạy vì chúng ta đã cho Python biết cách xử lý lỗi. Hãy xem xét một ví dụ trong đó việc bắt lỗi có thể cho phép chương trình tiếp tục chạy.
Sử dụng ngoại lệ để ngăn chặn sự cố
Xử lý lỗi đúng cách đặc biệt quan trọng khi chương trình có nhiều công việc phải làm sau khi xảy ra lỗi. Điều này thường xảy ra trong các chương trình yêu cầu người dùng nhập dữ liệu. Nếu chương trình phản hồi đúng cách với đầu vào không hợp lệ, nó có thể yêu cầu thêm đầu vào hợp lệ thay vì bị sập.
Hãy tạo một máy tính đơn giản chỉ thực hiện phép chia:
print("Give me two numbers, and I'll divide them.")
print("Enter 'q' to quit.")
while True:
first_number = input("\nFirst number: ")
if first_number == 'q':
break
second_number = input("Second number: ")
if second_number == 'q':
break
answer = int(first_number) / int(second_number)
print(answer)
Chương trình này yêu cầu người dùng nhập first_number
và, nếu người dùng không nhập 'q' để thoát, nhập second_number
. Sau đó, chúng ta chia hai số này để có được kết quả. Chương trình này không làm gì để xử lý lỗi, vì vậy yêu cầu nó chia cho 0 sẽ khiến nó bị sập:
Give me two numbers, and I'll divide them.
Enter 'q' to quit.
First number: 5
Second number: 0
Traceback (most recent call last):
File "division_calculator.py", line 11, in <module>
answer = int(first_number) / int(second_number)
ZeroDivisionError: division by zero
Thật tệ khi chương trình bị sập, nhưng cũng không phải là ý tưởng tốt để cho người dùng thấy các traceback. Người dùng không kỹ thuật sẽ bị nhầm lẫn bởi chúng, và trong một môi trường độc hại, kẻ tấn công sẽ biết nhiều hơn bạn muốn. Ví dụ, họ sẽ biết tên của tệp chương trình của bạn, và họ sẽ thấy một phần mã của bạn không hoạt động đúng. Một kẻ tấn công có kỹ năng có thể sử dụng thông tin này để xác định loại tấn công nào cần sử dụng chống lại mã của bạn.
Khối else
Chúng ta có thể làm cho chương trình này chống lỗi hơn bằng cách đặt dòng mã có thể gây ra lỗi trong một khối try-except. Lỗi xảy ra trên dòng thực hiện phép chia, vì vậy đó là nơi chúng ta sẽ đặt khối try-except. Ví dụ này cũng bao gồm một khối else. Bất kỳ mã nào phụ thuộc vào việc khối try thực thi thành công sẽ được đặt trong khối else:
while True:
first_number = input("\nFirst number: ")
if first_number == 'q':
break
second_number = input("Second number: ")
if second_number == 'q':
break
try:
answer = int(first_number) / int(second_number)
except ZeroDivisionError:
print("You can't divide by 0!")
else:
print(answer)
Chúng ta yêu cầu Python thử hoàn thành phép chia trong một khối try, bao gồm chỉ mã có thể gây ra lỗi. Bất kỳ mã nào phụ thuộc vào việc khối try thành công sẽ được thêm vào khối else. Trong trường hợp này, nếu phép chia thành công, chúng ta sử dụng khối else để in kết quả.
Khối except cho Python biết cách phản hồi khi xảy ra ngoại lệ ZeroDivisionError. Nếu khối try không thành công do lỗi chia cho 0, chúng ta in ra một thông báo thân thiện cho người dùng biết cách tránh lỗi này. Chương trình tiếp tục chạy, và người dùng không bao giờ thấy một traceback:
Give me two numbers, and I'll divide them.
Enter 'q' to quit.
First number: 5
Second number: 0
You can't divide by 0!
First number: 5
Second number: 2
2.5
First number: q
Chỉ mã có thể gây ra ngoại lệ mới nên được đặt trong khối try. Đôi khi bạn sẽ có thêm mã chỉ nên chạy nếu khối try thành công; mã này sẽ được đặt trong khối else. Khối except cho Python biết phải làm gì trong trường hợp xảy ra ngoại lệ khi nó cố gắng chạy mã trong khối try.
Bằng cách dự đoán các nguồn lỗi có thể xảy ra, bạn có thể viết các chương trình mạnh mẽ tiếp tục chạy ngay cả khi gặp phải dữ liệu không hợp lệ và tài nguyên bị thiếu. Mã của bạn sẽ chống lại các lỗi vô tình của người dùng và các cuộc tấn công độc hại.
Xử lý ngoại lệ FileNotFoundError
Một vấn đề phổ biến khi làm việc với tệp là xử lý các tệp bị thiếu. Tệp bạn đang tìm có thể ở một vị trí khác, tên tệp có thể bị sai chính tả, hoặc tệp có thể không tồn tại. Bạn có thể xử lý tất cả các tình huống này bằng một khối try-except.
Hãy thử đọc một tệp không tồn tại. Chương trình sau đây cố gắng đọc nội dung của Alice in Wonderland, nhưng tôi chưa lưu tệp alice.txt
trong cùng thư mục với alice.py
:
from pathlib import Path
path = Path('alice.txt')
contents = path.read_text(encoding='utf-8')
Lưu ý rằng chúng ta đang sử dụng read_text()
theo cách hơi khác so với những gì bạn đã thấy trước đó. Đối số encoding
là cần thiết khi mã hóa mặc định của hệ thống của bạn không khớp với mã hóa của tệp đang được đọc. Điều này có khả năng xảy ra nhất khi đọc từ một tệp không được tạo trên hệ thống của bạn.
Python không thể đọc từ một tệp bị thiếu, vì vậy nó tạo ra một ngoại lệ:
Traceback (most recent call last):
File "alice.py", line 4, in <module>
contents = path.read_text(encoding='utf-8')
FileNotFoundError: [Errno 2] No such file or directory: 'alice.txt'
Đây là một traceback dài hơn so với những gì chúng ta đã thấy trước đó, vì vậy hãy xem cách bạn có thể hiểu các traceback phức tạp hơn. Thường thì tốt nhất là bắt đầu từ cuối cùng của traceback. Trên dòng cuối cùng, chúng ta có thể thấy rằng một ngoại lệ FileNotFoundError đã được tạo ra. Điều này quan trọng vì nó cho chúng ta biết loại ngoại lệ nào cần sử dụng trong khối except mà chúng ta sẽ viết.
Nhìn lại gần đầu của traceback, chúng ta có thể thấy rằng lỗi xảy ra ở dòng 4 trong tệp alice.py
. Dòng tiếp theo hiển thị dòng mã gây ra lỗi. Phần còn lại của traceback hiển thị một số mã từ các thư viện liên quan đến việc mở và đọc từ các tệp. Bạn thường không cần đọc hoặc hiểu tất cả các dòng này trong một traceback.
Để xử lý lỗi đang được tạo ra, khối try sẽ bắt đầu với dòng được xác định là có vấn đề trong traceback. Trong ví dụ của chúng ta, đây là dòng chứa read_text()
:
from pathlib import Path
path = Path('alice.txt')
try:
contents = path.read_text(encoding='utf-8')
except FileNotFoundError:
print(f"Sorry, the file {path} does not exist.")
Trong ví dụ này, mã trong khối try tạo ra một ngoại lệ FileNotFoundError, vì vậy chúng ta viết một khối except khớp với lỗi đó. Python sau đó chạy mã trong khối đó khi tệp không thể được tìm thấy, và kết quả là một thông báo lỗi thân thiện thay vì một traceback:
Sorry, the file alice.txt does not exist.
Chương trình không có gì thêm để làm nếu tệp không tồn tại, vì vậy đây là tất cả đầu ra mà chúng ta thấy. Hãy xây dựng trên ví dụ này và xem cách xử lý ngoại lệ có thể giúp khi bạn làm việc với nhiều tệp hơn.
Phân tích văn bản
Bạn có thể phân tích các tệp văn bản chứa toàn bộ sách. Nhiều tác phẩm văn học cổ điển có sẵn dưới dạng tệp văn bản đơn giản vì chúng thuộc phạm vi công cộng. Các văn bản được sử dụng trong phần này đến từ Project Gutenberg (https://gutenberg.org). Project Gutenberg duy trì một bộ sưu tập các tác phẩm văn học có sẵn trong phạm vi công cộng, và đó là một tài nguyên tuyệt vời nếu bạn quan tâm đến việc làm việc với các văn bản văn học trong các dự án lập trình của mình.
Hãy lấy văn bản của Alice in Wonderland và cố gắng đếm số từ trong văn bản. Để làm điều này, chúng ta sẽ sử dụng phương thức chuỗi split()
, mặc định chia một chuỗi bất cứ nơi nào nó tìm thấy bất kỳ khoảng trắng nào:
from pathlib import Path
path = Path('alice.txt')
try:
contents = path.read_text(encoding='utf-8')
except FileNotFoundError:
print(f"Sorry, the file {path} does not exist.")
else:
# Đếm số từ xấp xỉ trong tệp:
words = contents.split()
num_words = len(words)
print(f"The file {path} has about {num_words} words.")
Tôi đã di chuyển tệp alice.txt
vào đúng thư mục, vì vậy khối try sẽ hoạt động lần này. Chúng ta lấy chuỗi contents
, hiện chứa toàn bộ văn bản của Alice in Wonderland dưới dạng một chuỗi dài, và sử dụng split()
để tạo ra một danh sách tất cả các từ trong cuốn sách. Sử dụng len()
trên danh sách này cho chúng ta một ước lượng tốt về số từ trong văn bản gốc. Cuối cùng, chúng ta in ra một câu báo cáo số từ đã tìm thấy trong tệp. Mã này được đặt trong khối else vì nó chỉ hoạt động nếu mã trong khối try được thực thi thành công.
Đầu ra cho chúng ta biết số từ trong tệp alice.txt
:
The file alice.txt has about 29594 words.
Số đếm hơi cao vì thông tin bổ sung được cung cấp bởi nhà xuất bản trong tệp văn bản được sử dụng ở đây, nhưng đó là một ước lượng tốt về độ dài của Alice in Wonderland.
Làm việc với nhiều tệp
Hãy thêm nhiều sách hơn để phân tích, nhưng trước khi làm điều đó, hãy di chuyển phần lớn chương trình này vào một hàm gọi là count_words()
. Điều này sẽ làm cho việc chạy phân tích cho nhiều sách dễ dàng hơn:
from pathlib import Path
def count_words(path):
"""Đếm số từ xấp xỉ trong một tệp."""
try:
contents = path.read_text(encoding='utf-8')
except FileNotFoundError:
print(f"Sorry, the file {path} does not exist.")
else:
# Đếm số từ xấp xỉ trong tệp:
words = contents.split()
num_words = len(words)
print(f"The file {path} has about {num_words} words.")
path = Path('alice.txt')
count_words(path)
Hầu hết mã này không thay đổi. Nó chỉ được thụt lề và di chuyển vào thân của count_words()
. Đó là một thói quen tốt để giữ các nhận xét cập nhật khi bạn đang sửa đổi một chương trình, vì vậy nhận xét cũng đã được thay đổi thành một docstring và được viết lại một chút.
Bây giờ chúng ta có thể viết một vòng lặp ngắn để đếm số từ trong bất kỳ văn bản nào chúng ta muốn phân tích. Chúng ta làm điều này bằng cách lưu trữ tên của các tệp mà chúng ta muốn phân tích trong một danh sách, và sau đó gọi count_words()
cho mỗi tệp trong danh sách. Chúng ta sẽ cố gắng đếm số từ cho Alice in Wonderland, Siddhartha, Moby Dick và Little Women, tất cả đều có sẵn trong phạm vi công cộng. Tôi đã cố tình để lại tệp siddhartha.txt
ra khỏi thư mục chứa word_count.py
, vì vậy chúng ta có thể thấy chương trình của chúng ta xử lý một tệp bị thiếu như thế nào:
from pathlib import Path
def count_words(filename):
"""Đếm số từ xấp xỉ trong một tệp."""
try:
contents = path.read_text(encoding='utf-8')
except FileNotFoundError:
print(f"Sorry, the file {path} does not exist.")
else:
# Đếm số từ xấp xỉ trong tệp:
words = contents.split()
num_words = len(words)
print(f"The file {path} has about {num_words} words.")
filenames = ['alice.txt', 'siddhartha.txt', 'moby_dick.txt', 'little_women.txt']
for filename in filenames:
path = Path(filename)
count_words(path)
Tên của các tệp được lưu trữ dưới dạng các chuỗi đơn giản. Mỗi chuỗi sau đó được chuyển đổi thành một đối tượng Path
trước khi gọi count_words()
. Tệp siddhartha.txt
bị thiếu không ảnh hưởng đến việc thực thi phần còn lại của chương trình:
The file alice.txt has about 29594 words.
Sorry, the file siddhartha.txt does not exist.
The file moby_dick.txt has about 215864 words.
The file little_women.txt has about 189142 words.
Sử dụng khối try-except trong ví dụ này mang lại hai lợi ích đáng kể. Chúng ta ngăn người dùng của mình không thấy một traceback, và chúng ta cho phép chương
Comments