- Hiểu module = một file
.py, và điều gì thực sự xảy ra khiimport. - Dùng đúng 4 biến thể
import; biết khi nàofrom X import Ygây rắc rối. - Nắm
sys.modules— vì sao module chỉ chạy một lần duy nhất. - Hiểu
__name__và viết đúng khốiif __name__ == "__main__":. - Tự tạo package:
__init__.py, sub-package,__all__, public API. - Phân biệt absolute và relative import; biết vì sao
from .x import ylỗi khi chạypython x.py. - Đọc được
sys.pathvà biết Python tìm module theo thứ tự nào. - Phân biệt
python file.pyvàpython -m package.module— khác biệt then chốt. - Dựng src layout chuẩn cho dự án thật.
- Chẩn đoán và gỡ circular import.
Đây là dòng cuối cùng của Zen of Python, và nó nói về chính chủ đề chương này. Mỗi module là một
namespace riêng: hai module có thể cùng định nghĩa hàm parse() mà không đụng
nhau, vì chúng sống ở json.parse và csv.parse — hai "họ" khác nhau.
1. Module — một file .py là một module
Không có cú pháp đặc biệt nào để "khai báo module". Bất kỳ file .py nào cũng là một
module, và tên module chính là tên file bỏ đuôi .py. Không cần export như JS —
mọi tên ở cấp cao nhất của file đều tự động nhìn thấy được từ ngoài.
"""Vài phép toán tiện dụng."""
PI = 3.14159
def dien_tich_tron(r: float) -> float:
return PI * r ** 2
def chu_vi_tron(r: float) -> float:
return 2 * PI * r
def _lam_tron(x: float) -> float:
"""Hàm nội bộ — dấu _ đầu tên là quy ước 'đừng dùng từ ngoài'."""
return round(x, 2)
import toan_hoc
print(toan_hoc.PI) # 3.14159
print(toan_hoc.dien_tich_tron(2)) # 12.56636
print(toan_hoc.__name__) # 'toan_hoc'
print(toan_hoc.__file__) # /duong/dan/toan_hoc.py
print(toan_hoc.__doc__) # 'Vài phép toán tiện dụng.'
JS (ESM): phải export tường minh từng thứ; cái không export thì bên ngoài
không thấy. Có export default.
Dart: mọi tên public mặc định; tên bắt đầu bằng _ là private thật
sự ở cấp thư viện — compiler chặn.
Python: mọi tên đều truy cập được từ ngoài, kể cả _lam_tron. Dấu gạch dưới
chỉ là quy ước xã hội — "đây là nội bộ, dùng thì tự chịu". Python tin vào kỷ luật của lập trình
viên hơn là rào chắn của compiler. Đây chính là triết lý
"we're all consenting adults here".
2. Bốn biến thể của import
# ① import module — namespace giữ nguyên, rõ ràng nhất
import json
data = json.loads('{"a": 1}')
# ② import ... as ... — đổi tên, tránh trùng hoặc rút gọn
import numpy as np
import xml.etree.ElementTree as ET
# ③ from ... import ... — kéo tên cụ thể vào namespace hiện tại
from pathlib import Path
from collections import Counter, defaultdict
p = Path("data.csv") # không cần tiền tố pathlib.
# ④ from ... import * — kéo TẤT CẢ. Gần như luôn là ý tồi.
from math import *
print(sqrt(16)) # 4.0 — nhưng pi, e, floor, ceil... cũng vừa tràn vào
| Dạng | Ưu điểm | Nhược điểm | Khi nào dùng |
|---|---|---|---|
import x |
Rõ nguồn gốc; không bao giờ trùng tên; không dính circular import | Viết dài hơn | Mặc định — nhất là với module stdlib |
import x as y |
Rút gọn tên dài; theo quy ước cộng đồng (np, pd, plt) |
Người đọc phải biết quy ước | Tên module quá dài hoặc có alias phổ biến |
from x import y |
Gọn khi dùng y nhiều lần |
Mất ngữ cảnh nguồn; dễ trùng tên; nhạy cảm với circular import | Class/hàm dùng dày đặc: Path, dataclass, Counter |
from x import * |
Gõ ít | Ô nhiễm namespace, linter cảnh báo, che tên có sẵn, không biết tên nào từ đâu | Chỉ trong REPL khi vọc; không bao giờ trong file thật |
from x import * âm thầm ghi đèfrom os import *
from time import *
print(open) # ⚠️ os.open — KHÔNG phải built-in open()!
# os.open trả file descriptor (int), không phải file object
# → open("x.txt") sẽ ném TypeError khó hiểu
Cả os lẫn built-in đều có tên open, và import * im lặng để cái sau
đè cái trước. Bug loại này mất hàng giờ để tìm vì dòng gây lỗi trông hoàn toàn bình thường.
3. Điều gì xảy ra khi import
import toan_hoc không phải là "chèn nội dung file vào đây" như #include của C.
Python thực hiện đúng 4 bước:
sys.modules?→ trả về ngay
sys.paththeo thứ tự
file, từ trên xuống
vào namespace hiện tại
Bước 3 là điều người mới hay bỏ sót: import thực thi code. Mọi câu lệnh ở cấp
cao nhất của module đều chạy — kể cả print, kể cả kết nối database, kể cả vòng lặp tốn 10 giây.
print("⚠️ Tôi chạy ngay khi ai đó import module này!")
ket_noi = tao_ket_noi_db() # ⚠️ mở kết nối lúc import — rất tệ
def an_toan():
print("tôi chỉ chạy khi được gọi") # ✅ code trong hàm thì không
3.1. sys.modules — module chỉ chạy một lần
Sau lần import đầu tiên, module object được lưu vào từ điển sys.modules. Mọi lần
import sau chỉ là tra từ điển — code trong module không chạy lại. Đây là lý do
bạn có thể import cùng một module ở 20 file khác nhau mà không tốn thêm gì.
import sys
import on_ao # in "⚠️ Tôi chạy ngay..."
import on_ao # im lặng — lấy từ cache
import on_ao # im lặng
print("on_ao" in sys.modules) # True
print(sys.modules["on_ao"] is on_ao) # True — cùng một object
print(len(sys.modules)) # ~70 module đã nạp sẵn khi khởi động
Hệ quả thực tế: module là singleton
Vì mỗi module chỉ tồn tại một bản duy nhất trong tiến trình, biến cấp module là trạng thái toàn
cục dùng chung. Đây là cách đơn giản nhất để tạo singleton trong Python — không cần metaclass,
không cần __new__. Ví dụ: file config.py đọc biến môi trường một lần rồi mọi
module khác from config import SETTINGS đều nhận đúng một object. Mặt trái: hai module cùng
sửa biến đó sẽ giẫm chân nhau — hãy giữ trạng thái cấp module ở dạng chỉ đọc khi có thể.
import importlib
import toan_hoc
# ... sửa file toan_hoc.py ở editor ...
importlib.reload(toan_hoc) # chạy lại module, cập nhật object
Hữu ích trong REPL/Jupyter. Nhưng lưu ý: các tên đã from toan_hoc import PI vẫn giữ giá trị
cũ, vì chúng là bản sao tham chiếu chứ không trỏ vào module. Trong Jupyter, dùng
%autoreload 2 tiện hơn.
4. __name__ và if __name__ == "__main__":
Mỗi module có biến __name__. Giá trị của nó phụ thuộc cách module được nạp:
| Cách nạp | __name__ bằng |
|---|---|
Chạy trực tiếp: python toan_hoc.py | "__main__" |
Được import: import toan_hoc | "toan_hoc" |
Import từ package: import goi.toan_hoc | "goi.toan_hoc" |
Chạy như module: python -m goi.toan_hoc | "__main__" |
Nhờ vậy ta có mẫu kinh điển: một file vừa dùng được như thư viện, vừa chạy được như script.
"""Vài phép toán tiện dụng."""
PI = 3.14159
def dien_tich_tron(r: float) -> float:
return PI * r ** 2
def main() -> None:
"""Điểm vào khi chạy trực tiếp."""
import sys
r = float(sys.argv[1]) if len(sys.argv) > 1 else 1.0
print(f"Diện tích hình tròn bán kính {r}: {dien_tich_tron(r):.2f}")
if __name__ == "__main__":
main()
$ python toan_hoc.py 3
Diện tích hình tròn bán kính 3.0: 28.27
$ python -c "import toan_hoc; print(toan_hoc.dien_tich_tron(1))"
3.14159 # main() KHÔNG chạy khi import
pytestimport mọi file test và file được test — không có guard, code cấp module sẽ chạy giữa lúc thu thập test.multiprocessingtrên macOS/Windows import lại file chính ở tiến trình con. Không có guard → tiến trình con lại spawn tiến trình con → fork bomb.- Công cụ tài liệu (Sphinx, pydoc) import module để đọc docstring — sẽ vô tình chạy code của bạn.
5. Package — thư mục chứa module
Khi số module tăng lên, ta nhóm chúng vào package: một thư mục có file
__init__.py. Package lồng nhau được (sub-package), tạo thành cây namespace phản chiếu cây thư mục.
cua_hang/
├── __init__.py # biến thư mục thành package
├── san_pham.py # cua_hang.san_pham
├── gio_hang.py # cua_hang.gio_hang
└── thanh_toan/ # sub-package
├── __init__.py
├── the_tin_dung.py # cua_hang.thanh_toan.the_tin_dung
└── vi_dien_tu.py # cua_hang.thanh_toan.vi_dien_tu
import cua_hang.san_pham
cua_hang.san_pham.tim("laptop")
from cua_hang import san_pham
san_pham.tim("laptop")
from cua_hang.thanh_toan import the_tin_dung
the_tin_dung.thanh_toan(100_000)
from cua_hang.thanh_toan.the_tin_dung import thanh_toan
thanh_toan(100_000)
5.1. __init__.py — mặt tiền của package
File __init__.py chạy khi package được import lần đầu. Nó có ba công dụng, theo thứ tự phổ biến:
"""Thư viện quản lý cửa hàng."""
# ① Định nghĩa metadata của package
__version__ = "1.2.0"
# ② Nâng API con lên mặt tiền — người dùng gõ ngắn hơn
from cua_hang.san_pham import SanPham, tim
from cua_hang.gio_hang import GioHang
# ③ Khai báo public API tường minh
__all__ = ["SanPham", "GioHang", "tim"]
# Nhờ ② người dùng viết được:
# from cua_hang import SanPham ← thay vì cua_hang.san_pham.SanPham
__all__ làm gì chính xác?
__all__ là danh sách tên mà from module import * sẽ lấy. Không có nó,
import * lấy mọi tên không bắt đầu bằng _. Nhưng công dụng thật sự lớn hơn:
nó là tài liệu về public API — người đọc biết ngay đâu là thứ được cam kết ổn định, đâu
là chi tiết nội bộ có thể đổi bất cứ lúc nào. Các type checker và công cụ sinh tài liệu cũng đọc
__all__.
__init__.py nặng làm chậm cả chương trình
Mỗi dòng from ... import ... trong __init__.py khiến module con bị nạp
ngay khi ai đó chạm vào package, dù họ chỉ cần một hàm nhỏ. Nếu package có 30 module và
__init__.py import hết, mọi lệnh import cua_hang đều tốn thời gian nạp cả 30.
Đây là lý do import pandas mất gần một giây. Với package lớn, cân nhắc để
__init__.py gần như rỗng, hoặc dùng __getattr__ cấp module (PEP 562) để nạp lười.
5.2. Namespace package — thư mục không có __init__.py
Từ Python 3.3 (PEP 420), thư mục không có __init__.py vẫn import được — gọi là
namespace package. Tính năng này sinh ra cho tình huống đặc thù: một package logic được
chia thành nhiều mảnh cài đặt riêng (như zope.*). Trong dự án thường ngày, sự vắng mặt của
__init__.py thường là quên chứ không phải cố ý, và nó gây ra những lỗi rất khó chẩn
đoán khi hai thư mục cùng tên nằm trên sys.path.
Luôn tạo __init__.py (dù rỗng) cho package của bạn. Nó khiến ý định rõ ràng, giúp công cụ
(pytest, mypy, setuptools) nhận diện package chính xác, và tránh xung đột namespace ngoài ý muốn.
6. Absolute vs relative import
# ✅ ABSOLUTE — nêu đường dẫn đầy đủ từ gốc
from cua_hang.san_pham import SanPham
from cua_hang.thanh_toan.vi_dien_tu import ViDienTu
# RELATIVE — nêu vị trí tương đối so với module hiện tại
from .san_pham import SanPham # . = cùng package (cua_hang)
from .thanh_toan.vi_dien_tu import ViDienTu # đi xuống sub-package
from ..tien_ich import dinh_dang # .. = package cha
| Absolute | Relative | |
|---|---|---|
| Cú pháp | from goi.con import x | from .con import x |
| Đọc hiểu | Biết ngay module ở đâu | Phải biết mình đang ở đâu |
| Đổi tên package | Phải sửa mọi import | Không cần sửa gì |
| Di chuyển file | Không ảnh hưởng | Phải sửa số dấu chấm |
| Chạy file trực tiếp | Được (nếu sys.path đúng) | Lỗi — xem bên dưới |
| PEP 8 khuyến nghị | Mặc định | Chấp nhận được cho layout package phức tạp |
$ python cua_hang/gio_hang.py
Traceback (most recent call last):
File "cua_hang/gio_hang.py", line 1, in <module>
from .san_pham import SanPham
ImportError: attempted relative import with no known parent package
Nguyên nhân: khi chạy python cua_hang/gio_hang.py, Python đặt
__name__ = "__main__" và __package__ = None. Không có "package cha" nào để dấu
. tham chiếu tới — Python không suy ra package từ vị trí file trên đĩa.
Cách sửa — chạy như module, đứng từ thư mục cha của package:
$ python -m cua_hang.gio_hang # ✅ __package__ = "cua_hang", relative import chạy tốt
7. sys.path — Python tìm module ở đâu
Khi cache trượt, Python duyệt danh sách sys.path theo thứ tự và dừng ở nơi
đầu tiên tìm thấy. Đây là danh sách chuỗi bình thường, có thể in ra để soi.
import sys
for i, p in enumerate(sys.path):
print(f"{i}. {p or '(thư mục hiện tại)'}")
# 0. /duong/dan/toi/script ← thư mục chứa file đang chạy
# 1. /usr/lib/python312.zip
# 2. /usr/lib/python3.12
# 3. /usr/lib/python3.12/lib-dynload
# 4. /duong/dan/.venv/lib/python3.12/site-packages ← pip install vào đây
| Thứ tự | Nguồn | Ghi chú |
|---|---|---|
| 1 | Thư mục chứa script đang chạy (hoặc "" = cwd với -m/REPL) | Ưu tiên cao nhất — nguồn của bẫy shadowing |
| 2 | Biến môi trường PYTHONPATH | Tách bằng : (Unix) / ; (Windows) |
| 3 | Thư mục stdlib | json, os, re… |
| 4 | site-packages | Nơi pip install đặt thư viện bên thứ ba |
# Bạn tạo file random.py để thử nghiệm...
# duan/
# ├── random.py ← file của bạn
# └── main.py
# Trong main.py:
import random
print(random.randint(1, 10))
# AttributeError: module 'random' has no attribute 'randint'
# → vì thư mục hiện tại đứng ĐẦU sys.path, file của bạn thắng stdlib
Tên hay bị trùng nhất: random.py, json.py, email.py,
types.py, string.py, copy.py, test.py,
queue.py, logging.py. Cách kiểm tra nhanh khi gặp
AttributeError lạ trên một module stdlib: in module.__file__ ra xem nó trỏ vào đâu.
8. python file.py vs python -m goi.module
python cua_hang/gio_hang.py | python -m cua_hang.gio_hang | |
|---|---|---|
sys.path[0] | Thư mục cua_hang/ | Thư mục hiện tại (cwd) |
__name__ | "__main__" | "__main__" |
__package__ | None hoặc "" | "cua_hang" |
| Relative import | ❌ Lỗi | ✅ Hoạt động |
__init__.py của package | Không chạy | Có chạy |
| Dùng cho | Script độc lập một file | Module bên trong package |
-m còn dùng để chạy công cụ
Bạn đã gặp -m ở Chương 1 mà có thể chưa để ý:
python -m venv .venv, python -m pip install X. Cú pháp này chạy module dưới
dạng script và đảm bảo dùng đúng interpreter hiện tại — quan trọng khi máy có nhiều Python.
Vài lệnh hữu ích khác: python -m http.server 8000 (server file tĩnh tại chỗ),
python -m json.tool data.json (format JSON đẹp),
python -m timeit "sum(range(100))" (đo tốc độ),
python -m this (in Zen of Python).
8.1. __main__.py — cho phép chạy cả package
Nếu package có file __main__.py, bạn chạy được cả package: python -m cua_hang.
Đây chính là cách python -m venv và python -m http.server hoạt động.
"""Điểm vào CLI của package: python -m cua_hang"""
import sys
from cua_hang.gio_hang import GioHang
def main(argv: list[str] | None = None) -> int:
argv = argv if argv is not None else sys.argv[1:]
gio = GioHang()
for ma in argv:
gio.them(ma)
print(f"Tổng: {gio.tong_tien():,}đ")
return 0
if __name__ == "__main__":
sys.exit(main())
main() nên trả về int
Trả về mã thoát (0 = thành công, khác 0 = lỗi) và bọc bằng sys.exit(main()) cho phép shell
script và CI kiểm tra kết quả bằng $?. Tách main(argv) nhận tham số cũng giúp
test được — gọi main(["A", "B"]) trực tiếp trong pytest mà không cần giả
lập dòng lệnh.
9. Cấu trúc dự án chuẩn — src layout
Cấu trúc dưới đây là quy ước được cộng đồng Python đồng thuận rộng rãi cho dự án thật, và cũng là thứ
uv init --lib tạo ra:
du-an-cua-toi/
├── pyproject.toml # metadata + dependency (Chương 1)
├── README.md
├── .gitignore
├── src/
│ └── cua_hang/ # package thật nằm trong src/
│ ├── __init__.py
│ ├── __main__.py # python -m cua_hang
│ ├── san_pham.py
│ ├── gio_hang.py
│ └── thanh_toan/
│ ├── __init__.py
│ └── the_tin_dung.py
└── tests/
├── test_san_pham.py
└── test_gio_hang.py
src/?
Nếu package nằm thẳng ở gốc dự án, thư mục gốc luôn nằm trên sys.path khi bạn chạy test —
nên test sẽ import code trong thư mục làm việc, không phải package đã cài đặt. Hậu quả:
test xanh trên máy bạn nhưng người dùng cài từ PyPI lại lỗi, vì bạn quên khai báo một file dữ liệu trong
pyproject.toml. Với src/, thư mục gốc không chứa package, nên cách duy
nhất để test import được là cài package (pip install -e .) — tức là test chạy trên đúng thứ
người dùng sẽ nhận.
[project]
name = "cua-hang"
version = "1.2.0"
requires-python = ">=3.12"
dependencies = ["httpx>=0.27"]
[project.scripts]
cua-hang = "cua_hang.__main__:main" # tạo lệnh `cua-hang` trong terminal
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/cua_hang"] # chỉ cho build backend biết package ở đâu
$ uv pip install -e . # hoặc: pip install -e .
$ python -c "import cua_hang; print(cua_hang.__version__)"
1.2.0
$ cua-hang SP001 SP002 # lệnh sinh từ [project.scripts]
Tổng: 500,000đ
Cờ -e (editable) tạo liên kết tới thư mục nguồn thay vì sao chép — sửa code là thấy hiệu lực
ngay, không cần cài lại. Chi tiết đóng gói và publish lên PyPI ở
Chương 13.
10. Circular import — chẩn đoán và gỡ
Circular import xảy ra khi A import B, mà B lại import A. Vì import thực thi
module, Python có thể chạm vào một module đang nạp dở — và thấy nó thiếu tên.
# nguoi_dung.py
from don_hang import DonHang
class NguoiDung:
def don_hang_moi_nhat(self) -> DonHang: ...
# don_hang.py
from nguoi_dung import NguoiDung # ⚠️ vòng!
class DonHang:
def chu_don(self) -> NguoiDung: ...
# ImportError: cannot import name 'DonHang' from partially initialized module
# 'don_hang' (most likely due to a circular import)
Thông điệp "partially initialized module" chính là manh mối: khi
nguoi_dung.py chạy dòng đầu, Python bắt đầu nạp don_hang; dòng đầu của
don_hang lại quay về nạp nguoi_dung — nhưng nguoi_dung mới chạy được
một dòng, class NguoiDung chưa tồn tại.
| Cách gỡ | Làm thế nào | Khi nào phù hợp |
|---|---|---|
| 1. Tái cấu trúc ⭐ | Tách phần dùng chung ra module thứ ba (models.py, types.py) |
Luôn là lựa chọn tốt nhất — vòng lặp thường là dấu hiệu thiết kế chưa đúng |
2. import x thay from x import y |
Chỉ tham chiếu x.y lúc chạy hàm, không phải lúc import |
Sửa nhanh, ít rủi ro |
| 3. Import trong hàm | Đặt import bên trong thân hàm |
Khi vòng lặp chỉ ở một vài chỗ; trả giá bằng lookup mỗi lần gọi |
4. TYPE_CHECKING ⭐ |
Import chỉ cho type checker, không chạy lúc runtime | Khi vòng lặp chỉ do type hint — trường hợp cực kỳ phổ biến |
# don_hang.py
from __future__ import annotations # hoãn đánh giá annotation
from typing import TYPE_CHECKING
if TYPE_CHECKING: # False lúc runtime, True với mypy/Pylance
from nguoi_dung import NguoiDung
class DonHang:
def chu_don(self) -> NguoiDung: # chỉ là chuỗi lúc runtime → không cần import thật
from nguoi_dung import tim_theo_id # import thật ở đây, khi module đã nạp xong
return tim_theo_id(self.chu_don_id)
from __future__ import annotationsDòng này bảo Python không đánh giá các annotation lúc runtime — chúng được giữ nguyên dưới dạng chuỗi. Nhờ vậy bạn viết được type hint tham chiếu tới class chưa tồn tại (forward reference) mà không cần đặt trong ngoặc kép. Nó phải là câu lệnh đầu tiên trong file (sau docstring). Chương 12 sẽ đào sâu.
11. Bản đồ stdlib — "batteries included"
Điểm mạnh lớn nhất của Python là thư viện chuẩn khổng lồ. Trước khi pip install bất cứ thứ gì,
hãy tự hỏi: stdlib có sẵn chưa?
| Nhóm | Module | Dùng để | Học ở |
|---|---|---|---|
| Hệ thống | os, sys | Biến môi trường, đường dẫn, tiến trình, argv | Chương 1, 7 |
pathlib | Thao tác đường dẫn hướng đối tượng ⭐ | Chương 7 | |
subprocess | Chạy lệnh hệ thống | Chương 14 | |
| Dữ liệu | json, csv | Đọc/ghi định dạng phổ biến | Chương 7 |
re | Biểu thức chính quy | Chương 7 | |
sqlite3 | Database nhúng, không cần server | Chương 14 | |
| Cấu trúc | collections | Counter, defaultdict, deque | Chương 3 |
itertools | Tổ hợp, lười, vô hạn | Chương 9 | |
functools | cache, partial, wraps, reduce | Chương 10 | |
| Thời gian | datetime, zoneinfo | Ngày giờ, múi giờ | Chương 14 |
time | Timestamp, đo thời gian, sleep | Chương 10 | |
| Ứng dụng | argparse | Phân tích tham số dòng lệnh | Chương 14 |
logging | Ghi log có cấp độ | Chương 5, 14 | |
| Nâng cao | asyncio | Lập trình bất đồng bộ | Chương 11 |
typing, dataclasses | Type hint, class dữ liệu | Chương 8, 12 |
# ① Thư viện tuỳ chọn — chạy được cả khi chưa cài
try:
import orjson as json_lib # nhanh hơn nhiều lần
except ImportError:
import json as json_lib # dự phòng bằng stdlib
# ② Import lười — module nặng chỉ nạp khi thật sự cần
def ve_bieu_do(du_lieu):
import matplotlib.pyplot as plt # ~1s để nạp — đừng bắt mọi user trả giá
plt.plot(du_lieu)
# ③ Kiểm tra tính khả dụng mà không nạp
import importlib.util
co_pandas = importlib.util.find_spec("pandas") is not None
12. Cheat sheet chương 6
| Tình huống | Cách làm |
|---|---|
| Tạo module | Tạo file ten.py — xong, không cần khai báo gì |
| Tạo package | Tạo thư mục + file __init__.py (dù rỗng) |
| Import an toàn nhất | import goi.module rồi dùng goi.module.ham() |
| Import trong cùng package | from .anh_em import X hoặc absolute đầy đủ |
| Chạy module trong package | python -m goi.module (không phải python goi/module.py) |
| Chạy cả package | Tạo __main__.py rồi python -m goi |
| Khai báo public API | __all__ = ["A", "B"] trong __init__.py |
| Xem module đến từ đâu | print(module.__file__) |
| Xem Python tìm ở đâu | import sys; print(sys.path) |
| Gỡ circular import do type hint | if TYPE_CHECKING: + from __future__ import annotations |
| Cài package đang phát triển | uv pip install -e . |
| Nạp lại khi đang code | importlib.reload(module) |
Bài tập
Tách một script thành package
Cho một file quan_ly.py dài 300 dòng chứa lẫn lộn: 3 class model, 5 hàm tính toán,
2 hàm đọc/ghi file, và phần CLI ở cuối. Hãy tách thành package theo src layout:
src/quan_ly/
├── __init__.py # nâng 3 class lên mặt tiền + __all__ + __version__
├── __main__.py # phần CLI, có hàm main() trả int
├── models.py # 3 class
├── tinh_toan.py # 5 hàm tính toán
└── luu_tru.py # 2 hàm đọc/ghi
Yêu cầu: dùng absolute import giữa các module; chạy được cả
python -m quan_ly lẫn from quan_ly import ClassA; thêm
pyproject.toml tối thiểu và cài bằng pip install -e ..
Gợi ý
Thứ tự phụ thuộc nên là: models (không import gì của package) →
tinh_toan + luu_tru (import models) →
__main__ (import cả ba). Giữ chiều phụ thuộc một hướng là cách chắc chắn
nhất để không bao giờ gặp circular import.
Chứng minh cơ chế cache của sys.modules
Tạo bo_dem.py:
print("[bo_dem] đang khởi tạo...")
import random
MA_PHIEN = random.randint(1000, 9999)
print(f"[bo_dem] mã phiên = {MA_PHIEN}")
Tạo a.py và b.py cùng import nó, rồi main.py import cả hai. Trả lời:
- Dòng
"[bo_dem] đang khởi tạo..."in ra mấy lần? Vì sao? a.MA_PHIEN == b.MA_PHIEN— đúng hay sai? Vì sao?- Sau
del sys.modules["bo_dem"]rồiimport bo_demlại,MA_PHIENcó đổi không? - Nếu
a.pyviếtfrom bo_dem import MA_PHIENrồi bạnimportlib.reload(bo_dem), biếna.MA_PHIENcó cập nhật không?
Gợi ý
Câu cuối là câu quan trọng nhất: from X import Y tạo một binding mới trong
namespace của bạn trỏ tới object; reload thay object trong module X nhưng binding cũ
của bạn vẫn trỏ vào object cũ. Đây chính là lý do import X + dùng X.Y
an toàn hơn khi cần reload.
Tái hiện và gỡ 3 lỗi import kinh điển
Với mỗi lỗi: tạo tình huống gây ra nó, chép lại thông điệp lỗi đầy đủ, rồi sửa và giải thích.
ImportError: attempted relative import with no known parent packageModuleNotFoundErrordù file nằm ngay cạnh (gợi ý: chạy từ thư mục khác)AttributeError: module 'random' has no attribute 'randint'(bẫy shadowing)
Gợi ý
Với lỗi ②, in sys.path[0] ra sẽ thấy ngay: nó là thư mục chứa file đang chạy,
không phải thư mục bạn đang đứng. Với lỗi ③, print(random.__file__) lộ ra thủ phạm.
Ghi lại "cách chẩn đoán" cho từng lỗi — đó mới là thứ bạn cần nhớ, không phải cách sửa.
Gỡ circular import bằng cả 4 cách
Tạo tình huống vòng lặp nguoi_dung.py ↔ don_hang.py như trong bài, rồi viết 4 phiên bản sửa:
- (a) tách
models.pychứa cả hai class - (b) đổi
from x import ythànhimport x - (c) chuyển import vào trong hàm
- (d) dùng
TYPE_CHECKING+from __future__ import annotations
Với mỗi cách, ghi lại: sửa mấy dòng, đọc còn dễ không, có ảnh hưởng tốc độ không, mypy có còn kiểm tra được kiểu không.
Gợi ý
Kết luận bạn nên rút ra: (a) tốt nhất về lâu dài nhưng tốn công; (d) tốt nhất khi vòng lặp chỉ do type hint — giữ nguyên khả năng kiểm tra kiểu mà không tốn gì lúc runtime; (b) sửa nhanh an toàn; (c) là giải pháp cuối cùng, dễ che giấu vấn đề thiết kế thật.
Đo chi phí import
Python có công cụ sẵn để đo thời gian nạp từng module:
$ python -X importtime -c "import json" # nhẹ
$ python -X importtime -c "import asyncio" # nặng hơn
$ python -X importtime main.py 2>&1 | sort -k2 -rn | head -15
Viết một package nhỏ có __init__.py import cả 5 module con, đo thời gian
import goi. Sau đó rút __init__.py về rỗng, đo lại. So sánh và giải thích
con số. Cuối cùng, cài pandas hoặc numpy và đo import pandas —
bạn sẽ hiểu vì sao CLI viết bằng Python thường bị chê chậm khởi động.
Gợi ý
Cột cumulative gồm cả thời gian nạp các module phụ thuộc; cột self chỉ
tính riêng module đó. Sắp xếp theo self giảm dần để tìm thủ phạm thật. Kỹ thuật
giảm chi phí: import lười trong hàm, hoặc __getattr__ cấp module (PEP 562).
Quiz
Import cùng một module ba lần thì code trong module chạy mấy lần? Vì sao?
Xem đáp án
Một lần. Lần import đầu tiên, Python thực thi toàn bộ file rồi lưu module object vào
từ điển sys.modules với khóa là tên module. Mọi lần import sau chỉ tra từ
điển đó và gắn tên vào namespace hiện tại — không chạy lại gì cả. Hệ quả: biến cấp module là trạng
thái dùng chung toàn tiến trình (module là singleton tự nhiên của Python), và mọi tác dụng phụ ở cấp
module (mở kết nối, đọc file, in ra màn hình) chỉ xảy ra đúng một lần — nhưng bạn không kiểm
soát được khi nào, nên đừng đặt tác dụng phụ ở đó.
if __name__ == "__main__": để làm gì? Chuyện gì xảy ra nếu bỏ nó?
Xem đáp án
Nó phân biệt "file đang được chạy trực tiếp" (__name__ == "__main__") với
"file đang được import" (__name__ = tên module). Nhờ vậy một file vừa là thư
viện vừa là script.
Bỏ nó thì code CLI chạy mỗi khi ai đó import module. Ba hậu quả thực tế:
(1) pytest import file để thu thập test và vô tình chạy chương trình;
(2) multiprocessing trên macOS/Windows import lại file chính ở tiến trình con → mỗi tiến
trình con lại spawn tiếp → fork bomb; (3) công cụ sinh tài liệu (Sphinx, pydoc)
import module để đọc docstring và cũng chạy code của bạn.
Vì sao python goi/module.py lỗi với relative import, còn python -m goi.module thì không?
Xem đáp án
Relative import (from .x import y) được giải quyết dựa vào biến
__package__, không dựa vào vị trí file trên đĩa.
Với python goi/module.py: Python coi file là script độc lập, đặt
__package__ = None — không có "package cha" nào để dấu . tham chiếu tới, nên
ném ImportError: attempted relative import with no known parent package. Ngoài ra
sys.path[0] là thư mục goi/ chứ không phải thư mục cha.
Với python -m goi.module: Python nạp goi như package thật (chạy cả
goi/__init__.py), đặt __package__ = "goi", và đặt sys.path[0]
là thư mục hiện tại — nên relative import phân giải đúng.
Vì sao from module import * bị coi là thói quen xấu?
Xem đáp án
Bốn lý do: (1) Ô nhiễm namespace — kéo hàng chục tên vào, không biết tên nào từ đâu.
(2) Ghi đè âm thầm — from os import * rồi from time import *
khiến open trỏ vào os.open thay vì built-in, gây TypeError ở
một dòng trông hoàn toàn bình thường. (3) Công cụ mù — linter không biết tên nào
được dùng, IDE không nhảy tới định nghĩa được. (4) Dễ vỡ — thư viện thêm một tên mới
ở phiên bản sau là code của bạn đổi hành vi.
Ngoại lệ duy nhất được chấp nhận: gõ nhanh trong REPL để thử nghiệm.
Tại sao dự án nên đặt package trong thư mục src/?
Xem đáp án
Để test chạy trên package đã cài đặt, không phải trên thư mục làm việc. Nếu package
nằm thẳng ở gốc dự án, thư mục gốc luôn có mặt trong sys.path, nên
import goi lấy thẳng thư mục nguồn — kể cả những file bạn quên khai báo trong
pyproject.toml. Kết quả: test xanh trên máy bạn, người dùng cài từ PyPI lại thiếu file
và lỗi. Với src/, gốc dự án không chứa package nên cách duy nhất để import được là cài
đặt (pip install -e .) — mọi thiếu sót về đóng gói lộ ra ngay khi chạy test.
Bạn tạo random.py trong thư mục dự án. Chuyện gì xảy ra và vì sao?
Xem đáp án
File của bạn che khuất module random của stdlib, vì
sys.path[0] — thư mục chứa script đang chạy — đứng trước thư mục stdlib. Mọi
import random trong dự án (kể cả từ thư viện bên thứ ba!) sẽ nhận file của bạn, và
random.randint(...) ném AttributeError: module 'random' has no attribute
'randint'.
Chẩn đoán: import random; print(random.__file__) — nếu nó trỏ vào thư
mục dự án thay vì thư mục Python thì đã rõ. Sửa: đổi tên file (và nhớ xoá
__pycache__/random.cpython-312.pyc). Các tên hay bị trùng khác:
json.py, email.py, types.py, string.py,
copy.py, queue.py, logging.py, test.py.
Circular import xảy ra khi nào, và cách gỡ nào sạch nhất cho trường hợp chỉ do type hint?
Xem đáp án
Xảy ra khi A import B và B import A. Vì import thực thi module, nên khi B chạy
dòng from A import X, module A mới nạp được vài dòng và class X chưa tồn
tại → ImportError: cannot import name 'X' from partially initialized module.
Nếu vòng lặp chỉ do type hint (rất phổ biến), cách sạch nhất là:
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from a import X # mypy thấy, runtime không chạy
TYPE_CHECKING là hằng False lúc runtime nên khối đó không bao giờ thực thi;
mypy/Pylance thì coi nó là True nên vẫn kiểm tra kiểu đầy đủ. Về lâu dài, giải pháp tốt
nhất vẫn là tái cấu trúc: tách phần dùng chung ra module thứ ba và giữ chiều phụ thuộc một hướng.
__init__.py rỗng và __init__.py có nhiều from ... import ... — khác nhau ra sao?
Xem đáp án
Rỗng: package chỉ là namespace; người dùng phải viết đường dẫn đầy đủ
from goi.con import X. Import nhanh vì không nạp gì thêm.
Có import: nâng API con lên "mặt tiền", người dùng viết ngắn gọn
from goi import X. Đổi lại, mọi lệnh chạm vào package đều kéo theo việc
nạp tất cả module được liệt kê — dù người dùng chỉ cần một hàm nhỏ. Đây là lý do
import pandas mất gần một giây.
Cân bằng: package nhỏ (dưới ~10 module) thì nâng mặt tiền cho tiện dùng; package lớn
thì giữ __init__.py gọn, hoặc dùng __getattr__ cấp module (PEP 562) để nạp
lười. Dù chọn cách nào, luôn khai báo __all__ để tuyên bố rõ public API.
Tổng kết
Sau chương 6, bạn đã master:
- Module = một file
.py; mọi tên cấp cao nhất đều public;_tenchỉ là quy ước. - Bốn dạng import và đánh đổi của từng dạng; vì sao
import *gần như luôn sai. - Cơ chế import 4 bước: tra
sys.modules→ tìm trênsys.path→ thực thi file → gắn tên. - Module là singleton — chạy đúng một lần cho cả tiến trình.
__name__và main guard — bắt buộc, không phải tuỳ chọn.- Package:
__init__.pylàm mặt tiền,__all__tuyên bố public API,__main__.pychopython -m goi. - Absolute vs relative: absolute là mặc định; relative cần
__package__nên phải chạy bằng-m. sys.pathvà bẫy shadowing tên stdlib.- src layout — cấu trúc dự án thật, cài bằng
pip install -e .. - Circular import: 4 cách gỡ, ưu tiên tái cấu trúc và
TYPE_CHECKING.
Kết nối
- Chương 1 (Hello Python) —
venvquyết địnhsite-packagesnào có mặt trênsys.path;pyproject.tomlkhai báo package. - Chương 5 (Exceptions) —
ImportError/ModuleNotFoundError; mẫutry: import nhanh except ImportError: import cham. - Chương 7 (File I/O & Regex) —
pathlib,json,csv,re: những module stdlib bạn sẽ dùng hằng ngày. - Chương 12 (Typing) —
TYPE_CHECKING,from __future__ import annotations, file.pyistub cho module không có type hint. - Chương 13 (Testing & Packaging) —
conftest.pylà module đặc biệt của pytest; build wheel và publish lên PyPI từ chính src layout ở chương này. - Chương 14 (Dự án) — dựng CLI thật với
argparse+__main__.py+[project.scripts]. - JavaScript Chương 8 (Modules) — đối chiếu ESM:
exporttường minh, import là bất đồng bộ và có hoisting, khác hẳn cơ chế thực thi tuần tự của Python. - Dart Chương 5 (Libraries) —
library/part/export, và_là private thật sự do compiler chặn.