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:

  1. Package vs Module - Hiểu sự khác biệt
  2. __init__.py - Package initialization, exports, lazy imports
  3. Imports - Absolute vs relative imports, best practices
  4. Package Structure - Flat, hierarchical, namespace packages
  5. setup.py - Traditional packaging với setuptools
  6. pyproject.toml - Modern packaging standard
  7. 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! 🗄️