Học bằng cách đọc theo thứ tự

__init__.py và Relative Import — Gói các file vào một package

Học vai trò của __init__.py để xây dựng package Python và khi nào dùng absolute import vs. relative import.

Bài trước đã bao quát module một-file và cách import hoạt động. Khi ứng dụng lớn lên, bạn sẽ muốn gói các module liên quan dưới một folder duy nhất như một package. Bài này đi qua cách import package.

Package là một folder chứa __init__.py

Một package là một folder chứa một file đặc biệt tên là __init__.py. Khi Python tìm thấy nó, cả folder trở thành một package và code khác có thể kéo nó vào với import tên_folder. __init__.pythứ chạy đầu tiên khi package được import, và đó là chỗ thường gặp để khai báo những hàm và class mà package expose ra ngoài.

Giải phẫu một package
my_package/ (một package)
  • __init__.py — file chạy đầu tiên khi package được import
  • calculation.py — một module (add / multiply)
  • string_utils.py — một module (format_name, v.v.)
Folder my_package/ chứa __init__.py, nên Python xem folder như một package. Các module nội bộ như calculation.py được expose ra ngoài qua __init__.py.

__init__.py có thể trống. Ngay cả file trống cũng đủ — Python chỉ cần sự hiện diện của file để nhận diện folder như một package.

Import mà không đi qua __init__.py

Ngay cả với __init__.py trống, bạn vẫn có thể import một module trực tiếp từ package. Dạng là from tên_package.tên_module import tên_hàm — nối vị trí của file bằng dấu chấm.

# my_package/__init__.py  <- trống cũng được

# my_package/calculation.py
def add(a, b):
    return a + b


# Trong main.py
from my_package.calculation import add   # ghi rõ tên file `calculation`
print(add(1, 2))   # 3

Nhược điểm: bên gọi cần biết file nào chứa cái gì. Nếu sau này bạn tổ chức lại package (ví dụ tách calculation.py thành hai), mọi bên gọi cũng phải đổi theo. Mẫu re-export qua __init__.py ở phần sau sửa được điều đó — nó cho phép bên gọi tham chiếu trực tiếp đến tên ngắn dưới tên package.

Tập hợp public API trong __init__.py

Viết from .tên_module import tên_hàm bên trong __init__.py và hàm đó xuất hiện như thể nó nằm trực tiếp dưới package. Ví dụ, from .calculation import add, multiply bên trong __init__.py cho phép bên gọi viết from my_package import add, multiply — ngắn và sạch.

Re-export qua __init__.py để import ngắn hơn
my_package/main.py__init__.pyfrom .calculationimport add, multiplycalculation.pydef add()def multiply()from my_packageimport addadd(1, 2)gọi đượcquakéo lên
Khi __init__.py kéo add lên từ calculation.py, bên gọi có thể viết from my_package import add mà không cần nghĩ đến file nào (calculation) chứa nó.

Folder my_package/ được đính kèm bên trái (mở calculation.py và __init__.py từ 📂 Files để xem).

① Dùng from my_package import add, multiply để load cả hai hàm.

② In kết quả của add(10, 20) và multiply(3, 4).

Python Editor

Chạy code để xem đầu ra

Vì sao tập hợp public API trong __init__.py

Nếu bên gọi chỉ động đến những gì __init__.py expose, sau này bạn có thể xáo trộn các file nội bộ mà không phải động vào code bên gọi. Đó là đóng gói kinh điển ở cấp package — mẫu cơ bản của thiết kế package.

from vs. import — Hai cách viết

hai dạng import: from package import nameimport package. Cả hai đều kéo target vào, nhưng cách bạn gọi sau đó khác nhau.

from vs. import — Cái gì rơi vào scope
from my_pkg.calcimport addtrong scope:addgọi:add(1, 2)importmy_pkg.calctrong scope:my_pkggọi:my_pkg.calc.add(1, 2)
from package import name đưa chính cái tên vào scope để bạn dùng trực tiếp. import package chỉ đưa tên package vào scope; lời gọi phải dùng đường dẫn dấu chấm đầy đủ.
# Dạng 1: from ... import ...
from my_package.calculation import add
print(add(1, 2))   # add dùng trực tiếp được


# Dạng 2: import ...
import my_package.calculation
print(my_package.calculation.add(1, 2))   # gọi với đường dẫn dấu chấm đầy đủ


# Dạng 2 + as alias (rút gọn tên dài)
import my_package.calculation as calc
print(calc.add(1, 2))
DạngTên trong scopeCách gọi
from my_package.calculation import addaddadd(1, 2)
import my_package.calculationmy_packagemy_package.calculation.add(1, 2)
import my_package.calculation as calccalccalc.add(1, 2)

