Câu 1Bạn phải đặt file nào trong một folder để nó được nhận diện như một package?
__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__.py là thứ 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.
- __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.)
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.
from my_package import add mà không cần nghĩ đến file nào (calculation) chứa nó.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
Có hai dạng import: from package import name và import package. Cả hai đều kéo target vào, nhưng cách bạn gọi sau đó khác nhau.
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ạng | Tên trong scope | Cách gọi |
|---|---|---|
| from my_package.calculation import add | add | add(1, 2) |
| import my_package.calculation | my_package | my_package.calculation.add(1, 2) |
| import my_package.calculation as calc | calc | calc.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.
- 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
- 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.addlà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).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 import và relative import. Để chọn giữa chúng, trước tiên bạn cần rõ gốc dự án nghĩa là gì.
- main.py ← entry point bạn chạy đầu tiên
- config.py ← cấu hình chung của ứng dụng
- __init__.py ← chạy khi
import my_appxảy ra
- __init__.py
- validator.py
- helper.py
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ại | Dạng | Nghĩa là gì |
|---|---|---|
| Absolute import | from my_app.utility import helper | Đường dẫn đầy đủ từ gốc dự án |
| Relative import | from . import helper | Tương đối với file bạn đang ở |
| Relative import (cha) | from .. import config | Nhắm tới file ở cấp folder trên |
. 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
- config.py ← target của
from ..config import ...
- validator.py ← viết
from .helper import log_message - helper.py ← cung cấp
log_message(...)
from .helper import log_message trong validator.py dùng . — "folder này (= utility/)". config.py ở cấ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).
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:
- main.py — entry point (file bạn chạy với python)
- config.py — cấu hình chung của ứng dụng
- __init__.py — public API của package my_app
- __init__.py
- connection.py / models.py
- __init__.py
- validator.py / helper.py
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.
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.
- main.py — entry point (dùng cả hai package để xử lý một đơn hàng)
- __init__.py — re-export
from .products import get_price - products.py — triển khai
get_price(name)
- __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_price và from billing import format_invoice.__init__.py để re-export .products / .invoice.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__ = ["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 ``**.
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 name và import 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.
Kiểm tra kiến thức
Hãy trả lời từng câu hỏi một.
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)?