Tư duy Tính toán

Ngoại lệ & Gỡ lỗi
(Exception & Debugging)

Trường ĐH Công nghệ – Đại học Quốc gia Hà Nội

Nội dung

  1. Lỗi cú pháp (Syntax error)
  2. Ngoại lệ (Exception)
  3. Xử lý ngoại lệ với if/try/except
  4. Ngoại lệ không được xử lý (Unhandled exception); Dọn dẹp với finally
  5. Ném ngoại lệ với raise
  6. Assertion
  7. Gỡ lỗi (Debugging)

1.
Lỗi cú pháp

Lỗi cú pháp (Syntax error)

  • Xảy ra khi mã vi phạm ngữ pháp của Python.
  • Được phát hiện bởi parser trước khi chương trình chạy.
while True
	print("Hello world")

File "script.py", line 1
    while True
              ^
SyntaxError: expected ':'

Lỗi cú pháp: thiếu dấu :

  • Chỉ cần thiếu một ký hiệu cũng khiến chương trình không thể chạy.
while True
	print("Hello world")

Thiếu dấu hai chấm (:) sau while True.

Hàm kết thúc thực thi như thế nào?

Nếu một hàm đúng cú pháp, nó “kết thúc” theo cách nào?

Hai cách kết thúc thực thi

Kiểu kết thúc Dấu hiệu Ý nghĩa
✅ Bình thường return Trả về một giá trị; nếu không có return thì tự trả về None.
⚠️ Bất thường Exception Hàm dừng trước khi chạy hết; ngoại lệ được “ném” (một số ngôn ngữ gọi là “throw”).

2.
Ngoại lệ
(Exception)

Ngoại lệ là gì?

  • Ngoại lệ là lỗi được phát hiện khi đang chạy.
  • Nó làm gián đoạn luồng thực thi bình thường của chương trình.
  • Ví dụ: ValueError, ZeroDivisionError, TypeError, FileNotFoundError.

Các loại ngoại lệ phổ biến

Python có nhiều ngoại lệ dựng sẵn để mô tả các lỗi runtime thường gặp.

Ngoại lệ Khi nào thường gặp Gợi ý
ZeroDivisionError Chia cho 0 ⚠️ Kiểm tra mẫu số trước khi chia
IndexError Chỉ số vượt phạm vi danh sách ✅ Kiểm tra 0 ≤ i < len(lst)
ValueError Giá trị không hợp lệ cho một thao tác ⚠️ Xác thực đầu vào
FileNotFoundError Không tìm thấy tệp ✅ Kiểm tra đường dẫn / quyền truy cập
TypeError Thao tác trên kiểu dữ liệu không tương thích ⚠️ Kiểm tra kiểu/ép kiểu hợp lệ

Có thể tra thêm trong tài liệu Python.

Ví dụ: chia cho 0

Khi mẫu số bằng 0, phép chia sẽ gây ngoại lệ.


def divide(a, b):
    """
    Divide a by b and return the result.
    Raises an exception if b is zero.
    """
    return a / b

res = divide(10, 0)
print(res)

Điều gì xảy ra khi chạy?

  • Lời gọi divide(10, 0) sẽ làm chương trình dừng vì ngoại lệ.
  • Điển hình là ZeroDivisionError.

Câu hỏi

code này có thể lỗi gì?

import random
the_num = random.randint(1, 100)
guess = input('What number am I thinking of?')
num = int(guess)
if num == the_num:
	  print('Correct!')   
else:
	  print('Sorry, that was not it.')

3.
Xử lý ngoại lệ
if / try-except

Dùng if để chặn lỗi

def divide(a, b):
    if b == 0:
        print("Error: Cannot divide by zero.")
        return None
    return a / b

res = divide(10, 0)
print(res)
  • if b == 0 kiểm tra điều kiện và tránh phép chia không hợp lệ a / b.

if là không đủ?

import random
the_num = random.randint(1, 100)
guess = input('What number am I thinking of?')
if is_int(guess):
    num = int(guess)
    if num == the_num:
        print('Correct!')
    else:
        print('Sorry, that was not it.')
else:
    print('Sorry, you did not enter a number')
  • Sẽ “đẹp” nếu có is_int(), nhưng Python không có sẵn hàm này.
  • Ta cần cách tổng quát hơn: try/except.

try/except

import random
the_num = random.randint(1, 100)
guess = input('What number am I thinking of?')
try:
    num = int(guess)
    if num == the_num:
        print('Correct!')
    else:
        print('Sorry, that was not it.')
except:
    print('Sorry, you did not enter a number')
  • Khi có lỗi trong try, phần còn lại của try bị bỏ qua, và chạy except.

Câu hỏi nhanh (iClicker)

Output là gì?

  • A. Prints None
  • B. Prints 'Out of bounds'
  • C. Prints 'Will this run?' rồi 'Out of bounds'
  • D. Prints 'Out of bounds' rồi 'Will this run?'
  • E. Prints 'IndexError'

def get_item(lst, idx):
    try:
        return lst[idx]
    except:
        return 'Out of bounds'