Dạng phổ biến nhất là from package import name — tên hàm ngắn và code phía main đọc sạch. Nhưng khi hai package khác nhau expose cùng tên hàm và bạn cần cả hai, dạng import package giữ tên module nhìn thấy được nên rõ là bạn đang gọi cái nào.

from vs. import — Cái gì rơi vào scope của main
from my_pkg.calc import add
  • add — chính cái tên rơi vào scope của main
  • Gọi: add(1, 2) chạy trực tiếp
  • Viết ngắn, nhưng không rõ ngay add đến từ package nào
import my_pkg.calc
  • my_pkg — chỉ tên package rơi vào scope của main
  • Gọi: my_pkg.calc.add(1, 2) với đường dẫn dấu chấm đầy đủ
  • Viết dài hơn, nhưng my_pkg.calc.add làm rõ nguồn gốc
from package import name đưa chính cái tên vào scope của main (= gọi ngắn). import package chỉ đưa tên package, nên lời gọi dùng đường dẫn dấu chấm đầy đủ (= rõ nguồn gốc).

Thử cả hai dạng với package mathlib/ đính kèm. calculator.py định nghĩa triple(n) (xác nhận trong 📂 Files).

① Dùng dạng from ... import ... để đưa triple vào, rồi print(triple(7)).

② Sau đó dùng dạng import ... as ... để cũng đưa mathlib.calculator vào dưới alias calc, và print(calc.triple(7)).

Python Editor

Chạy code để xem đầu ra

Absolute Import vs. Relative Import

Còn nếu các module bên trong một package cần tham chiếu lẫn nhau thì sao? Giả sử utility/validator.py muốn import từ utility/helper.py — có hai cách để viết: absolute importrelative import. Để chọn giữa chúng, trước tiên bạn cần rõ gốc dự án nghĩa là gì.

Gốc dự án (project root) là gì
project/ ← gốc (nơi main.py nằm)
  • main.py ← entry point bạn chạy đầu tiên
  • config.py ← cấu hình chung của ứng dụng
my_app/
  • __init__.py ← chạy khi import my_app xảy ra
utility/
  • __init__.py
  • validator.py
  • helper.py
Gốc dự án là folder mà main.py (entry point) nằm trong đó. my_app trong absolute import from my_app.utility import validator tham chiếu đến folder cùng tên trực tiếp dưới gốc đó.

Gốc dự án là folder chứa entry point bạn chạy với python main.py. Khi bạn viết absolute import from my_app.utility import validator, Python tìm my_app trực tiếp dưới gốc đó, rồi đào vào utility/validator.py. Đường dẫn dấu chấm phản chiếu cấu trúc folder — đó chính là tất cả những gì absolute import là.

LoạiDạngNghĩa là gì
Absolute importfrom my_app.utility import helperĐường dẫn đầy đủ từ gốc dự án
Relative importfrom . import helperTương đối với file bạn đang ở
Relative import (cha)from .. import configNhắm tới file ở cấp folder trên
Hai cách đọc helper.py từ validator.py
Absolutefrom my_app.utilityimport helperĐường dẫn đầy đủtừ gốcRelativefrom . importhelperDựa trênfolder hiện tại
Khi bạn đọc một module hàng xóm trong cùng package, cả absolute và relative đều chạy được. Dấu . trong from . import nghĩa là "folder mà tôi đang ở".

Cách chia thông thường: entry point (main.py) dùng absolute import để với vào các package, và các module bên trong package dùng relative import để tham chiếu lẫn nhau. Với relative import, sau này đổi tên folder package không bắt nội bộ phải thay đổi theo.

# my_app/utility/validator.py
# Relative import cho helper.py trong cùng package
from .helper import log_message


def validate_user(user):
    if user.name and user.email:
        log_message("OK")
        return True
    log_message("phát hiện vấn đề")
    return False


# my_app/utility/helper.py
def log_message(message):
    print("[LOG]", message)


# Để với tới config.py ở package khác,
# đi lên một cấp với ..:
# from ..config import get_config
Bố cục folder cho code ở trên
my_app/
  • config.py ← target của from ..config import ...
utility/
  • validator.py ← viết from .helper import log_message
  • helper.py ← cung cấp log_message(...)
validator.py và helper.py nằm cạnh nhau trong cùng folder utility/. Đó là lý do from .helper import log_message trong validator.py dùng . — "folder này (= utility/)". config.pycấp trên một bậc trong my_app/, nên từ góc nhìn của validator bạn sẽ với tới nó là ..config (.. = một cấp lên).

