Trong một dự án dữ liệu hay AI, thứ làm mất nhiều giờ nhất thường không phải thuật toán mà là câu hỏi "sao trên máy tôi chạy được?". Một notebook dùng numpy bản này, máy đồng nghiệp cài bản khác; server production lại chạy Python cũ hơn một phiên bản. Bài này trình bày cách dựng một môi trường Python tách biệt và tái lập được cho từng dự án: dùng venv và pip có sẵn của thư viện chuẩn, dùng Poetry khi cần khoá phiên bản chặt hơn, và quản lý nhiều phiên bản interpreter song song bằng pyenv.
Toàn bộ lệnh dựng môi trường trong bài được chạy thật trên Python 3.10 (dòng phát hành tháng 10/2021, là dòng ổn định mới nhất thời điểm bài đăng; máy chạy dùng bản vá 3.10 mới nhất nên dòng 3.10.22 trong output là số bản vá, còn các gói ứng dụng được ghim ở phiên bản đầu năm 2022), Poetry 1.1.12, máy Apple M4 16 GB; output được dán lại, chỉ rút gọn hoặc che đường dẫn máy và lược một số dòng thông báo của pip. Riêng ví dụ di chuyển venv, pip được ghim về 21.3.1 (tháng 10/2021) cho khớp thời điểm bài.
Vì sao mỗi dự án cần một môi trường Python riêng
Khi cài gói bằng pip install trực tiếp vào interpreter hệ thống, mọi dự án trên máy dùng chung một thư mục site-packages. Hậu quả quen thuộc: dự án A cần requests==2.27, dự án B cần bản cũ hơn vì một thư viện nội bộ chưa cập nhật; nâng cấp cho dự án này làm hỏng dự án kia. Trên macOS và Linux còn có rủi ro lớn hơn: Python của hệ điều hành phục vụ cả công cụ hệ thống, gỡ hay nâng gói bừa bãi có thể làm hỏng chính những công cụ đó.
Giải pháp là cô lập: mỗi môi trường Python gắn với một dự án, có một interpreter và một thư mục site-packages riêng. Nguyên tắc này gần với những gì Nguyên lý SOLID trong Java qua ví dụ thực tế nói về phụ thuộc: một thành phần không nên bị buộc vào chi tiết của thứ khác. Ở tầng môi trường Python, "chi tiết" là phiên bản gói. Một môi trường Python tốt phải đạt ba tiêu chí:
- Cô lập: mỗi môi trường Python tách biệt, nên cài hay gỡ gói ở dự án này không ảnh hưởng dự án khác hoặc hệ thống.
- Tái lập: môi trường Python phải dựng lại được: đồng nghiệp, máy CI và server dựng lại đúng tập phiên bản gói đã chạy tốt trên máy bạn.
- Dễ vứt bỏ: môi trường Python hỏng thì xoá và dựng lại trong vài phút, không cần "sửa" môi trường.
venv: môi trường Python có sẵn trong thư viện chuẩn
Module venv, công cụ tạo môi trường Python chuẩn, đi kèm Python từ phiên bản 3.3 (PEP 405) và được mô tả đầy đủ trong tài liệu venv của Python. Lệnh python -m venv <thư mục> tạo thư mục chứa bản sao hoặc symlink của interpreter, thư mục lib/pythonX.Y/site-packages trống, tập lệnh kích hoạt và file pyvenv.cfg. Pip được cài sẵn thông qua ensurepip.
Script dưới đây in ra thông tin về môi trường Python đang chạy, dùng xuyên suốt bài để so sánh "trước" và "sau":
"""In thông tin môi trường Python đang chạy (đường dẫn in tương đối so với thư mục hiện tại)."""
import os
import sys
from importlib import metadata
def rel(p: str) -> str:
return os.path.relpath(p) if p.startswith(os.getcwd()) else p
in_venv = sys.prefix != sys.base_prefix
print("python :", sys.version.split()[0])
print("trong venv :", in_venv)
print("sys.prefix :", rel(sys.prefix))
for name in ("requests", "pytest", "numpy"):
try:
print(f"{name:<12}:", metadata.version(name))
except metadata.PackageNotFoundError:
print(f"{name:<12}: (chưa cài)")Điểm đáng chú ý là điều kiện sys.prefix != sys.base_prefix: đây là cách chính thức để một chương trình Python biết mình đang chạy trong venv hay không, vì biến VIRTUAL_ENV chỉ có khi bạn đã activate. Chạy lần lượt: interpreter hệ thống, tạo môi trường Python bằng venv, rồi gọi trực tiếp python trong venv.
$ python3.10 --version
Python 3.10.22
$ python3.10 check_env.py # interpreter hệ thống
python : 3.10.22
trong venv : False
sys.prefix : <python-3.10>
requests : (chưa cài)
pytest : (chưa cài)
numpy : (chưa cài)
$ python3.10 -m venv .venv-pip
$ cat .venv-pip/pyvenv.cfg | sed "s#^home = .*#home = <thư mục python gốc>#"
home = <thư mục python gốc>
include-system-site-packages = false
version = 3.10.22
$ ls .venv-pip
bin
include
lib
pyvenv.cfg
$ .venv-pip/bin/python check_env.py # không cần activate
python : 3.10.22
trong venv : True
sys.prefix : .venv-pip
requests : (chưa cài)
pytest : (chưa cài)
numpy : (chưa cài)Có ba quan sát từ output về môi trường Python vừa tạo. Thứ nhất, pyvenv.cfg chỉ có vài dòng: khoá home trỏ về interpreter gốc (dòng này tôi thay bằng chữ giữ chỗ để không lộ đường dẫn máy), include-system-site-packages = false nghĩa là venv không nhìn thấy gói của hệ thống. Thứ hai, venv chỉ có bốn mục (bin, include, lib, pyvenv.cfg), nên tạo rất nhanh. Thứ ba, tôi không cần activate: gọi .venv-pip/bin/python là đủ, và sys.prefix đã trỏ vào venv. Tài liệu Python nêu rõ activate chỉ là tiện lợi (thêm bin vào đầu PATH); trong script CI hay Dockerfile, gọi thẳng đường dẫn tuyệt đối còn rõ ràng hơn.
Cài gói và ghim phiên bản bằng pip
Quy trình tối thiểu cho một môi trường Python tái lập được bằng pip gồm hai file. File thứ nhất khai báo thứ bạn muốn (phụ thuộc trực tiếp), file thứ hai ghi thứ bạn đã có (mọi gói, kể cả phụ thuộc gián tiếp, đều có số phiên bản chính xác). Tài liệu Repeatable Installs của pip mô tả đúng mô hình này: ghim bằng ==, sinh danh sách đầy đủ bằng pip freeze, và ở mức chặt hơn là kiểm tra hash của từng file tải về.
requests==2.27.1
numpy==1.22.0Gói requests kéo theo bốn phụ thuộc gián tiếp (certifi, charset-normalizer, idna, urllib3). Nếu chỉ ghim requests, mỗi lần cài mới bạn có thể nhận phiên bản khác của bốn gói còn lại, và đó chính là nguồn gốc của lỗi "hôm qua còn chạy". Một cách đơn giản là dùng thêm constraints file (tùy chọn -c của pip) để áp trần phiên bản cho các gói gián tiếp mà không buộc phải cài chúng:
# Ghim các gói phụ ở phiên bản đầu năm 2022 để kết quả tái lập được
certifi==2021.10.8
charset-normalizer==2.0.10
idna==3.3
urllib3==1.26.8Sau đó cài, freeze ra requirements.txt, rồi chứng minh tính tái lập bằng cách xoá venv và dựng lại từ file đó:
$ .venv-pip/bin/pip install -c constraints.txt -r requirements.in
$ .venv-pip/bin/pip freeze
certifi==2021.10.8
charset-normalizer==2.0.10
idna==3.3
numpy==1.22.0
requests==2.27.1
urllib3==1.26.8
$ .venv-pip/bin/python check_env.py
python : 3.10.22
trong venv : True
sys.prefix : .venv-pip
requests : 2.27.1
pytest : (chưa cài)
numpy : 1.22.0
$ PATH tuỳ chọn: which python sau khi source activate
VIRTUAL_ENV=.venv-pip
<demo>/.venv-pip/bin/python
$ xoá venv và dựng lại từ requirements.txt
python : 3.10.22
trong venv : True
sys.prefix : .venv-pip
requests : 2.27.1
pytest : (chưa cài)
numpy : 1.22.0Môi trường Python dựng lại từ requirements.txt cho đúng kết quả: requests 2.27.1, numpy 1.22.0. Đó là điều kiện tối thiểu để tuyên bố một môi trường Python là "tái lập được". Chi phí là bạn phải tự giữ hai file đồng bộ; công cụ pip-tools (lệnh pip-compile) tự động hoá việc biên dịch từ requirements.in sang file đã ghim, và là lựa chọn nhẹ nhất trước khi chuyển sang Poetry.
Vì sao không được copy hoặc di chuyển venv
Tài liệu venv khuyến cáo: đừng di chuyển hay sao chép venv, hãy dựng lại. Lý do nằm ở shebang. Các lệnh như pip trong bin/ là script có dòng đầu chứa đường dẫn tuyệt đối tới interpreter lúc tạo. Thử tạo venv rồi đổi tên thư mục (script run_move.sh) để thấy:
$ head -1 /tmp/venv-a/bin/pip
#!/tmp/venv-a/bin/python
$ mv /tmp/venv-a /tmp/venv-b && /tmp/venv-b/bin/pip --version
run_move.sh: /tmp/venv-b/bin/pip: /tmp/venv-a/bin/python: bad interpreter: No such file or directory
$ /tmp/venv-b/bin/python -m pip --version # python vẫn chạy, nhưng script thì không
pip 21.3.1 from /tmp/venv-b/lib/python3.10/site-packages/pip (python 3.10)Script pip hỏng (bad interpreter) vì vẫn trỏ về thư mục cũ, trong khi python -m pip còn chạy được. Hệ quả thực tế với môi trường Python: không bao giờ commit thư mục .venv vào Git, không đóng gói nó vào Docker image bằng cách copy từ máy dev, và đừng đổi tên thư mục dự án mà quên dựng lại. Hãy thêm .venv/ vào .gitignore và commit requirements.txt thay thế.
Poetry: quản lý môi trường Python, khai báo phụ thuộc và khoá phiên bản
Poetry gộp nhiều việc rời rạc ở trên vào một công cụ quản lý môi trường Python: khai báo phụ thuộc trong pyproject.toml, giải phụ thuộc (dependency resolution), ghi poetry.lock, tạo venv và cài gói. File pyproject.toml ra đời từ PEP 518 (tạo năm 2016) để các công cụ khai báo yêu cầu build một cách tĩnh; bảng [build-system] là phần bắt buộc của chuẩn, còn bảng [tool.poetry] là chỗ Poetry giữ cấu hình riêng của mình. Hướng dẫn cơ bản của Poetry nêu rõ hai hành vi cần nhớ: nếu chưa có poetry.lock, poetry install sẽ giải phụ thuộc và tạo lock; nếu đã có lock, nó cài đúng các phiên bản đã khoá. Bạn có thể đối chiếu trong tài liệu Basic usage của Poetry.
File pyproject.toml của demo như sau. Cú pháp ^3.10 là "caret": theo tài liệu đặc tả phụ thuộc, ^1.2.3 nghĩa là >=1.2.3 <2.0.0, còn dấu ~1.2.3 chỉ cho phép đổi phiên bản vá (>=1.2.3 <1.3.0). Riêng với các gói phiên bản 0.x, caret chặt hơn: ^0.2.3 chỉ nhận >=0.2.3 <0.3.0. Trong demo, tôi ghim chính xác để kết quả tái lập được, nhưng với dự án thật thường dùng caret cho phụ thuộc trực tiếp và để file lock giữ phiên bản chính xác.
[tool.poetry]
name = "bao-cao-don-hang"
version = "0.1.0"
description = "Demo quản lý môi trường bằng Poetry"
authors = ["Sudo <dev@example.com>"]
[tool.poetry.dependencies]
python = "^3.10"
requests = "2.27.1"
numpy = "1.22.0"
# gói phụ của requests: ghim ở bản đầu 2022 để kết quả tái lập được
certifi = "2021.10.8"
urllib3 = "1.26.8"
idna = "3.3"
charset-normalizer = "2.0.10"
[tool.poetry.dev-dependencies]
pyflakes = "2.4.0"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"Biến POETRY_VIRTUALENVS_IN_PROJECT=true yêu cầu Poetry tạo venv ngay trong thư mục dự án (.venv) thay vì thư mục cache chung; nhiều đội thích cách này vì IDE tự nhận ra interpreter.
Script run_poetry.sh (trong thư mục mã nguồn của bài) lần lượt chạy poetry env use, poetry install hai lần, poetry show --tree, poetry export và poetry run.
Kết quả thực tế:
$ poetry --version
Poetry version 1.1.12
$ poetry env use python3.10
Creating virtualenv bao-cao-don-hang in <dir>/.venv
Using virtualenv: <dir>/.venv
$ poetry install
Updating dependencies
Resolving dependencies...
Writing lock file
Package operations: 7 installs, 0 updates, 0 removals
• Installing certifi (2021.10.8)
• Installing charset-normalizer (2.0.10)
• Installing idna (3.3)
• Installing urllib3 (1.26.8)
• Installing numpy (1.22.0)
• Installing pyflakes (2.4.0)
• Installing requests (2.27.1)
$ ls
check_env.py
poetry.lock
pyproject.toml
run_poetry.sh
$ grep -A1 "^name = .requests" poetry.lock
name = "requests"
version = "2.27.1"
$ poetry show --tree
certifi 2021.10.8 Python package for providing Mozilla's CA Bundle.
charset-normalizer 2.0.10 The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet.
idna 3.3 Internationalized Domain Names in Applications (IDNA)
numpy 1.22.0 NumPy is the fundamental package for array computing with Python.
pyflakes 2.4.0 passive checker of Python programs
requests 2.27.1 Python HTTP for Humans.
|-- certifi >=2017.4.17
|-- charset-normalizer >=2.0.0,<2.1.0
|-- idna >=2.5,<4
`-- urllib3 >=1.21.1,<1.27
urllib3 1.26.8 HTTP library with thread-safe connection pooling, file post, and more.
$ poetry install # chạy lần 2, đã có lock
Installing dependencies from lock file
No dependencies to install or update
$ poetry export -f requirements.txt --dev --without-hashes
certifi==2021.10.8
charset-normalizer==2.0.10; python_full_version >= "3.5.0"
idna==3.3; python_version >= "3.5"
numpy==1.22.0; python_version >= "3.8"
pyflakes==2.4.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0")
requests==2.27.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0")
urllib3==1.26.8; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0" and python_version < "4")
$ poetry run python check_env.py
python : 3.10.22
trong venv : True
sys.prefix : .venv
requests : 2.27.1
pytest : (chưa cài)
numpy : 1.22.0Có vài điểm cần giải thích. Poetry tự tạo môi trường Python (venv), giải phụ thuộc, cài bảy gói và ghi poetry.lock trong một lệnh. Lần chạy install thứ hai báo "Installing dependencies from lock file / No dependencies to install or update": đó là hành vi tái lập, vì mọi thứ lấy từ lock chứ không giải lại. Lệnh poetry show --tree cho thấy cây phụ thuộc, chẳng hạn requests yêu cầu charset-normalizer >=2.0.0,<2.1.0. Lệnh poetry export xuất ra requirements.txt để những nơi không cài Poetry (ví dụ stage cuối của Docker) vẫn dựng lại được đúng phiên bản.
Quy tắc dùng file lock
Với ứng dụng (pipeline dữ liệu, API phục vụ mô hình), hãy commit poetry.lock. Tài liệu Poetry khuyên lập trình viên ứng dụng nên commit file này; với thư viện công bố cho người khác dùng, ứng dụng của họ sẽ bỏ qua lock của thư viện, nên cần cân nhắc có commit hay không và cập nhật định kỳ. Muốn nâng cấp có chủ đích, dùng poetry update (cập nhật trong giới hạn của pyproject.toml) và xem diff của lock trong pull request như xem diff code. Nếu chỉnh pyproject.toml tay mà quên cập nhật lock, Poetry cảnh báo hai file lệch nhau; hãy coi cảnh báo đó là lỗi chặn merge trong CI.
So sánh các công cụ quản lý môi trường Python
Hệ sinh thái Python có nhiều công cụ chồng lấp. Bảng dưới đây tóm tắt phạm vi của từng loại, theo hiện trạng đầu năm 2022:
| Công cụ | Làm gì | Khoá phiên bản | Khi nào chọn |
|---|---|---|---|
venv + pip | Tạo môi trường Python và cài gói, có sẵn khắp nơi | Thủ công (pip freeze, constraints) | Script, dự án nhỏ, Dockerfile, máy chủ không cài thêm gì |
virtualenv | Tiền thân của venv, nhanh hơn và hỗ trợ nhiều interpreter | Như pip | Cần tạo env cho Python cũ hoặc cần tính năng nâng cao |
pip-tools | Biên dịch requirements.in thành file đã ghim | Tự động, có thể kèm hash | Muốn quy trình nhẹ nhưng vẫn lock đầy đủ |
| Poetry | Phụ thuộc, lock, venv, đóng gói và publish | poetry.lock | Ứng dụng hoặc thư viện nhiều người cùng phát triển |
| Pipenv | Pipfile và Pipfile.lock, tạo venv | Pipfile.lock | Đội đã quen công cụ này |
| conda | Quản lý cả gói hệ thống (CUDA, BLAS) lẫn gói Python | environment.yml (+ lock ngoài) | Khoa học dữ liệu cần thư viện biên dịch sẵn phức tạp |
| pyenv | Cài và chuyển đổi nhiều phiên bản interpreter | Không quản lý gói | Cần chạy Python 3.8, 3.9, 3.10 song song |
Nguyên tắc chọn môi trường Python của chúng tôi: dùng venv cùng pip cho mọi thứ nhỏ; chuyển sang Poetry hoặc pip-tools khi có từ hai người hoặc có triển khai production; dùng conda khi dự án phụ thuộc vào thư viện nhị phân nặng mà wheel của PyPI chưa phục vụ tốt trên nền tảng của bạn. Đừng trộn: mỗi môi trường Python chỉ nên có một "nguồn sự thật" cho phiên bản gói.
Quản lý nhiều phiên bản Python bằng pyenv
venv và Poetry đều không cài interpreter: chúng dùng interpreter có sẵn trên máy. Tài liệu Poetry nói rõ phiên bản Python khớp ràng buộc python = "^3.10" phải được cài trước. Khi máy dev dùng 3.10 mà server vẫn chạy 3.8, bạn cần công cụ chọn interpreter. Với pyenv, theo tài liệu của dự án pyenv, mọi lệnh python và pip đi qua các "shim" nằm đầu PATH; shim hỏi pyenv xem cần dùng phiên bản nào, theo thứ tự ưu tiên: biến PYENV_VERSION (đặt bởi pyenv shell), file .python-version trong thư mục hiện tại (do pyenv local ghi), file cùng tên ở thư mục cha, cuối cùng là phiên bản toàn cục.
pyenv install 3.10.1 # build từ mã nguồn, cần sẵn công cụ build
pyenv local 3.10.1 # ghi .python-version, commit file này vào Git
python -m venv .venv # venv dùng đúng interpreter 3.10.1
poetry env use python3.10 # hoặc chỉ cho Poetry dùng interpreter đó
Lưu ý: tôi không chạy pyenv trong bài này (máy dùng một bản Python 3.10 tải riêng để lấy output ở trên), nên đoạn trên là mô tả theo tài liệu, và số bản vá 3.10.1 (tháng 12/2021) chỉ là ví dụ. Vì pyenv build interpreter từ mã nguồn, trên macOS bạn cần cài Xcode Command Line Tools trước, và nên cài thêm các thư viện như openssl, readline, zlib bằng Homebrew theo hướng dẫn "Suggested build environment" của pyenv, nếu không module ssl hay sqlite3 có thể thiếu. Điều quan trọng cần nhớ là file .python-version nên được commit để cả đội dùng chung một phiên bản interpreter cho môi trường Python, và trường python trong pyproject.toml là ràng buộc ở mức công bố, còn .python-version là lựa chọn ở mức máy dev.
Quy trình đề xuất cho dự án dữ liệu và AI
Gộp các phần trên thành quy trình quản lý môi trường Python cho đội nhỏ:
- Chốt phiên bản Python của dự án (ví dụ dòng 3.10) và ghi vào
.python-versioncùngpyproject.toml. - Tạo môi trường Python riêng:
poetry installhoặcpython -m venv .venv. Không cài gói vào interpreter hệ thống. - Khai báo phụ thuộc trực tiếp; để công cụ sinh file lock. Commit
poetry.lockhoặcrequirements.txt. - Tách phụ thuộc phát triển (
pytest,pyflakes) khỏi phụ thuộc chạy thật bằng nhóm dev của Poetry hoặc hai file requirements. - Trong CI và Docker, dựng môi trường từ file lock, không dùng
pip installkhông ghim phiên bản. - Nâng cấp theo lịch (ví dụ mỗi tháng): chạy update, chạy test, đọc diff của lock, rồi merge.
Với dự án AI, môi trường Python ổn định còn quan trọng hơn: kết quả huấn luyện phụ thuộc vào phiên bản numpy, scikit-learn hay torch, và mô hình đã lưu bằng pickle hay joblib thường chỉ nạp lại được đúng trên phiên bản gói đã lưu. File lock chính là một phần của "tính tái lập" của thí nghiệm.
Lỗi thường gặp khi dựng môi trường Python
- Quên activate nhưng vẫn cài vào hệ thống: gõ
pip installmà không qua venv. Thói quen an toàn làpython -m pipvới interpreter của venv, hoặc đường dẫn đầy đủ như trong demo. - Commit
.venvvào Git: thư mục nặng, không di chuyển được và chứa đường dẫn máy bạn. Thêm vào.gitignore. - Chỉ ghim gói trực tiếp: gói gián tiếp vẫn trôi. Dùng
pip freeze, constraints file hoặc lock file. - Lock lệch với
pyproject.toml: cài ra khác với kỳ vọng. Cho CI báo lỗi khi hai file lệch nhau. - Python đổi nhưng môi trường Python cũ giữ nguyên: sau khi nâng interpreter hệ thống, venv cũ có thể hỏng; cách sạch nhất là xoá và dựng lại (hoặc dùng
--upgradecủavenvtheo tài liệu nếu chỉ nâng bản vá tại chỗ). - Chạy bằng cron hoặc systemd mà không chỉ rõ interpreter: dịch vụ dùng Python hệ thống thay vì venv. Luôn ghi đường dẫn tuyệt đối tới
pythontrong venv.
Checklist nhanh
- Mỗi dự án một môi trường Python (venv); không cài gói vào Python hệ thống.
- Có file ghim phiên bản đầy đủ (kể cả phụ thuộc gián tiếp) được commit.
- Dựng lại được môi trường Python từ đầu bằng một lệnh, đã thử trên máy sạch.
- Phiên bản interpreter được ghi trong repo.
.venv/nằm trong.gitignore.
Toàn bộ mã trong bài nằm ở thư mục code/s7-01/ (demo/run_venv.sh, demo/run_move.sh, poetry-demo/run_poetry.sh); chạy trên Python 3.10 và Poetry 1.1.12 (tháng 11/2021). Khi đã có môi trường Python ổn định, bước tiếp theo trong series là dùng nó để lấy dữ liệu từ web một cách có trách nhiệm.
Bài tiếp theo
Đọc tiếp: Crawl data bằng Python: requests, BeautifulSoup và robots.txt.



