Khi chương trình lớn dần, để tất cả trong một file trở nên khó quản lý. Module và package là cách Python tổ chức code thành nhiều phần.
Module là gì?
Mỗi file .py chính là một module. Tạo file tinh_toan.py:
# tinh_toan.py
THUE_SUAT = 0.1
def tinh_thue(so_tien):
return so_tien * THUE_SUAT
def tinh_tong(gia, so_luong):
return gia * so_luong
Dùng ở file khác:
import tinh_toan
print(tinh_toan.tinh_thue(1_000_000))
print(tinh_toan.THUE_SUAT)
Các cách nhập khẩu
import tinh_toan # dùng: tinh_toan.tinh_thue()
import tinh_toan as tt # dùng: tt.tinh_thue()
from tinh_toan import tinh_thue # dùng: tinh_thue()
from tinh_toan import tinh_thue, tinh_tong
Tránh from tinh_toan import *. Nó kéo mọi thứ vào không gian tên hiện tại, và khi có xung đột tên bạn sẽ rất khó tìm ra nguồn gốc.
Package: nhiều module trong một thư mục
ung_dung/
├── __init__.py
├── cau_hinh.py
├── mo_hinh/
│ ├── __init__.py
│ ├── nguoi_dung.py
│ └── don_hang.py
└── tien_ich/
├── __init__.py
└── dinh_dang.py
File __init__.py đánh dấu thư mục là một package. Nó có thể để trống, hoặc dùng để gom các thứ hay dùng lên trên cho tiện:
# ung_dung/mo_hinh/__init__.py
from .nguoi_dung import NguoiDung
from .don_hang import DonHang
__all__ = ["NguoiDung", "DonHang"]
Nhờ vậy người dùng viết được from ung_dung.mo_hinh import NguoiDung thay vì đường dẫn dài.
Nhập khẩu tương đối
Bên trong package, dùng dấu chấm để trỏ tới module cùng cấp:
# ung_dung/mo_hinh/don_hang.py
from .nguoi_dung import NguoiDung # cùng thư mục
from ..cau_hinh import THUE_SUAT # lên một cấp
from ..tien_ich.dinh_dang import tien_te
Nhập khẩu tương đối chỉ hoạt động khi file được chạy như một phần của package. Chạy trực tiếp python ung_dung/mo_hinh/don_hang.py sẽ báo lỗi. Đó là lý do nên chạy bằng:
python -m ung_dung.mo_hinh.don_hang
Khối if __name__ == “__main__”
# tinh_toan.py
def tinh_thue(so_tien):
return so_tien * 0.1
if __name__ == "__main__":
# chỉ chạy khi gọi trực tiếp file này,
# không chạy khi file được import
print(tinh_thue(1_000_000))
Khi Python nhập khẩu một module, nó chạy toàn bộ nội dung file đó. Không có khối bảo vệ này, mọi lệnh in ấn hay xử lý ở cấp cao nhất sẽ chạy mỗi lần ai đó import module của bạn.
Python tìm module ở đâu?
import sys
for d in sys.path:
print(d)
Thứ tự tìm kiếm: thư mục chứa script đang chạy, rồi tới các thư mục trong biến môi trường PYTHONPATH, cuối cùng là thư viện chuẩn và site-packages.
Cái bẫy đặt tên trùng thư viện
Nếu bạn tạo file tên random.py hay json.py trong thư mục dự án, mọi lệnh import random sẽ lấy file của bạn thay vì thư viện chuẩn — và lỗi báo ra thường rất khó hiểu:
AttributeError: module 'random' has no attribute 'randint'
Gặp lỗi kiểu này, việc đầu tiên nên làm là kiểm tra xem trong thư mục có file trùng tên thư viện không. Cũng nhớ xóa cả thư mục __pycache__ tương ứng.
Nhập khẩu vòng tròn
Module A import B, mà B lại import A:
ImportError: cannot import name 'X' from partially initialized module
Đây là dấu hiệu thiết kế cần xem lại. Vài cách xử lý:
- Tách phần dùng chung ra module thứ ba mà cả hai cùng import
- Chuyển lệnh import vào bên trong hàm, để nó chỉ chạy khi hàm được gọi
- Nếu chỉ cần cho gợi ý kiểu, dùng
if TYPE_CHECKING
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .nguoi_dung import NguoiDung # chỉ để công cụ kiểm tra kiểu đọc
def xu_ly(nd: "NguoiDung") -> None:
...
Cấu trúc dự án nên theo
du_an/
├── pyproject.toml
├── README.md
├── src/
│ └── ung_dung/
│ ├── __init__.py
│ └── ...
└── tests/
└── test_ung_dung.py
Đặt mã nguồn trong thư mục src/ có một lợi ích thiết thực: khi chạy kiểm thử, Python buộc phải dùng bản đã cài đặt thay vì đọc thẳng thư mục dự án. Nhờ đó bạn phát hiện sớm những file bị quên khai báo trong cấu hình đóng gói.