🌱 [Python] 16 - Python Exception Handling – Xử lý ngoại lệ trong Python
Trong các ngôn ngữ lập trình nói chung và python nói riêng, việc xử lý các exception là vô cùng quan trọng, mục tiêu là tránh chương trình bị crash do các lỗi trong quá trình runtime!
1 - Exception Handling, Cấu trúc try-except và finally
Trong Python, Exception Handling là cơ chế giúp chương trình không bị dừng đột ngột khi gặp lỗi, mà cho phép bạn kiểm soát và xử lý lỗi hợp lý để chương trình vẫn hoạt động ổn định.
print("Start")
x = 10 / 0
print("End")
Kết quả:
ZeroDivisionError: division by zero
→ Chương trình dừng tại lỗi chia cho 0, dòng print("End") không được thực thi. Để tránh điều đó, ta dùng try–except.
Cấu trúc try-except và finally
Cấu trúc cơ bản:
try:
# đoạn code có thể gây lỗi
except SomeException:
# xử lý lỗi
finally:
# luôn chạy dù có lỗi hay không
Ví dụ:
- try:
- num = int(input("Nhập số: "))
- print(10 / num)
- except ZeroDivisionError:
- print("Không thể chia cho 0!")
- except ValueError:
- print("Vui lòng nhập số hợp lệ!")
- finally:
- print("Kết thúc chương trình.")
Trong đoạn code trên,
- try: chứa code có khả năng gây lỗi.
- except: xử lý loại lỗi cụ thể.
- finally: luôn chạy, dù có lỗi hay không, thường dùng để đóng file, ngắt kết nối, ...
2 - Các Exception có sẵn trong Python
Python có nhiều Exception mặc định, nếu sử dụng, khi có exception tương ứng xảy ra, chương trình sẽ thực thi nhanh except tương ứng. Dưới đây là một số lỗi phổ biến:
| Exception | Ý nghĩa |
|---|---|
ZeroDivisionError |
Chia cho 0 |
ValueError |
Giá trị không hợp lệ |
TypeError |
Kiểu dữ liệu không hợp lệ |
IndexError |
Vượt quá chỉ số danh sách |
KeyError |
Key không tồn tại trong dict |
FileNotFoundError |
Không tìm thấy file |
Ví dụ:
try:
lst = [1, 2, 3]
print(lst[5])
except IndexError as e:
print("Lỗi:", e)
# Lỗi: list index out of range
3 - Tạo Exception do người dùng định nghĩa
Bạn có thể định nghĩa Exception riêng để xử lý các lỗi riêng biệt tùy mục đích người dùng.
- class NegativeNumberError(Exception):
- """Lỗi: số âm không hợp lệ"""
- pass
- def check_positive(x):
- if x < 0:
- raise NegativeNumberError("Giá trị âm không được phép!")
- print("Giá trị hợp lệ:", x)
- try:
- check_positive(-10)
- except NegativeNumberError as e:
- print("Lỗi người dùng:", e)
Output:
Lỗi người dùng: Giá trị âm không được phép!
4 - raise – chủ động ném ngoại lệ
Bạn có thể chủ động "ném lỗi" bằng lệnh raise để kiểm soát luồng chương trình.
- def divide(a, b):
- if b == 0:
- raise ZeroDivisionError("Không thể chia cho 0")
- return a / b
- try:
- print(divide(10, 0))
- except ZeroDivisionError as e:
- print("Lỗi:", e)
Output:
Lỗi: Không thể chia cho 0
5 - Thực hành: Retry & Logging
Tình huống thực tế: Gọi API hoặc xử lý dữ liệu mạng có thể thất bại tạm thời. Ta xây dựng một decorator retry tự động thử lại khi có lỗi, và ghi log mỗi lần lỗi.
- import time, random, logging
- logging.basicConfig(level=logging.INFO)
- class ConnectionError(Exception):
- pass
- def unstable_api_call():
- """Giả lập API lỗi ngẫu nhiên"""
- if random.random() < 0.7:
- raise ConnectionError("Mất kết nối API")
- return "✅ Kết nối thành công!"
- def retry(retries=3, delay=1):
- def decorator(func):
- def wrapper(*args, **kwargs):
- for i in range(retries):
- try:
- return func(*args, **kwargs)
- except Exception as e:
- logging.warning(f"Thử lần {i+1} thất bại: {e}")
- time.sleep(delay)
- raise Exception(f"Thất bại sau {retries} lần thử")
- return wrapper
- return decorator
- @retry(retries=3, delay=1)
- def call_api():
- return unstable_api_call()
- print(call_api())
Output:
WARNING:root:Thử lần 1 thất bại: Mất kết nối API
✅ Kết nối thành công!
Kiểm thử bằng Unit Test
Unit Test để kiểm tra decorator hoạt động đúng:
- import time
- import logging
- import unittest
- # -------------------------------------------------
- # Decorator: retry with exponential backoff + logging
- # -------------------------------------------------
- def retry(retries=3, delay=1, backoff=2, exceptions=(Exception,)):
- """
- Retry decorator with exponential backoff and logging.
- :param retries: number of retries
- :param delay: initial delay between retries
- :param backoff: multiplier applied to delay after each failure
- :param exceptions: tuple of exception types to catch
- """
- def decorator(func):
- def wrapper(*args, **kwargs):
- current_delay = delay
- for attempt in range(1, retries + 1):
- try:
- return func(*args, **kwargs)
- except exceptions as e:
- if attempt == retries:
- logging.error(f"All {retries} attempts failed.")
- raise
- logging.warning(f"Attempt {attempt} failed: {e}")
- time.sleep(current_delay)
- current_delay *= backoff
- return wrapper
- return decorator
- # -------------------------------------------------
- # Unit Tests
- # -------------------------------------------------
- class TestRetryDecorator(unittest.TestCase):
- def test_success_on_first_try(self):
- @retry(retries=3, delay=0.1)
- def always_succeed():
- return "success"
- self.assertEqual(always_succeed(), "success")
- def test_success_after_retries(self):
- counter = {'attempts': 0}
- @retry(retries=3, delay=0.1)
- def fail_twice_then_succeed():
- counter['attempts'] += 1
- if counter['attempts'] < 3:
- raise ValueError("Fake error")
- return "success"
- self.assertEqual(fail_twice_then_succeed(), "success")
- def test_all_retries_fail(self):
- @retry(retries=3, delay=0.1)
- def always_fail():
- raise RuntimeError("Always fail")
- with self.assertRaises(RuntimeError):
- always_fail()
- if __name__ == "__main__":
- unittest.main()
Output:
Attempt 1 failed: Always fail
Attempt 2 failed: Always fail
Attempt 3 failed: Always fail
All 3 attempts failed.
.Attempt 1 failed: Fake error
..
----------------------------------------------------------------------
Ran 3 tests in 0.001s
OK
>>>>>> Follow ngay <<<<<<<
Để nhận được những bài học miễn phí mới nhất nhé 😊
Chúc các bạn học tập tốt 😊