print('Will this run?')
print(get_item([1, 2, 3], 5))

Cú pháp try/except cơ bản

try: 
    <statements>
except:
    <statements>
  • try: đoạn có thể phát sinh lỗi.
  • except: đoạn xử lý khi lỗi xảy ra.

Ví dụ: đầu vào hợp lệ

  • Input là 5 → không có ngoại lệ → bỏ qua except.
try:
    guess = input("Input an int: ")
    num = int(guess)
    print('The number is ', num)
except:
    print("Error! it's not an int")
print("Done.")
Input an int: 5
The number is  5
Done.

Ví dụ: đầu vào không hợp lệ

  • Input là abc → lỗi trong try → chạy except.
try:
    guess = input("Input an int: ")
    num = int(guess)
    print('The number is ', num)
except:
    print("Error! it's not an int")
print("Done.")
Input an int: abc
Error! it's not an int
Done.

Bắt theo loại ngoại lệ

  • Nếu ngoại lệ xảy ra trong try, Python sẽ chạy except phù hợp.
  • Nếu không khớp, có thể rơi vào except: mặc định (nếu có).
try: 
    guess = input('?')
    num = int(guess)
except ValueError:
    print('Not an int')
except TypeError:
    print()
except:
    print('Sorry')

Không có except mặc định

  • Nếu không có nhánh nào khớp và cũng không có nhánh mặc định → lỗi sẽ không được xử lý.
  • Ngoại lệ sẽ lan truyền (propagate) và có thể làm chương trình dừng.
try: 
    guess = input('?')
    num = int(guess)
except ValueError:
    print('Not an int')
except TypeError:
    print()

4.
Ngoại lệ không được xử lý
và finally

Ngoại lệ lan truyền (Propagation)

  • Nếu không được bắt, ngoại lệ sẽ lan truyền theo call stack.
  • finally giúp đảm bảo dọn dẹp luôn chạy.

Ngoại lệ không được xử lý (Unhandled)

  • Có thể xảy ra khi:
  • ❌ Không có try.
  • ❌ Không có except cho đúng loại ngoại lệ.
  • ❌ Không có except mặc định.
  • Ngoại lệ sẽ được “đẩy lên” frame kế tiếp trên call stack để tìm nơi bắt.
  • Nếu không nơi nào bắt → chương trình kết thúc với lỗi (crash).

Ví dụ: ngoại lệ bị “đẩy lên”

from typing import IO

def str_to_int(s):
    return int(s)

def func1():
    try:
        num = str_to_int('apple')
        print(num + 1)
    except IOError:
        print('Not a number')

func1()
  • int('apple') gây ValueError, nhưng ta lại chỉ bắt IOError.

Dọn dẹp với finally

try: 
    <Statements>
except <exn-name>:
    <statements>
…
except:
    <statements>
finally:
    <statements>
  • finally giúp đảm bảo các thao tác dọn dẹp luôn chạy.

Ví dụ: finally luôn chạy

def divide(a, b):
    try:
        result = a/b;
        print("Result is ", result)
    except ZeroDivisionError:
        print("Error: Cannot divide by zero.")
    finally:
         print("Cleaning up and completing")

res = divide(10, 0)
print(res)
Error: Cannot divide by zero.
Cleaning up and completing
None

Khi nào dùng finally?

  • Khối finally luôn chạy:
    • ✅ Dù try có lỗi hay không.
    • ✅ Dù ngoại lệ có được bắt hay không.
  • Dùng để giải phóng tài nguyên:
    • Đóng file, ngắt kết nối mạng, giải phóng bộ nhớ/lock…

5.
Ném ngoại lệ
raise

Ném ngoại lệ với raise

  • Ngoại lệ có thể do Python tự ném ra, hoặc do ta chủ động ném ra.
  • Ví dụ: num = int('apple')ValueError.

Ngoại lệ đến từ đâu?

  • Python ném ngoại lệ khi có vấn đề (đầu vào sai, không tìm thấy file, …).
  • Trong ví dụ trên, hàm dựng sẵn int() là “người ném” ngoại lệ.
  • Ta cũng có thể ném ngoại lệ trong hàm do mình định nghĩa.

Vì sao cần raise?

  • Để báo hiệu đầu vào không hợp lệ hoặc lỗi logic.
  • Dừng thực thi khi có vấn đề quan trọng.
  • Ép việc xử lý lỗi trở nên rõ ràng và có chủ đích.

Ví dụ: raise ValueError

def check_password(pwd):
    if len(pwd) < 6:
        raise ValueError("Password must be at least 6 characters long.")
    if pwd.isalpha() or pwd.isdigit():
        raise ValueError("Password must contain both letters and numbers.")
    print("Password is valid!")

try:
    check_password("abc12")
except ValueError as e:
    print("Invalid password:", e)

6.
Assertion
bắt lỗi sớm

Assertion là gì?

  • Công cụ chuyên biệt để phát hiện và xử lý lỗi sớm.
  • Ném ngoại lệ nếu điều kiện không thỏa.
  • Mục đích: phát hiện lỗi của lập trình viên, debug logic trong quá trình phát triển.
  • Thường dùng: kiểm thử (assert equals), debug (assert statement).