Entry point không dùng được relative import

Một file bạn chạy trực tiếp với python main.py không dùng được relative import — bạn sẽ gặp ImportError. Relative import chỉ chạy khi file được load như một thành viên của một package nào đó. Từ entry point, luôn dùng absolute import (ví dụ, from my_app.utility import validate_user).

Thử absolute và relative import với package shop/ đính kèm. Mở 📂 Files và bạn sẽ thấy shop/__init__.py trống — chưa có Cart hay to_yen nào được expose ở gốc package.

① Đầu tiên, chạy main.py như hiện tại — bạn sẽ nhận được ImportError vì shop không export gì cả.

② Tiếp theo, mở shop/__init__.py từ 📂 và viết from .cart import Cartfrom .formatter import to_yen như relative import (. = package này), rồi save.

③ Thêm phần dùng Cart vào main.py — dựng một Cart, thêm apple giá $100 và orange giá $200, rồi định dạng tổng với to_yen() và in ra. Phía main dùng absolute import, nội bộ package dùng relative import — đó là cách chia.

Python Editor

Chạy code để xem đầu ra

Bố cục giống một dự án thật

Với những quy tắc này, bạn có thể ráp các cấu trúc folder khớp với app thật. Đây là một bố cục chia thành ba package — settings, database, và utility:

Bố cục gần với một dự án thật
project/ (gốc dự án)
  • main.py — entry point (file bạn chạy với python)
  • config.py — cấu hình chung của ứng dụng
my_app/
  • __init__.py — public API của package my_app
database/
  • __init__.py
  • connection.py / models.py
utility/
  • __init__.py
  • validator.py / helper.py
Entry point main.py kéo cả package my_app/ vào; các module nội bộ dùng relative import để với tới nhau. Mỗi subfolder có __init__.py riêng tập hợp public API của subpackage đó.

Từ main.py bạn gọi ra ngoài bằng absolute import như from my_app.utility import validate_user. Bên trong utility/, validator.py với tới helper.py với from .helper import log_message — một relative import. Đó là cách phân chia trách nhiệm kinh điển.

Ranh giới giữa Absolute và Relative
my_app/utility/main.pyvalidator.pyhelper.pyabsoluterelative
main.py với vào package utility bằng absolute (đường dẫn từ gốc); bên trong utility, validator → helper đi relative (đường dẫn từ chỗ bạn đang ở). Biết ranh giới giúp việc đổi tên folder sau này không lan ra ngoài.

Triển khai validator.py bên trong package my_app/utility/ chính bạn và gọi nó từ main.py (mở validator.py từ 📂 Files, sửa, rồi save với Cmd+S hoặc 💾). helper.py đã làm sẵn rồi.

① Triển khai validate_user(email) trong validator.py:

- Bắt đầu với from .helper import log_message — một relative import của helper.py trong cùng folder

- Nếu email chứa cả @., gọi log_message("OK: " + email) và trả về True

- Nếu khác, gọi log_message("phát hiện vấn đề: " + email) và trả về False

② Trong main.py, import nó với absolute import (from my_app.utility import validate_user) và print(validate_user("minh@example.com")).

Python Editor

Chạy code để xem đầu ra

Từ đây là phần nâng cao — cứ quay lại nếu bạn bí

Bài tập tiếp theo là đỉnh điểm — xây song song hai package. Bạn sẽ làm trơn tru nhất nếu mẫu re-export trong __init__.py đã quen tay với bạn và sự phân biệt absolute / relative import đã vững. Nếu bị kẹt, hãy nhảy về bài tập validator.py trước đó hoặc sơ đồ "Bố cục giống dự án thực tế" ở trên để định lại trước khi bắt tay vào bài này.

Thử thách nhiều folder

Đến lúc thử xây dựng hai package song song với mọi thứ bạn đã học. Bạn sẽ quản lý một package catalog/ cho sản phẩm và một package billing/ ở các folder riêng biệt, rồi điều phối chúng từ main.py để hoàn thành một đơn hàng. Chia trách nhiệm qua các folder nghĩa là thay đổi dữ liệu sản phẩm chỉ động đến catalog/, và thay đổi định dạng hóa đơn chỉ động đến billing/bán kính ảnh hưởng nằm trong một folder.

Bố cục dự án (catalog và billing là hai package)
project/ ← gốc dự án
  • main.py — entry point (dùng cả hai package để xử lý một đơn hàng)
catalog/
  • __init__.py — re-export from .products import get_price
  • products.py — triển khai get_price(name)
