Bài 19: Python Best Practices
Mục Tiêu Bài Học
Sau khi hoàn thành bài này, bạn sẽ:
- ✅ Follow PEP 8 style guide
- ✅ Organize code hiệu quả
- ✅ Viết documentation (docstrings)
- ✅ Áp dụng comments best practices
- ✅ Hiểu SOLID principles
- ✅ Sử dụng design patterns
Tại Sao Cần Best Practices?
Best practices giúp:
- Readable code - Dễ đọc và maintain
- Collaboration - Team work hiệu quả
- Bug prevention - Tránh common mistakes
- Professional - Code quality cao
# ❌ Bad codedef f(x,y): return x+y # ✅ Good codedef add_numbers(first_number: int, second_number: int) -> int: """Add two numbers and return the result.""" return first_number + second_number
1. PEP 8 - Python Style Guide
PEP 8 là official style guide cho Python code.
Indentation và Spacing
# ✅ Use 4 spaces per indentation leveldef function(): if condition: do_something() do_another_thing() # ❌ Don't use tabs or inconsistent spacingdef function(): if condition: # 2 spaces do_something() # Mix tabs and spaces # ✅ Line length: max 79 characters (or 120 for modern)result = some_function( first_argument, second_argument, third_argument) # ✅ Blank linesclass MyClass: """Class docstring.""" def method_one(self): """Method docstring.""" pass def method_two(self): """Another method.""" pass # Two blank lines between top-level definitionsdef standalone_function(): """Function docstring.""" pass
Imports
# ✅ Imports at top of file, groupedimport osimport sysfrom typing import List, Dict import requestsimport numpy as np from mypackage import module1from mypackage.subpackage import module2 # ❌ Don't use wildcard importsfrom module import * # Unclear what's imported # ❌ Don't use multiple imports on one lineimport os, sys # Hard to read # ✅ One import per lineimport osimport sys # ✅ Group imports:# 1. Standard library# 2. Third-party packages# 3. Local modules
Naming Conventions
# ✅ snake_case for functions and variablesdef calculate_total_price(items): total_price = 0 return total_price # ✅ PascalCase for classesclass ShoppingCart: pass class UserAccount: pass # ✅ UPPER_CASE for constantsMAX_RETRY_COUNT = 3DEFAULT_TIMEOUT = 30API_BASE_URL = "https://api.example.com" # ✅ _single_leading_underscore for internal useclass MyClass: def __init__(self): self._internal_variable = 10 def _internal_method(self): pass # ✅ __double_leading_underscore for name manglingclass Parent: def __init__(self): self.__private = 10 # Becomes _Parent__private # ❌ Avoid single letter names (except counters)def f(x, y): # Unclear return x + y # ✅ Use descriptive namesdef calculate_distance(point1, point2): return ((point2[0] - point1[0])**2 + (point2[1] - point1[1])**2)**0.5
Whitespace
# ✅ Operatorsresult = x + yis_equal = a == bvalue = x * 2 + y * 3 # ❌ Don't add extra spacesresult = x+y # No spacesresult = x + y # Too many spaces # ✅ Function callsfunction(arg1, arg2, kwarg1=value1) # ❌ Don't add spacesfunction ( arg1 , arg2 ) # Wrongfunction(arg1,arg2) # Missing spaces # ✅ Slicingmy_list[1:5]my_list[start:end]my_list[::2] # ❌ Slicing with spacesmy_list[1 : 5] # Wrong # ✅ Trailing commas (for multi-line)items = [ 'item1', 'item2', 'item3', # Trailing comma makes diffs cleaner]
Tools để Check PEP 8
# Install toolspip install flake8 black isort # Check style with flake8flake8 mycode.py # Auto-format with blackblack mycode.py # Sort imports with isortisort mycode.py # Configure in pyproject.toml[tool.black]line-length = 88target-version = ['py38', 'py39', 'py310'] [tool.isort]profile = "black"line_length = 88
2. Code Organization
Project Structure
# ✅ Good project structuremyproject/├── README.md├── LICENSE├── setup.py├── pyproject.toml├── requirements.txt├── .gitignore├── src/│ └── myproject/│ ├── __init__.py│ ├── core/│ │ ├── __init__.py│ │ ├── engine.py│ │ └── processor.py│ ├── utils/│ │ ├── __init__.py│ │ ├── helpers.py│ │ └── validators.py│ └── config/│ ├── __init__.py│ └── settings.py├── tests/│ ├── __init__.py│ ├── test_core.py│ └── test_utils.py├── docs/│ ├── index.md│ └── api.md└── examples/ └── basic_usage.py
Module Organization
# ✅ Good module structure"""Module docstring explaining purpose. This module provides utilities for data processing.""" # Standard library importsimport osimport sysfrom typing import List, Dict # Third-party importsimport requestsimport pandas as pd # Local importsfrom myproject.utils import helper # ConstantsMAX_RETRIES = 3DEFAULT_TIMEOUT = 30 # Module-level variables_cache = {} # Classesclass DataProcessor: """Main data processor class.""" pass # Functionsdef process_data(data: List[Dict]) -> List[Dict]: """Process data and return results.""" pass # Main executionif __name__ == "__main__": # Test code pass
Function Organization
# ✅ Functions should do ONE thingdef get_user_and_send_email(user_id): # ❌ Does two things user = get_user(user_id) send_email(user.email) # ✅ Split into separate functionsdef get_user(user_id): """Get user by ID.""" # Implementation pass def send_email(email): """Send email to user.""" # Implementation pass def notify_user(user_id): """Get user and send notification.""" user = get_user(user_id) send_email(user.email) # ✅ Keep functions short (< 50 lines ideally)# ✅ Clear function names that describe what they do# ✅ Limit parameters (max 3-5)
Class Organization
# ✅ Good class structureclass UserAccount: """ User account management. Attributes: username: User's username email: User's email address """ # Class variables MAX_LOGIN_ATTEMPTS = 3 def __init__(self, username: str, email: str): """Initialize user account.""" # Public attributes self.username = username self.email = email # Protected attributes (internal use) self._login_attempts = 0 # Private attributes (name mangling) self.__password_hash = None # Public methods def login(self, password: str) -> bool: """Attempt to login user.""" pass def reset_password(self): """Reset user password.""" pass # Protected methods def _verify_password(self, password: str) -> bool: """Verify password (internal use).""" pass # Private methods def __hash_password(self, password: str) -> str: """Hash password (strongly internal).""" pass # Properties @property def is_active(self) -> bool: """Check if account is active.""" return self._login_attempts < self.MAX_LOGIN_ATTEMPTS # Magic methods def __str__(self) -> str: return f"UserAccount(username={self.username})" def __repr__(self) -> str: return f"UserAccount(username={self.username!r}, email={self.email!r})"
3. Documentation
Docstrings
# ✅ Module docstring"""User authentication module. This module provides functions and classes for user authentication,including login, logout, and session management. Example: >>> from myproject import auth >>> user = auth.login('username', 'password')""" # ✅ Class docstring (Google style)class UserAccount: """ Represents a user account in the system. This class handles user account operations including authentication, profile management, and permissions. Attributes: username (str): The user's unique username. email (str): The user's email address. created_at (datetime): Account creation timestamp. Example: >>> user = UserAccount("john_doe", "[email protected]") >>> user.login("password123") True """ def __init__(self, username: str, email: str): """ Initialize a new user account. Args: username: The unique username for this account. email: The user's email address. Raises: ValueError: If username or email is invalid. """ pass # ✅ Function docstring (Google style)def calculate_discount( price: float, discount_percent: float, max_discount: float = 100.0) -> float: """ Calculate discounted price with optional maximum discount. This function calculates the final price after applying a percentage discount, ensuring the discount doesn't exceed the maximum allowed. Args: price: Original price before discount. discount_percent: Discount percentage (0-100). max_discount: Maximum discount amount in currency units. Defaults to 100.0. Returns: Final price after applying discount. Raises: ValueError: If price is negative or discount_percent is invalid. Example: >>> calculate_discount(100, 20) 80.0 >>> calculate_discount(100, 50, max_discount=30) 70.0 """ if price < 0: raise ValueError("Price cannot be negative") if not 0 <= discount_percent <= 100: raise ValueError("Discount percent must be between 0 and 100") discount = price * (discount_percent / 100) discount = min(discount, max_discount) return price - discount # ✅ NumPy/SciPy style docstring (alternative)def process_data(data, normalize=True): """ Process input data with optional normalization. Parameters ---------- data : array_like Input data to process. normalize : bool, optional Whether to normalize data. Default is True. Returns ------- processed_data : ndarray Processed and optionally normalized data. See Also -------- normalize_data : Function to normalize data separately. Notes ----- The normalization uses min-max scaling. Examples -------- >>> data = [1, 2, 3, 4, 5] >>> process_data(data) array([0. , 0.25, 0.5 , 0.75, 1. ]) """ pass
Comments
# ✅ Good comments explain WHY, not WHAT# Bad: Add 1 to countercounter += 1 # Good: Increment counter to track processed itemscounter += 1 # ✅ Use comments for complex logicdef calculate_tax(amount, rate): """Calculate tax on amount.""" # Apply progressive tax rate based on brackets # Bracket 1: 0-10000 at 10% # Bracket 2: 10001-50000 at 20% # Bracket 3: 50001+ at 30% if amount <= 10000: return amount * 0.10 elif amount <= 50000: return 1000 + (amount - 10000) * 0.20 else: return 9000 + (amount - 50000) * 0.30 # ✅ TODO comments for future workdef process_payment(amount): """Process payment.""" # TODO: Add support for multiple currencies # TODO: Implement retry logic for failed payments pass # ✅ FIXME comments for known issuesdef calculate_total(items): """Calculate total price.""" # FIXME: This doesn't handle negative prices correctly return sum(item.price for item in items) # ❌ Don't comment obvious codex = x + 1 # Increment x by 1 (unnecessary) # ❌ Don't leave commented-out code# def old_function():# pass # Remove instead of commenting
4. Error Handling
Exception Handling Best Practices
# ✅ Catch specific exceptionstry: file = open('data.txt') data = file.read()except FileNotFoundError: print("File not found")except PermissionError: print("Permission denied")finally: file.close() # ❌ Don't catch all exceptions blindlytry: risky_operation()except: # Catches everything, even KeyboardInterrupt! pass # ✅ Use context managerswith open('data.txt') as file: data = file.read()# File automatically closed # ✅ Custom exceptionsclass ValidationError(Exception): """Raised when data validation fails.""" pass class DatabaseError(Exception): """Raised when database operation fails.""" pass def validate_email(email): if '@' not in email: raise ValidationError(f"Invalid email: {email}") # ✅ Chain exceptions for contexttry: process_data()except ValueError as e: raise DataProcessingError("Failed to process data") from e # ✅ Log exceptions properlyimport logging logger = logging.getLogger(__name__) try: risky_operation()except Exception as e: logger.exception("Operation failed") # Logs with traceback raise
Defensive Programming
# ✅ Validate inputsdef divide(a: float, b: float) -> float: """ Divide two numbers. Args: a: Dividend b: Divisor Returns: Result of division Raises: ValueError: If b is zero """ if b == 0: raise ValueError("Cannot divide by zero") return a / b # ✅ Use assertions for internal checksdef calculate_average(numbers: list) -> float: """Calculate average of numbers.""" assert len(numbers) > 0, "List cannot be empty" assert all(isinstance(n, (int, float)) for n in numbers), "All items must be numbers" return sum(numbers) / len(numbers) # ✅ Provide defaultsdef get_config(key: str, default=None): """Get configuration value with optional default.""" return config.get(key, default) # ✅ Early returns for error conditionsdef process_user(user): """Process user data.""" if not user: return None if not user.is_active: return None if not user.has_permission(): return None # Main logic here return process(user)
5. Testing Best Practices
Test Organization
# ✅ Good test structureimport unittestfrom myproject import calculator class TestCalculator(unittest.TestCase): """Test calculator functions.""" def setUp(self): """Set up test fixtures.""" self.calc = calculator.Calculator() def tearDown(self): """Clean up after tests.""" pass def test_add_positive_numbers(self): """Test adding positive numbers.""" result = self.calc.add(2, 3) self.assertEqual(result, 5) def test_add_negative_numbers(self): """Test adding negative numbers.""" result = self.calc.add(-2, -3) self.assertEqual(result, -5) def test_divide_by_zero_raises_error(self): """Test that dividing by zero raises ValueError.""" with self.assertRaises(ValueError): self.calc.divide(10, 0) # ✅ Use descriptive test names# Pattern: test_<method>_<condition>_<expected_result>def test_login_with_valid_credentials_returns_token(self): pass def test_login_with_invalid_password_raises_error(self): pass # ✅ AAA pattern: Arrange, Act, Assertdef test_user_creation(): # Arrange username = "test_user" email = "[email protected]" # Act user = User(username, email) # Assert assert user.username == username assert user.email == email
Test Coverage
# Install coveragepip install coverage pytest-cov # Run with coveragepytest --cov=myproject --cov-report=html # View reportopen htmlcov/index.html # Aim for 80-90% coverage (not 100%)# Focus on critical paths
5 Ứng Dụng Thực Tế
1. Code Quality Checker
import astimport osfrom pathlib import Pathfrom typing import List, Dict class CodeQualityChecker: """Check code quality metrics.""" def __init__(self): self.issues = [] def check_file(self, filepath: str) -> Dict[str, any]: """Check code quality of a Python file.""" with open(filepath) as f: content = f.read() tree = ast.parse(content) metrics = { 'file': filepath, 'lines': len(content.splitlines()), 'functions': 0, 'classes': 0, 'complex_functions': [], 'long_functions': [], 'missing_docstrings': [] } for node in ast.walk(tree): # Count functions and classes if isinstance(node, ast.FunctionDef): metrics['functions'] += 1 self._check_function(node, metrics) elif isinstance(node, ast.ClassDef): metrics['classes'] += 1 self._check_class(node, metrics) return metrics def _check_function(self, node: ast.FunctionDef, metrics: Dict): """Check function quality.""" # Check docstring docstring = ast.get_docstring(node) if not docstring: metrics['missing_docstrings'].append(node.name) # Check function length lines = node.end_lineno - node.lineno if lines > 50: metrics['long_functions'].append({ 'name': node.name, 'lines': lines }) # Check complexity (count if/for/while/try) complexity = sum( 1 for child in ast.walk(node) if isinstance(child, (ast.If, ast.For, ast.While, ast.Try)) ) if complexity > 10: metrics['complex_functions'].append({ 'name': node.name, 'complexity': complexity }) def _check_class(self, node: ast.ClassDef, metrics: Dict): """Check class quality.""" docstring = ast.get_docstring(node) if not docstring: metrics['missing_docstrings'].append(node.name) def check_project(self, project_path: str) -> List[Dict]: """Check all Python files in project.""" results = [] for filepath in Path(project_path).rglob("*.py"): if 'venv' in str(filepath) or '__pycache__' in str(filepath): continue metrics = self.check_file(str(filepath)) results.append(metrics) return results def print_report(self, results: List[Dict]): """Print quality report.""" print("\n" + "="*80) print("CODE QUALITY REPORT") print("="*80) total_lines = sum(r['lines'] for r in results) total_functions = sum(r['functions'] for r in results) total_classes = sum(r['classes'] for r in results) print(f"\nProject Statistics:") print(f" Total files: {len(results)}") print(f" Total lines: {total_lines}") print(f" Total functions: {total_functions}") print(f" Total classes: {total_classes}") # Issues all_missing_docs = [] all_long_functions = [] all_complex_functions = [] for result in results: all_missing_docs.extend(result['missing_docstrings']) all_long_functions.extend(result['long_functions']) all_complex_functions.extend(result['complex_functions']) if all_missing_docs: print(f"\n⚠️ Missing Docstrings: {len(all_missing_docs)}") for name in all_missing_docs[:5]: print(f" - {name}") if all_long_functions: print(f"\n⚠️ Long Functions (>50 lines): {len(all_long_functions)}") for func in all_long_functions[:5]: print(f" - {func['name']}: {func['lines']} lines") if all_complex_functions: print(f"\n⚠️ Complex Functions (complexity >10): {len(all_complex_functions)}") for func in all_complex_functions[:5]: print(f" - {func['name']}: complexity {func['complexity']}") # Sử dụngchecker = CodeQualityChecker()results = checker.check_project("./myproject")checker.print_report(results)
2. Docstring Generator
import astimport inspectfrom typing import Callable class DocstringGenerator: """Generate docstrings for functions and classes.""" def generate_function_docstring(self, func: Callable) -> str: """ Generate Google-style docstring for function. Args: func: Function to generate docstring for Returns: Generated docstring """ sig = inspect.signature(func) # Function description lines = [ f'"""', f'{func.__name__.replace("_", " ").title()}.', '', 'Args:' ] # Parameters for name, param in sig.parameters.items(): param_type = 'any' if param.annotation != inspect.Parameter.empty: param_type = param.annotation.__name__ default = '' if param.default != inspect.Parameter.empty: default = f' Defaults to {param.default}.' lines.append(f' {name} ({param_type}): Description.{default}') # Return type if sig.return_annotation != inspect.Signature.empty: return_type = sig.return_annotation.__name__ lines.extend([ '', 'Returns:', f' {return_type}: Description.' ]) # Example lines.extend([ '', 'Example:', f' >>> {func.__name__}()', ' result', '"""' ]) return '\n'.join(lines) def generate_class_docstring(self, cls: type) -> str: """ Generate Google-style docstring for class. Args: cls: Class to generate docstring for Returns: Generated docstring """ lines = [ '"""', f'{cls.__name__} class.', '', 'Brief description of the class.', '', 'Attributes:' ] # Get __init__ signature for attributes if hasattr(cls, '__init__'): sig = inspect.signature(cls.__init__) for name, param in sig.parameters.items(): if name == 'self': continue param_type = 'any' if param.annotation != inspect.Parameter.empty: param_type = param.annotation.__name__ lines.append(f' {name} ({param_type}): Description.') lines.extend([ '', 'Example:', f' >>> obj = {cls.__name__}()', ' >>> obj.method()', '"""' ]) return '\n'.join(lines) # Sử dụnggenerator = DocstringGenerator() def example_function(x: int, y: str = "default") -> bool: pass print(generator.generate_function_docstring(example_function))
3. Import Sorter
import astfrom pathlib import Pathfrom typing import List, Dict class ImportSorter: """Sort and organize imports in Python files.""" def __init__(self): self.stdlib_modules = { 'os', 'sys', 'time', 'datetime', 'json', 'logging', 'collections', 'itertools', 'functools', 'typing' } def parse_imports(self, filepath: str) -> Dict[str, List[str]]: """Parse imports from Python file.""" with open(filepath) as f: tree = ast.parse(f.read()) imports = { 'stdlib': [], 'third_party': [], 'local': [] } for node in ast.walk(tree): if isinstance(node, ast.Import): for alias in node.names: module = alias.name.split('.')[0] import_line = f"import {alias.name}" if alias.asname: import_line += f" as {alias.asname}" self._categorize_import(module, import_line, imports) elif isinstance(node, ast.ImportFrom): if node.module: module = node.module.split('.')[0] names = ', '.join(a.name for a in node.names) import_line = f"from {node.module} import {names}" self._categorize_import(module, import_line, imports) return imports def _categorize_import(self, module: str, import_line: str, imports: Dict): """Categorize import into stdlib, third-party, or local.""" if module in self.stdlib_modules: imports['stdlib'].append(import_line) elif module.startswith('.') or module.startswith('myproject'): imports['local'].append(import_line) else: imports['third_party'].append(import_line) def format_imports(self, imports: Dict[str, List[str]]) -> str: """Format imports according to PEP 8.""" sections = [] # Sort each section for key in ['stdlib', 'third_party', 'local']: if imports[key]: sorted_imports = sorted(set(imports[key])) sections.append('\n'.join(sorted_imports)) # Join sections with blank line return '\n\n'.join(sections) # Sử dụngsorter = ImportSorter()imports = sorter.parse_imports("myfile.py")formatted = sorter.format_imports(imports)print(formatted)
Bài tiếp theo: Bài 19.2: Best Practices (Phần 2) - SOLID principles, Factory Pattern! ⭐