Ví dụ: lỗi do lập trình viên

def greet(name):
    print("Hello," + name)

for n in ["Roy", 6]:
    greet(n)
  • "Hello," + 6 gây lỗi kiểu dữ liệu → đây là lỗi do người viết code.

Vì sao khó debug?

  • Lỗi xuất hiện vì vi phạm tiền điều kiện (precondition) của hàm.
  • Thông báo lỗi đôi khi không “nói rõ” ý định của người viết → càng lớn càng khó tìm.
  • Giải pháp: dùng assert để báo lỗi sớm và rõ ràng.

Dùng assert để kiểm tra tiền điều kiện

def greet(name):
    assert isinstance(name, str), \
         "name must be a string but " \
         + str(name) + " is not" 
    print("Hello," + name)

for n in ["Roy", 6]:
    greet(n)
  • Thông báo lỗi hữu ích hơn cho lập trình viên khi debug.

Assertion cũng là ngoại lệ

  • AssertionError là một loại ngoại lệ.
  • assert là cách “nâng cấp” để ném AssertionError.
  • Có thể tắt assertions khi chạy:
  • Python -0 script.py (chạy script nhưng bỏ qua các assert).
  • Dùng assertions để phát hiện sai, không phải để “bảo đảm đúng”.

7.
Gỡ lỗi
(Debugging)

Bug & Debugging

  • Bug: sai sót/lỗi khiến hành vi bất ngờ.
  • Thường do logic sai, triển khai sai, giả định sai, …
  • Debugging: quá trình xác định, cô lập và sửa bug.

Cách tư duy khi debug

Đừng hỏi:

“Vì sao code của tôi không làm điều tôi muốn?”

Hãy hỏi:

“Code của tôi đang làm gì?”

Hai cách quan sát chương trình

  1. Bước qua từng dòng, vẽ sơ đồ thực thi hoặc dùng Python tutor.
  2. Dùng print/logging để in thông tin quan trọng:
  • Giá trị biến trung gian
  • Đầu vào/đầu ra của hàm

Ví dụ: debug nhanh bằng print

Mẹo: dùng repr() để nhìn “dạng lập trình viên”.

def last_name_first(full_name):
   print('DEBUG: full_name =' + repr(full_name))
   space_index = full_name.index(' ')
   print('DEBUG: space_index =' + repr(space_index))
   first = full_name[:space_index]
   print('DEBUG: first =' + repr(first))
   last = full_name[space_index + 1:]
   print('DEBUG: last =' + repr(last))
   return_value = last + ", " + first
   print('DEBUG: return_value = ' + repr(return_value))
   return return_value

last_name_first("Toni Marrison")

Từ print ra bằng chứng

  • Giờ ta thấy code đang làm gì.
  • Ta có bằng chứng để khoanh vùng dòng “sai”.

repr() là gì?

  • repr(): trả về biểu diễn chuỗi “chuẩn” (canonical) của một giá trị.
  • Hữu ích cho lập trình viên khi debug, không nhằm hiển thị cho người dùng cuối.

print để debug: ưu/nhược

Tiêu chí print logging
Dùng nhanh ✅ Rất nhanh, đơn giản ⚠️ Cần cấu hình ban đầu
Quy mô lớn ❌ Dễ làm “bẩn” output ✅ Linh hoạt, có mức log
Kiểm soát ❌ Khó bật/tắt có chọn lọc ✅ Điều chỉnh theo level (INFO, DEBUG…)

Khi dự án lớn, nên ưu tiên logging.

logging: ví dụ

import logging

logging.basicConfig( level=logging.DEBUG, format="%(levelname)s: %(message)s")

def last_name_first(full_name):
    logging.debug('full_name = %r', full_name)
    space_index = full_name.index(' ')
    logging.debug('space_index = %r', space_index)
    first = full_name[:space_index]
    logging.debug('first = %r', first)
    last = full_name[space_index + 1:]
    logging.debug('last = %r', last)
    return_value = last + ", " + first
    logging.debug('return_value = %r', return_value)
    return return_value

logging: vì sao nên dùng?

  • Hệ thống ghi nhận sự kiện có cấu trúc và dễ kiểm soát.
  • Dễ điều chỉnh: bật/tắt theo mức (logging.INFO, logging.ERROR, logging.DEBUG).
  • Định dạng phong phú: timestamp, level, module name, …
  • Phù hợp quy mô production: mở rộng được, cấu hình được.

Xem thêm: Python logging documentation

Tóm tắt

  • Ngoại lệ làm gián đoạn luồng thực thi bình thường.
  • Dùng try/except để xử lý lỗi “êm” và giữ chương trình chạy.
  • Thêm finally để đảm bảo dọn dẹp luôn diễn ra.
  • Dùng raise để báo lỗi có chủ đích khi input/logic không hợp lệ.
  • Gỡ lỗi bằng print hoặc logging để quan sát hành vi chương trình.