Bài 15: Python Package Structure
Mục Tiêu Bài Học
Sau khi hoàn thành bài này, bạn sẽ:
- ✅ Hiểu init.py
- ✅ Sử dụng relative vs absolute imports
- ✅ Tạo packages
- ✅ Làm việc với setup.py và pyproject.toml
- ✅ Distribute packages
- ✅ Áp dụng best practices
Package vs Module
Module = Single Python file (.py)
Package = Directory chứa modules với __init__.py
# Module: utils.pydef hello(): return "Hello!" # Package structuremypackage/├── __init__.py # Làm directory thành package├── module1.py└── module2.py
1. __init__.py - Package Initializer
Empty __init__.py
# mypackage/__init__.py# File rỗng - package vẫn hoạt động # Sử dụngfrom mypackage import module1from mypackage.module1 import function
Package-Level Exports
# mypackage/__init__.pyfrom .module1 import function1, Class1from .module2 import function2, Class2 # Export ra package level__all__ = ["function1", "Class1", "function2", "Class2"] # Bây giờ có thể import trực tiếpfrom mypackage import function1, Class1
Package Initialization
# mypackage/__init__.pyimport logging # Setup logging khi package được importlogging.basicConfig(level=logging.INFO)logger = logging.getLogger(__name__) logger.info("Package mypackage initialized") # Version info__version__ = "1.0.0" # Package metadata__author__ = "Your Name"__email__ = "[email protected]"
Lazy Imports
# mypackage/__init__.pydef __getattr__(name): """Lazy import heavy modules""" if name == "heavy_module": from . import heavy_module return heavy_module raise AttributeError(f"module {__name__!r} has no attribute {name!r}") # heavy_module chỉ được import khi được sử dụngfrom mypackage import heavy_module
2. Import Mechanisms
Absolute Imports
# Preferred - Clear và explicitfrom mypackage.subpackage.module import functionimport mypackage.subpackage.module as mod # Example structuremypackage/├── __init__.py├── module1.py└── subpackage/ ├── __init__.py └── module2.py # In module2.pyfrom mypackage.module1 import helper # ✅ Absolute import
Relative Imports
# In mypackage/subpackage/module2.py from . import module_in_same_dir # Same directoryfrom .. import module1 # Parent directoryfrom ..sibling import something # Sibling directoryfrom ...parent import item # Grandparent # Structuremypackage/├── __init__.py├── module1.py # from .. import module1├── subpackage/│ ├── __init__.py│ ├── module2.py # from . import module3│ └── module3.py└── sibling/ ├── __init__.py └── module4.py # from ..sibling import module4
Import Best Practices
# ✅ Goodfrom mypackage.utils import calculateimport mypackage.config as cfg # ❌ Avoidfrom mypackage.utils import * # Import tất cả - unclearimport mypackage.utils # Phải dùng mypackage.utils.calculate # ✅ Conditional importstry: import optional_dependencyexcept ImportError: optional_dependency = None # ✅ Type-checking imports (không runtime)from typing import TYPE_CHECKINGif TYPE_CHECKING: from mypackage.models import User # Tránh circular imports
3. Package Structure Patterns
Flat Package
mypackage/├── __init__.py├── core.py├── utils.py├── config.py└── exceptions.py # Simple và dễ navigatefrom mypackage import core, utils
Hierarchical Package
mypackage/├── __init__.py├── core/│ ├── __init__.py│ ├── engine.py│ └── processor.py├── utils/│ ├── __init__.py│ ├── helpers.py│ └── validators.py└── config/ ├── __init__.py ├── settings.py └── constants.py # Organized nhưng deeper importsfrom mypackage.core.engine import Enginefrom mypackage.utils.helpers import format_data
Namespace Package
# Không cần __init__.py (Python 3.3+)# Cho phép multiple packages với cùng namespace # Package Anamespace/└── package_a/ └── module_a.py # Package B (separate location)namespace/└── package_b/ └── module_b.py # Cả 2 có thể importfrom namespace.package_a import somethingfrom namespace.package_b import something_else
4. setup.py - Traditional Packaging
Basic setup.py
# setup.pyfrom setuptools import setup, find_packages setup( name="mypackage", version="1.0.0", author="Your Name", author_email="[email protected]", description="A short description", long_description=open("README.md").read(), long_description_content_type="text/markdown", url="https://github.com/yourusername/mypackage", packages=find_packages(exclude=["tests", "docs"]), classifiers=[ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ], python_requires=">=3.8", install_requires=[ "requests>=2.25.0", "click>=8.0.0", ], extras_require={ "dev": [ "pytest>=7.0.0", "black>=22.0.0", "mypy>=0.950", ], "docs": [ "sphinx>=4.0.0", ], }, entry_points={ "console_scripts": [ "mycommand=mypackage.cli:main", ], },)
Advanced setup.py
# setup.pyfrom setuptools import setup, find_packagesimport os # Read version từ packagedef get_version(): with open(os.path.join("mypackage", "__init__.py")) as f: for line in f: if line.startswith("__version__"): return line.split("=")[1].strip().strip('"').strip("'") return "0.0.0" # Read long description từ READMEdef get_long_description(): with open("README.md", encoding="utf-8") as f: return f.read() setup( name="mypackage", version=get_version(), description="A sophisticated package", long_description=get_long_description(), long_description_content_type="text/markdown", author="Your Name", author_email="[email protected]", url="https://github.com/yourusername/mypackage", license="MIT", # Package discovery packages=find_packages( where="src", exclude=["tests*", "docs*", "examples*"] ), package_dir={"": "src"}, # Include non-Python files package_data={ "mypackage": ["data/*.json", "templates/*.html"], }, include_package_data=True, # MANIFEST.in # Dependencies install_requires=[ "requests>=2.25.0,<3.0.0", "click>=8.0.0", "pydantic>=1.9.0", ], extras_require={ "dev": [ "pytest>=7.0.0", "pytest-cov>=3.0.0", "black>=22.0.0", "isort>=5.10.0", "mypy>=0.950", "flake8>=4.0.0", ], "async": [ "aiohttp>=3.8.0", "asyncpg>=0.25.0", ], }, # Python version python_requires=">=3.8,<4.0", # CLI commands entry_points={ "console_scripts": [ "mypackage=mypackage.cli:main", "mypackage-admin=mypackage.admin:main", ], }, # Metadata classifiers=[ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Topic :: Software Development :: Libraries", ], keywords="package utility tools", project_urls={ "Documentation": "https://mypackage.readthedocs.io", "Source": "https://github.com/yourusername/mypackage", "Tracker": "https://github.com/yourusername/mypackage/issues", },)
5. pyproject.toml - Modern Packaging
Basic pyproject.toml
[build-system]requires = ["setuptools>=61.0", "wheel"]build-backend = "setuptools.build_meta" [project]name = "mypackage"version = "1.0.0"description = "A short description"readme = "README.md"authors = [ {name = "Your Name", email = "[email protected]"}]license = {text = "MIT"}classifiers = [ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License",]requires-python = ">=3.8"dependencies = [ "requests>=2.25.0", "click>=8.0.0",] [project.optional-dependencies]dev = [ "pytest>=7.0.0", "black>=22.0.0",] [project.scripts]mycommand = "mypackage.cli:main" [project.urls]Homepage = "https://github.com/yourusername/mypackage"Documentation = "https://mypackage.readthedocs.io"
Poetry pyproject.toml
[tool.poetry]name = "mypackage"version = "1.0.0"description = "A sophisticated package"authors = ["Your Name <[email protected]>"]license = "MIT"readme = "README.md"homepage = "https://github.com/yourusername/mypackage"repository = "https://github.com/yourusername/mypackage"documentation = "https://mypackage.readthedocs.io"keywords = ["package", "utility", "tools"]classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10",] # Package structurepackages = [ { include = "mypackage", from = "src" }]include = ["LICENSE", "README.md"] [tool.poetry.dependencies]python = "^3.8"requests = "^2.25.0"click = "^8.0.0"pydantic = "^1.9.0" # Optional dependenciesaiohttp = {version = "^3.8.0", optional = true}asyncpg = {version = "^0.25.0", optional = true} [tool.poetry.group.dev.dependencies]pytest = "^7.0.0"pytest-cov = "^3.0.0"black = "^22.0.0"isort = "^5.10.0"mypy = "^0.950" [tool.poetry.extras]async = ["aiohttp", "asyncpg"] [tool.poetry.scripts]mypackage = "mypackage.cli:main"mypackage-admin = "mypackage.admin:main" [build-system]requires = ["poetry-core>=1.0.0"]build-backend = "poetry.core.masonry.api" # Tool configurations[tool.black]line-length = 88target-version = ['py38'] [tool.isort]profile = "black" [tool.mypy]python_version = "3.8"strict = true [tool.pytest.ini_options]testpaths = ["tests"]python_files = "test_*.py"
5 Ứng Dụng Thực Tế
1. CLI Application Package
mypackage/cli.py
import clickfrom mypackage import __version__from mypackage.core import process_data @click.group()@click.version_option(version=__version__)def cli(): """MyPackage CLI tool""" pass @cli.command()@click.argument("input_file")@click.option("--output", "-o", help="Output file")@click.option("--verbose", "-v", is_flag=True, help="Verbose output")def process(input_file, output, verbose): """Process input file""" if verbose: click.echo(f"Processing {input_file}...") result = process_data(input_file) if output: with open(output, "w") as f: f.write(result) click.echo(f"✅ Output written to {output}") else: click.echo(result) @cli.command()def info(): """Show package info""" click.echo(f"MyPackage v{__version__}") click.echo("Author: Your Name") if __name__ == "__main__": cli()
setup.py
setup( name="mypackage", # ... entry_points={ "console_scripts": [ "mypackage=mypackage.cli:cli", ], },) # Sau khi install: mypackage process file.txt -o output.txt
2. Plugin System
mypackage/plugins/init.py
from typing import Dict, Typeimport importlib.metadata class PluginManager: """Quản lý plugins thông qua entry points""" def __init__(self): self._plugins: Dict[str, Type] = {} self.discover_plugins() def discover_plugins(self): """Discover plugins từ entry points""" # Entry point group: mypackage.plugins for entry_point in importlib.metadata.entry_points( group="mypackage.plugins" ): try: plugin_class = entry_point.load() self._plugins[entry_point.name] = plugin_class print(f"✅ Loaded plugin: {entry_point.name}") except Exception as e: print(f"❌ Failed to load {entry_point.name}: {e}") def get_plugin(self, name: str): """Get plugin by name""" return self._plugins.get(name) def list_plugins(self): """List all available plugins""" return list(self._plugins.keys()) # Sử dụngplugin_manager = PluginManager()print(plugin_manager.list_plugins())
Plugin package setup.py
# my-plugin/setup.pysetup( name="mypackage-plugin-awesome", # ... entry_points={ "mypackage.plugins": [ "awesome=my_plugin.plugin:AwesomePlugin", ], },) # Sau khi install, plugin tự động được discovered
3. Configuration Package
mypackage/config/init.py
import osfrom pathlib import Pathfrom typing import Dict, Anyimport json class Config: """Configuration manager với multiple sources""" def __init__(self, app_name: str): self.app_name = app_name self._config: Dict[str, Any] = {} self.load_config() def load_config(self): """Load config từ multiple sources""" # 1. Default config từ package self._load_package_config() # 2. System config (/etc/) self._load_system_config() # 3. User config (~/.config/) self._load_user_config() # 4. Environment variables self._load_env_config() # 5. Local config (./config.json) self._load_local_config() def _load_package_config(self): """Load default config từ package""" config_file = Path(__file__).parent / "default.json" if config_file.exists(): with open(config_file) as f: self._config.update(json.load(f)) def _load_system_config(self): """Load system-wide config""" config_file = Path(f"/etc/{self.app_name}/config.json") if config_file.exists(): with open(config_file) as f: self._config.update(json.load(f)) def _load_user_config(self): """Load user-specific config""" config_dir = Path.home() / ".config" / self.app_name config_file = config_dir / "config.json" if config_file.exists(): with open(config_file) as f: self._config.update(json.load(f)) def _load_env_config(self): """Load config từ environment variables""" prefix = f"{self.app_name.upper()}_" for key, value in os.environ.items(): if key.startswith(prefix): config_key = key[len(prefix):].lower() self._config[config_key] = value def _load_local_config(self): """Load local config file""" config_file = Path("config.json") if config_file.exists(): with open(config_file) as f: self._config.update(json.load(f)) def get(self, key: str, default=None): """Get config value""" return self._config.get(key, default) def __getitem__(self, key: str): return self._config[key] # Sử dụngconfig = Config("myapp")db_url = config.get("database_url", "sqlite:///db.sqlite3")
4. API Client Library Package
mypackage/client.py
from typing import Optional, Dict, Anyimport requestsfrom mypackage import __version__ class APIClient: """API client với proper structure""" def __init__( self, base_url: str, api_key: Optional[str] = None, timeout: int = 30 ): self.base_url = base_url.rstrip("/") self.api_key = api_key self.timeout = timeout self.session = requests.Session() self.session.headers.update({ "User-Agent": f"MyPackage/{__version__}", "Content-Type": "application/json", }) if api_key: self.session.headers["Authorization"] = f"Bearer {api_key}" def _request( self, method: str, endpoint: str, **kwargs ) -> Dict[str, Any]: """Internal request method""" url = f"{self.base_url}/{endpoint.lstrip('/')}" response = self.session.request( method, url, timeout=self.timeout, **kwargs ) response.raise_for_status() return response.json() def get(self, endpoint: str, **kwargs) -> Dict[str, Any]: """GET request""" return self._request("GET", endpoint, **kwargs) def post(self, endpoint: str, **kwargs) -> Dict[str, Any]: """POST request""" return self._request("POST", endpoint, **kwargs) def __enter__(self): return self def __exit__(self, *args): self.session.close() # Sử dụngfrom mypackage import APIClient with APIClient("https://api.example.com", api_key="xxx") as client: data = client.get("/users/me") print(data)
5. Data Processing Library
mypackage/init.py
"""MyPackage - A data processing library""" __version__ = "1.0.0"__author__ = "Your Name" # Public APIfrom mypackage.core import DataProcessorfrom mypackage.utils import validate_data, format_outputfrom mypackage.exceptions import DataError, ValidationError # Convenience exports__all__ = [ "DataProcessor", "validate_data", "format_output", "DataError", "ValidationError",] # Package-level functionsdef process(data, **options): """Convenience function cho quick processing""" processor = DataProcessor(**options) return processor.process(data)
mypackage/core.py
from typing import Any, Dict, Listfrom mypackage.utils import validate_datafrom mypackage.exceptions import DataError class DataProcessor: """Main data processor""" def __init__(self, strict: bool = False): self.strict = strict def process(self, data: List[Dict]) -> List[Dict]: """Process data""" if self.strict: validate_data(data) return [self._process_item(item) for item in data] def _process_item(self, item: Dict) -> Dict: """Process single item""" # Processing logic return item
mypackage/exceptions.py
class MyPackageError(Exception): """Base exception""" pass class DataError(MyPackageError): """Data processing error""" pass class ValidationError(MyPackageError): """Data validation error""" pass
Building and Publishing
Building Package
# Với setuptoolspython setup.py sdist bdist_wheel # Với Poetrypoetry build # Outputdist/├── mypackage-1.0.0.tar.gz # Source distribution└── mypackage-1.0.0-py3-none-any.whl # Wheel
Publishing to PyPI
# Install twinepip install twine # Upload to TestPyPI (testing)twine upload --repository testpypi dist/* # Upload to PyPI (production)twine upload dist/* # Với Poetrypoetry publish --build
Installing Published Package
# Từ PyPIpip install mypackage # Từ TestPyPIpip install --index-url https://test.pypi.org/simple/ mypackage # Từ local wheelpip install dist/mypackage-1.0.0-py3-none-any.whl # Development modepip install -e .
Best Practices
1. Project Structure
mypackage/├── README.md├── LICENSE├── pyproject.toml├── setup.py (optional)├── MANIFEST.in├── src/│ └── mypackage/│ ├── __init__.py│ ├── core.py│ ├── utils.py│ └── data/│ └── config.json├── tests/│ ├── __init__.py│ ├── test_core.py│ └── test_utils.py├── docs/│ ├── index.md│ └── api.md└── examples/ └── basic_usage.py
2. Versioning
# Semantic Versioning: MAJOR.MINOR.PATCH# 1.0.0 -> 1.0.1 (patch: bug fixes)# 1.0.0 -> 1.1.0 (minor: new features, backwards compatible)# 1.0.0 -> 2.0.0 (major: breaking changes) # mypackage/__init__.py__version__ = "1.2.3" # setup.pysetup( version="1.2.3", # Or read từ __init__.py)
3. Dependencies
# ✅ Pin major versionrequests>=2.0.0,<3.0.0 # ✅ Pin exact version cho critical depscryptography==38.0.1 # ❌ Avoidrequests # No version constraintrequests==2.28.0 # Too strict for library
4. Documentation
# mypackage/__init__.py"""MyPackage========= A sophisticated data processing library. Basic Usage----------->>> from mypackage import process>>> data = [{"id": 1, "name": "John"}]>>> result = process(data) For more information, visit: https://mypackage.readthedocs.io"""
Bài Tập Thực Hành
Bài 1: Simple Package
Tạo package mathutils với modules basic.py (add, subtract) và advanced.py (power, sqrt). Setup proper __init__.py để export functions.
Bài 2: CLI Tool
Tạo CLI tool với Click, package thành installable command với entry points.
Bài 3: Plugin System
Implement plugin system với entry points, tạo 2 sample plugins.
Bài 4: pyproject.toml
Convert một existing project từ setup.py sang pyproject.toml với Poetry.
Bài 5: Publish Package
Tạo simple package, build, và publish lên TestPyPI.
Tóm Tắt
Trong bài này chúng ta đã học:
- ✅ Package vs Module - Hiểu sự khác biệt
- ✅
__init__.py- Package initialization, exports, lazy imports - ✅ Imports - Absolute vs relative imports, best practices
- ✅ Package Structure - Flat, hierarchical, namespace packages
- ✅ setup.py - Traditional packaging với setuptools
- ✅ pyproject.toml - Modern packaging standard
- ✅ Distribution - Building và publishing packages
Package structure tốt giúp code organized, maintainable, và dễ distribute!
Bài tiếp theo: Bài 16: Working with Databases - Học cách làm việc với databases trong Python! 🗄️