Bài 14: Exception Handling - Xử Lý Lỗi (Phần 1)
Mục Tiêu Bài Học
Sau khi hoàn thành bài này, bạn sẽ:
- ✅ Hiểu exceptions và errors trong Python
- ✅ Sử dụng try/except để handle errors
- ✅ Làm việc với finally và else clauses
- ✅ Raise exceptions
- ✅ Biết các built-in exception types
Exceptions Là Gì?
Exception là error xảy ra khi chạy program, làm program dừng lại.
Errors vs Exceptions
# Syntax Error - code sai cú pháp# if True # SyntaxError: invalid syntax # Exception - error khi runtimenumber = int("abc") # ValueError: invalid literal for int() # ZeroDivisionErrorresult = 10 / 0 # ZeroDivisionError: division by zero # NameErrorprint(undefined_variable) # NameError: name 'undefined_variable' is not defined
Tại Sao Cần Exception Handling?
# ❌ Không handle - program crashesdef divide(a, b): return a / b result = divide(10, 0) # Crash!print("This never executes") # ✅ Handle exceptions - program continuesdef divide(a, b): try: return a / b except ZeroDivisionError: print("Cannot divide by zero!") return None result = divide(10, 0) # Handled gracefullyprint("Program continues") # This executes!
Try/Except - Basic Syntax
Simple Try/Except
# Basic syntaxtry: # Code that might raise exception number = int(input("Enter a number: ")) print(f"You entered: {number}")except: # Handle any exception print("Invalid input!") # Specific exception typetry: number = int(input("Enter a number: ")) result = 10 / numberexcept ValueError: print("Please enter a valid number!")except ZeroDivisionError: print("Cannot divide by zero!")
Multiple Exceptions
# Handle different exceptions differentlytry: numbers = [1, 2, 3] index = int(input("Enter index: ")) print(numbers[index])except ValueError: print("Please enter a valid integer!")except IndexError: print("Index out of range!") # Handle multiple exceptions the same waytry: value = int(input("Enter number: ")) result = 10 / valueexcept (ValueError, ZeroDivisionError): print("Invalid input or division by zero!") # Multiple except blockstry: file = open('data.txt', 'r') content = file.read() number = int(content) result = 100 / numberexcept FileNotFoundError: print("File not found!")except ValueError: print("File content is not a number!")except ZeroDivisionError: print("Number cannot be zero!")
Catching Exception Object
# Get exception detailstry: number = int("abc")except ValueError as e: print(f"Error occurred: {e}") print(f"Error type: {type(e)}") # Output:# Error occurred: invalid literal for int() with base 10: 'abc'# Error type: <class 'ValueError'> # Use exception infotry: result = 10 / 0except ZeroDivisionError as e: print(f"Math error: {e}") # Log error, send notification, etc. # Generic exception handlertry: # Some risky code value = risky_operation()except Exception as e: print(f"Something went wrong: {e}") print(f"Exception type: {type(e).__name__}")
else Clause
else chạy nếu không có exception trong try block.
# else clausetry: number = int(input("Enter a number: "))except ValueError: print("Invalid number!")else: # Only runs if no exception print(f"Success! You entered: {number}") result = number * 2 print(f"Double: {result}") # Real-world exampledef read_file(filename): try: file = open(filename, 'r') except FileNotFoundError: print(f"File not found: {filename}") return None else: # File opened successfully content = file.read() file.close() return content content = read_file('data.txt')if content: print(content)
finally Clause
finally luôn chạy, dù có exception hay không.
# finally always executestry: file = open('data.txt', 'r') content = file.read() print(content)except FileNotFoundError: print("File not found!")finally: print("This always executes") # Use finally for cleanupdef read_file(filename): file = None try: file = open(filename, 'r') content = file.read() return content except FileNotFoundError: print(f"File not found: {filename}") return None finally: # Cleanup - always close file if file: file.close() print("File closed") # Complete structuretry: # Try some code number = int(input("Enter number: ")) result = 10 / numberexcept ValueError: # Handle specific exception print("Invalid number!")except ZeroDivisionError: # Handle another exception print("Cannot divide by zero!")else: # Runs if no exception print(f"Result: {result}")finally: # Always runs print("Calculation complete")
Raising Exceptions
Dùng raise để tạo exceptions.
Basic raise
# Raise exception manuallydef check_age(age): if age < 0: raise ValueError("Age cannot be negative!") if age < 18: raise ValueError("Must be 18 or older!") return True try: check_age(-5)except ValueError as e: print(f"Error: {e}") # Raise without messagedef validate(value): if not value: raise ValueError return True # Re-raise exceptiontry: number = int("abc")except ValueError: print("Logging error...") raise # Re-raise the same exception
raise with Different Exception Types
# Raise different exceptionsdef divide(a, b): if not isinstance(a, (int, float)): raise TypeError("First argument must be a number") if not isinstance(b, (int, float)): raise TypeError("Second argument must be a number") if b == 0: raise ZeroDivisionError("Cannot divide by zero") return a / b try: result = divide(10, "2")except TypeError as e: print(f"Type error: {e}")except ZeroDivisionError as e: print(f"Math error: {e}") # Raise with formatted messagedef withdraw(balance, amount): if amount > balance: raise ValueError( f"Insufficient funds: " f"Balance ${balance}, requested ${amount}" ) return balance - amount
Built-in Exception Types
Common Exceptions
# ValueError - invalid valueint("abc") # ValueError # TypeError - wrong type"hello" + 5 # TypeError # ZeroDivisionError - division by zero10 / 0 # ZeroDivisionError # IndexError - invalid indexlist = [1, 2, 3]list[10] # IndexError # KeyError - invalid keydict = {"name": "Alice"}dict["age"] # KeyError # AttributeError - invalid attribute"hello".invalid_method() # AttributeError # FileNotFoundError - file doesn't existopen('nonexistent.txt') # FileNotFoundError # ImportError - cannot import moduleimport nonexistent_module # ImportError # NameError - undefined variableprint(undefined) # NameError
Exception Hierarchy
# All exceptions inherit from BaseExceptionBaseException├── Exception│ ├── ValueError│ ├── TypeError│ ├── ZeroDivisionError│ ├── IndexError│ ├── KeyError│ ├── AttributeError│ ├── FileNotFoundError│ └── ...├── KeyboardInterrupt└── SystemExit # Catch all exceptions (not recommended)try: # risky code passexcept Exception as e: # Catches most exceptions print(f"Error: {e}") # Be specific (recommended)try: # risky code passexcept ValueError: print("Value error!")except TypeError: print("Type error!")
Ví Dụ Thực Tế
1. Safe Input Function
def get_integer(prompt, min_value=None, max_value=None): """ Get integer input with validation. Args: prompt (str): Input prompt min_value (int): Minimum allowed value max_value (int): Maximum allowed value Returns: int: Valid integer """ while True: try: value = int(input(prompt)) # Check range if min_value is not None and value < min_value: print(f"Value must be at least {min_value}") continue if max_value is not None and value > max_value: print(f"Value must be at most {max_value}") continue return value except ValueError: print("Please enter a valid integer!") except KeyboardInterrupt: print("\nOperation cancelled") return None # Usageage = get_integer("Enter your age: ", min_value=0, max_value=150)if age: print(f"Age: {age}")
2. Safe File Reader
def read_file_safe(filename, encoding='utf-8'): """ Safely read file with error handling. Args: filename (str): File path encoding (str): File encoding Returns: str: File content or None if error """ try: with open(filename, 'r', encoding=encoding) as file: return file.read() except FileNotFoundError: print(f"Error: File not found - {filename}") return None except PermissionError: print(f"Error: No permission to read - {filename}") return None except UnicodeDecodeError: print(f"Error: Cannot decode file - {filename}") print(f"Try different encoding (current: {encoding})") return None except Exception as e: print(f"Unexpected error: {e}") return None # Usagecontent = read_file_safe('data.txt')if content: print(content)else: print("Failed to read file")
3. Calculator with Error Handling
def calculate(a, b, operation): """ Perform calculation with error handling. Args: a (float): First number b (float): Second number operation (str): Operation (+, -, *, /) Returns: float: Result or None if error """ try: # Validate inputs a = float(a) b = float(b) # Perform operation if operation == '+': return a + b elif operation == '-': return a - b elif operation == '*': return a * b elif operation == '/': if b == 0: raise ZeroDivisionError("Cannot divide by zero") return a / b else: raise ValueError(f"Invalid operation: {operation}") except ValueError as e: print(f"Value error: {e}") return None except ZeroDivisionError as e: print(f"Math error: {e}") return None except Exception as e: print(f"Unexpected error: {e}") return None # Usageresult = calculate(10, 5, '+')print(f"Result: {result}") # 15.0 result = calculate(10, 0, '/')print(f"Result: {result}") # None (error handled) result = calculate(10, 5, '%')print(f"Result: {result}") # None (invalid operation)
4. User Registration Validator
def validate_username(username): """Validate username.""" if len(username) < 3: raise ValueError("Username must be at least 3 characters") if len(username) > 20: raise ValueError("Username must be at most 20 characters") if not username.isalnum(): raise ValueError("Username must contain only letters and numbers") return True def validate_email(email): """Validate email.""" if '@' not in email: raise ValueError("Email must contain @") if '.' not in email.split('@')[1]: raise ValueError("Email must have valid domain") return True def validate_password(password): """Validate password.""" if len(password) < 8: raise ValueError("Password must be at least 8 characters") if not any(c.isupper() for c in password): raise ValueError("Password must contain uppercase letter") if not any(c.islower() for c in password): raise ValueError("Password must contain lowercase letter") if not any(c.isdigit() for c in password): raise ValueError("Password must contain digit") return True def register_user(username, email, password): """ Register user with validation. Returns: dict: User data or None if error """ try: # Validate all fields validate_username(username) validate_email(email) validate_password(password) # Create user user = { 'username': username, 'email': email, 'password': password # Should be hashed in real app! } print(f"User {username} registered successfully!") return user except ValueError as e: print(f"Validation error: {e}") return None # Usageuser = register_user("alice", "[email protected]", "Password123")if user: print(f"Welcome, {user['username']}!") user = register_user("ab", "invalid", "weak")# Multiple validation errors shown
5. Database Connection Mock
class DatabaseError(Exception): """Custom database exception.""" pass class ConnectionError(DatabaseError): """Connection failed.""" pass class QueryError(DatabaseError): """Query execution failed.""" pass def connect_database(host, port): """ Connect to database with error handling. Returns: bool: True if connected """ try: print(f"Connecting to {host}:{port}...") # Validate parameters if not host: raise ValueError("Host cannot be empty") if not isinstance(port, int): raise TypeError("Port must be an integer") if port < 1 or port > 65535: raise ValueError("Port must be between 1 and 65535") # Simulate connection if host == "localhost" and port == 5432: print("Connected successfully!") return True else: raise ConnectionError(f"Cannot connect to {host}:{port}") except ValueError as e: print(f"Invalid parameter: {e}") return False except TypeError as e: print(f"Type error: {e}") return False except ConnectionError as e: print(f"Connection failed: {e}") return False except Exception as e: print(f"Unexpected error: {e}") return False finally: print("Connection attempt completed") def execute_query(query): """Execute database query.""" try: if not query: raise ValueError("Query cannot be empty") # Check for dangerous operations dangerous_keywords = ['DROP', 'DELETE', 'TRUNCATE'] if any(keyword in query.upper() for keyword in dangerous_keywords): raise QueryError("Dangerous operation not allowed") print(f"Executing: {query}") print("Query successful!") return True except ValueError as e: print(f"Invalid query: {e}") return False except QueryError as e: print(f"Query error: {e}") return False # Usageif connect_database("localhost", 5432): execute_query("SELECT * FROM users") execute_query("DROP TABLE users") # Blocked!
Best Practices
# 1. Be specific with exceptions# ❌ Too broadtry: value = int("abc")except: print("Error") # ✅ Specifictry: value = int("abc")except ValueError: print("Invalid number") # 2. Don't catch everything# ❌ Catches too muchtry: # code passexcept Exception: pass # Silent failure! # ✅ Let important exceptions propagatetry: # code passexcept ValueError: # Handle expected error pass# KeyboardInterrupt, SystemExit not caught # 3. Use finally for cleanuptry: file = open('data.txt', 'r') # process filefinally: file.close() # Always close # 4. Provide helpful error messages# ❌ Not helpfulraise ValueError("Invalid") # ✅ Descriptiveraise ValueError(f"Age must be positive, got {age}") # 5. Don't use exceptions for control flow# ❌ Badtry: while True: value = list[index] index += 1except IndexError: pass # ✅ Goodfor value in list: # process value pass
Common Patterns
# Pattern 1: EAFP (Easier to Ask Forgiveness than Permission)# Pythonic way - try first, handle exceptions # EAFPtry: value = dictionary[key]except KeyError: value = default # vs LBYL (Look Before You Leap)if key in dictionary: value = dictionary[key]else: value = default # Pattern 2: Retry with limitdef retry_operation(func, max_attempts=3): """Retry operation with limit.""" for attempt in range(max_attempts): try: return func() except Exception as e: if attempt == max_attempts - 1: raise # Last attempt, re-raise print(f"Attempt {attempt + 1} failed, retrying...") # Pattern 3: Context-specific exceptionsdef process_data(data): """Process data with specific exceptions.""" try: # Validate if not data: raise ValueError("Data is empty") # Process result = complex_operation(data) # Return return result except ValueError: # Handle validation errors return None except Exception as e: # Log unexpected errors log_error(e) raise
Bài Tập Thực Hành
Bài 1: Safe Division
Viết function safe_divide(a, b):
- Handle ZeroDivisionError
- Handle TypeError for non-numeric inputs
- Return result or None with error message
Bài 2: File Processor
Viết function process_file(filename):
- Read file
- Handle FileNotFoundError, PermissionError
- Count lines, words, characters
- Use finally to ensure cleanup
Bài 3: Input Validator
Viết class InputValidator:
- Method
get_int()- get integer with retry - Method
get_float()- get float with retry - Method
get_choice()- get choice from list - Handle all input errors gracefully
Bài 4: Safe JSON Parser
Viết function parse_json_safe(json_string):
- Parse JSON string
- Handle JSONDecodeError
- Return parsed data or None
- Provide detailed error messages
Bài 5: Retry Decorator
Viết decorator retry(max_attempts=3):
- Retry function on exception
- Limit number of attempts
- Log each attempt
- Re-raise on final failure
Tóm Tắt
✅ Exception: Error xảy ra khi runtime
✅ try/except: Bắt và xử lý exceptions
✅ else: Chạy nếu không có exception
✅ finally: Luôn chạy (cleanup)
✅ raise: Tạo exception manually
✅ Exception types: ValueError, TypeError, ZeroDivisionError, etc.
✅ Best practice: Be specific, don't catch all, use finally
Bài Tiếp Theo
Remember:
- Be specific with exception types!
- Use finally for cleanup
- Don't silence exceptions without good reason
- Provide helpful error messages
- EAFP is Pythonic!