billing/
  • __init__.py — re-export from .invoice import format_invoice
  • invoice.py — triển khai format_invoice(name, qty, unit_price)
catalog/ giữ dữ liệu sản phẩm (products.py); billing/ giữ định dạng hóa đơn (invoice.py). Mỗi __init__.py re-export public API, nên main.py có thể gọi ra ngoài với hai absolute import: from catalog import get_pricefrom billing import format_invoice.
main.py điều phối hai package
catalogget_price()main.pyđiều phối cả haibillingformat_invoice()from catalog import get_pricefrom billing import ...
main.py gọi ra ngoài bằng absolute import để lấy unit_price từ catalog và để định dạng hóa đơn qua billing. Mỗi package nội bộ dùng relative import trong __init__.py để re-export .products / .invoice.

Hoàn thiện package catalog/billing/ đính kèm bao gồm cả file __init__.py của chúng, rồi điều phối chúng từ main.py với absolute import (mở mỗi file từ 📂 Files và save với Cmd+S hoặc 💾).

① Trong catalog/products.py, triển khai get_price(name)"apple" → 100, "orange" → 150, các trường hợp khác → 0 (một dict + dict.get(name, 0) là tiện).

② Trong catalog/__init__.py, viết from .products import get_price để bên ngoài có thể gọi from catalog import get_price (một relative import).

③ Trong billing/invoice.py, triển khai format_invoice(name, qty, unit_price) — tính qty * unit_price và trả về một chuỗi như "apple x 3 = $300".

④ Trong billing/__init__.py, viết from .invoice import format_invoice.

⑤ Trong main.py, làm from catalog import get_pricefrom billing import format_invoice (absolute import). Đặt 3 đơn vị "apple", tra giá đơn vị, định dạng hóa đơn, và print().

Python Editor

Chạy code để xem đầu ra

Từ đây là phần thưởng — hiếm gặp trong code thực

Phần dưới đây nói về __all__ và chỉ mang tính tham khảo. Code thực tế gần như không bao giờ dùng from package import *, nên phần này không ảnh hưởng đến mạch chính. Miễn là bạn đã nắm vững dạng import với tên tường minh, cứ thoải mái lướt qua phần này.

Điều khiển from package import * với __all__

Khi ai đó viết from my_package import * để kéo mọi thứ vào với dấu sao, bạn có thể điều khiển những gì được expose qua __all__ trong __init__.py. Với __all__ = ["add"], lệnh import * chỉ lấy add — không gì khác.

__all__ thu hẹp những gì được expose
__init__.pyfrom .calc importadd, multiply__all__ = ["add"]from pkgimport *add→ được kéo vàomultiply→ không được kéo vào(NameError)
Với __all__ = ["add"] trong __init__.py, chỉ add đi qua from package import *. multiply bị bỏ ra (bạn vẫn có thể lấy nó tường minh).
# my_package/__init__.py
from .calculation import add, multiply

__all__ = ["add"]   # chỉ add được expose qua *

# Phía bên gọi
# from my_package import *
# add(1, 2)        # OK
# multiply(1, 2)   # NameError (không được * kéo vào)

Trong thực tế, tránh import *

from my_package import * khó đọc — bạn không biết ngay cái gì đã vào. Code thực tế gần như luôn dùng tên tường minh như from my_package import add, multiply. Hãy xem __all__ như một *lưới an toàn cho trường hợp hiếm khi ai đó dùng ``**.

Xác nhận hành vi của __all__ với package bundle/ đính kèm. Mở bundle/__init__.py từ 📂 Files — nó kéo cả addmultiply vào nhưng thu hẹp expose với __all__ = ["add"].

① Trong main.py, làm from bundle import * và in add(2, 3).

② Sau đó bọc một lời gọi multiply(2, 3) trong try/except và xác nhận nó raise NameError (bài này dùng runtime Pyodide tương thích CPython — load lần đầu mất 5–15 giây).

Python Editor

Chạy code để xem đầu ra

Bài này đã bao quát việc dùng __init__.py để gói nhiều module vào một package, sự khác biệt giữa from package import nameimport package, lựa chọn giữa absolute và relative import, và một bố cục folder gần với một dự án thật.

QUIZ

Kiểm tra kiến thức

Hãy trả lời từng câu hỏi một.

Câu 1Bạn phải đặt file nào trong một folder để nó được nhận diện như một package?

Câu 2Từ my_app/utility/validator.py, đâu là relative import đúng để load helper.py trong cùng folder utility?

Câu 3Điều gì xảy ra nếu bạn viết một relative import (from . import xxx) bên trong một entry point (file bạn chạy trực tiếp với python main.py)?