Python 4 — Hàm, Module & Môi trường ảo
Python 4 — Hàm, Module & Môi trường ảo
Ở các bài trước bạn đã viết những đoạn code chạy tuần tự từ trên xuống. Khi chương trình lớn lên, cách viết đó nhanh chóng trở nên rối rắm: cùng một logic bị lặp đi lặp lại, sửa một chỗ phải sửa hàng chục chỗ, và không ai đọc nổi file 800 dòng. Bài này dạy bạn ba công cụ để tổ chức code: hàm (đóng gói một đoạn logic có thể tái sử dụng), module/package (đóng gói nhiều hàm vào file và thư mục), và môi trường ảo (cô lập thư viện của từng dự án). Đi từ zero đến mức đủ tự tin xây một dự án nhỏ thật sự.
Định nghĩa hàm: viên gạch tái sử dụng
Một hàm là một khối code có tên, nhận đầu vào và (tuỳ chọn) trả về đầu ra. Dùng từ khoá def.
def chao(ten):
return f"Xin chào, {ten}!"
loi_chao = chao("Lan") # gọi hàm
print(loi_chao) # Xin chào, Lan!
Cú pháp gồm: def + tên hàm + danh sách tham số trong ngoặc + dấu hai chấm, rồi thân hàm thụt lề. Câu lệnh return kết thúc hàm và gửi giá trị về nơi gọi.
Vì sao nên dùng hàm? Ba lý do cốt lõi:
- DRY (Don't Repeat Yourself): viết một lần, gọi nhiều nơi.
- Trừu tượng hoá: người gọi chỉ cần biết hàm làm gì, không cần biết làm thế nào.
- Kiểm thử: một hàm nhỏ với đầu vào/đầu ra rõ ràng rất dễ test.
Docstring — tài liệu sống
Ngay dòng đầu thân hàm, một chuỗi nằm trong """...""" được Python lưu thành docstring và truy cập qua help() hoặc thuộc tính __doc__.
def dien_tich_hinh_chu_nhat(rong, cao):
"""Tính diện tích hình chữ nhật.
Tham số:
rong (float): chiều rộng.
cao (float): chiều cao.
Trả về:
float: diện tích = rong * cao.
"""
return rong * cao
print(dien_tich_hinh_chu_nhat.__doc__)
Hãy tập thói quen viết docstring cho mọi hàm công khai — nó là tài liệu không bao giờ lỗi thời nếu bạn cập nhật cùng code.
Tham số: vị trí, từ khoá và mặc định
Python cho phép truyền đối số theo vị trí (positional) hoặc theo từ khoá (keyword).
def tao_tai_khoan(ten, tuoi, vai_tro):
return f"{ten} ({tuoi}) - {vai_tro}"
# Theo vị trí: thứ tự quan trọng
tao_tai_khoan("Hùng", 30, "admin")
# Theo từ khoá: thứ tự không quan trọng, rõ nghĩa hơn
tao_tai_khoan(ten="Hùng", vai_tro="admin", tuoi=30)
Giá trị mặc định
Gán giá trị mặc định để tham số trở thành tuỳ chọn:
def ket_noi(host, port=5432, timeout=30):
return f"{host}:{port} (timeout={timeout}s)"
ket_noi("localhost") # localhost:5432 (timeout=30s)
ket_noi("db.local", port=3306) # db.local:3306 (timeout=30s)
Cảnh báo kinh điển — đừng dùng đối tượng khả biến (mutable) làm mặc định:
# SAI: list mặc định được chia sẻ giữa các lần gọi!
def them(item, gio=[]):
gio.append(item)
return gio
them("táo") # ['táo']
them("cam") # ['táo', 'cam'] <-- bất ngờ!
# ĐÚNG: dùng None làm cờ
def them(item, gio=None):
if gio is None:
gio = []
gio.append(item)
return gio
Giá trị mặc định được tính một lần lúc định nghĩa hàm, nên một list dùng chung sẽ tích luỹ dữ liệu qua các lần gọi.
Trả về nhiều giá trị
return a, b, c thực ra trả về một tuple, và ta có thể "gỡ" ra ngay:
def chia(a, b):
thuong = a // b
du = a % b
return thuong, du
q, r = chia(17, 5) # q=3, r=2
Nếu không có return, hàm trả về None.
*args và **kwargs: số lượng đối số linh hoạt
*args gom các đối số vị trí dư thừa thành một tuple; **kwargs gom các đối số từ khoá dư thừa thành một dict.
def tong(*args):
return sum(args)
tong(1, 2, 3) # 6
tong(1, 2, 3, 4, 5) # 15
def cau_hinh(**kwargs):
for khoa, gia_tri in kwargs.items():
print(f"{khoa} = {gia_tri}")
cau_hinh(debug=True, level=3)
Kết hợp đầy đủ và đúng thứ tự — tham số thường, rồi *args, rồi tham số chỉ-từ-khoá, rồi **kwargs:
def ham(a, b, *args, c=10, **kwargs):
print(a, b, args, c, kwargs)
ham(1, 2, 3, 4, c=99, x="y")
# 1 2 (3, 4) 99 {'x': 'y'}
Toán tử * và ** còn dùng để trải (unpack) khi gọi hàm:
so = [3, 5]
print(chia(*so)) # tương đương chia(3, 5)
tham_so = {"host": "db", "port": 3306}
print(ket_noi(**tham_so)) # trải dict thành keyword args
Phạm vi biến: quy tắc LEGB & closure
Khi bạn dùng một tên biến, Python tìm nó theo thứ tự LEGB:
- Local — bên trong hàm hiện tại.
- Enclosing — hàm bao ngoài (với hàm lồng nhau).
- Global — cấp module.
- Built-in —
print,len,range...
x = "global"
def ngoai():
x = "enclosing"
def trong():
print(x) # tìm thấy ở Enclosing
trong()
ngoai() # enclosing
Mặc định, gán giá trị bên trong hàm tạo ra biến local mới. Muốn ghi đè biến cấp ngoài, dùng global hoặc nonlocal:
dem = 0
def tang():
global dem
dem += 1
def bo_dem():
n = 0
def tang():
nonlocal n # sửa biến của hàm bao ngoài
n += 1
return n
return tang
Closure
Một closure là hàm "nhớ" được các biến của phạm vi bao quanh nó, ngay cả khi hàm bao đã trả về. Ví dụ bo_dem() ở trên: mỗi lần gọi tạo một bộ đếm độc lập.
dem_a = bo_dem()
print(dem_a(), dem_a(), dem_a()) # 1 2 3
dem_b = bo_dem()
print(dem_b()) # 1 — độc lập với dem_a
Closure là nền tảng để hiểu decorator ở phần sau.
Lambda và hàm bậc cao
lambda tạo một hàm vô danh, ngắn gọn, dùng một dòng:
binh_phuong = lambda x: x * x
binh_phuong(5) # 25
Trong Python, hàm là đối tượng hạng nhất: có thể gán vào biến, truyền vào hàm khác, trả về từ hàm. Hàm nhận/trả về hàm khác gọi là hàm bậc cao (higher-order function).
so = [5, 2, 8, 1, 9]
# map: áp dụng hàm lên từng phần tử
binh_phuong = list(map(lambda x: x * x, so)) # [25, 4, 64, 1, 81]
# filter: giữ phần tử thoả điều kiện
chan = list(filter(lambda x: x % 2 == 0, so)) # [2, 8]
# sorted với key: sắp xếp theo tiêu chí tuỳ chỉnh
nguoi = [("Lan", 30), ("Hùng", 25), ("An", 35)]
theo_tuoi = sorted(nguoi, key=lambda p: p[1]) # sắp theo tuổi
Lời khuyên: với logic phức tạp, list comprehension thường dễ đọc hơn map/filter. Hãy giữ lambda cho những biểu thức ngắn, đặc biệt là tham số key.
binh_phuong = [x * x for x in so] # dễ đọc hơn map+lambda
Decorator: bao bọc hành vi của hàm
Một decorator là hàm nhận một hàm và trả về một hàm mới, thường để thêm hành vi (logging, đo thời gian, kiểm tra quyền...) mà không sửa code gốc. Đây chính là closure áp dụng thực tế.
import time
import functools
def do_thoi_gian(func):
@functools.wraps(func) # giữ tên & docstring gốc
def wrapper(*args, **kwargs):
bat_dau = time.perf_counter()
ket_qua = func(*args, **kwargs)
thoi_gian = time.perf_counter() - bat_dau
print(f"{func.__name__} chạy mất {thoi_gian:.4f}s")
return ket_qua
return wrapper
@do_thoi_gian
def tinh_tong(n):
return sum(range(n))
tinh_tong(1_000_000)
# tinh_tong chạy mất 0.0123s
Cú pháp @do_thoi_gian đặt trên def tinh_tong tương đương tinh_tong = do_thoi_gian(tinh_tong). Dùng functools.wraps để hàm sau khi bọc vẫn giữ __name__ và __doc__ đúng — nếu không, mọi hàm sẽ thành "wrapper".
Generator & yield: dữ liệu theo dòng chảy
Một generator là hàm dùng yield thay vì return. Nó sản sinh giá trị lười (lazy), từng cái một, thay vì tạo cả danh sách trong bộ nhớ. Lý tưởng cho dữ liệu lớn hoặc vô hạn.
def dem_len(toi_da):
n = 1
while n <= toi_da:
yield n # tạm dừng, trả về n, ghi nhớ trạng thái
n += 1
for so in dem_len(3):
print(so) # 1, 2, 3
Khác biệt cốt lõi với hàm thường: mỗi khi gặp yield, hàm tạm dừng và trả về giá trị; lần lặp kế tiếp nó tiếp tục ngay sau yield với toàn bộ trạng thái cũ.
So sánh bộ nhớ với một file khổng lồ:
# Tốn bộ nhớ: nạp toàn bộ vào list
def doc_het(ten_file):
return open(ten_file).readlines() # cả file vào RAM
# Tiết kiệm: đọc từng dòng theo nhu cầu
def doc_tung_dong(ten_file):
with open(ten_file) as f:
for dong in f:
yield dong.strip()
Còn có generator expression — như list comprehension nhưng dùng ngoặc tròn và lười:
tong_binh_phuong = sum(x * x for x in range(1_000_000)) # không tạo list trung gian
Module: gom hàm vào file
Một module đơn giản là một file .py. Mọi hàm, biến, lớp trong đó có thể được dùng lại ở file khác qua import.
Giả sử có file toan_hoc.py:
# toan_hoc.py
PI = 3.14159
def dien_tich_tron(r):
return PI * r * r
def chu_vi_tron(r):
return 2 * PI * r
Dùng ở file khác:
# main.py
import toan_hoc # import cả module
print(toan_hoc.dien_tich_tron(5))
from toan_hoc import dien_tich_tron # import tên cụ thể
print(dien_tich_tron(5))
from toan_hoc import dien_tich_tron as dt # đặt bí danh
import toan_hoc as th # bí danh cho module
Tránh from module import * trong code thật vì nó làm ô nhiễm không gian tên và khó truy vết tên đến từ đâu.
Khối if __name__ == "__main__"
Khi một file được chạy trực tiếp, Python gán __name__ = "__main__". Khi nó được import, __name__ bằng tên module. Mẫu này cho phép một file vừa là thư viện vừa là script:
# toan_hoc.py
def dien_tich_tron(r):
return PI * r * r
if __name__ == "__main__":
# chỉ chạy khi gọi `python toan_hoc.py`, không chạy khi bị import
print("Kiểm tra nhanh:", dien_tich_tron(2))
Package: gom module vào thư mục
Một package là một thư mục chứa nhiều module. Theo truyền thống, nó có file __init__.py (có thể rỗng) để Python nhận diện đây là package và để gom các export.
myapp/
├── __init__.py
├── toan_hoc.py
└── chuoi/
├── __init__.py
└── dinh_dang.py
Import theo cấu trúc thư mục bằng dấu chấm:
from myapp.toan_hoc import dien_tich_tron
from myapp.chuoi.dinh_dang import viet_hoa
File __init__.py chạy khi package được import lần đầu. Bạn có thể dùng nó để tạo API gọn cho người dùng:
# myapp/__init__.py
from .toan_hoc import dien_tich_tron, chu_vi_tron
# nay có thể: from myapp import dien_tich_tron
Dấu chấm phía trước (.toan_hoc) là import tương đối — tham chiếu module cùng package.
Quản lý môi trường ảo: venv
Mỗi dự án cần bộ thư viện riêng với phiên bản riêng. Cài mọi thứ vào Python hệ thống sẽ dẫn tới xung đột phiên bản giữa các dự án. Môi trường ảo (virtual environment) là một bản Python độc lập, cô lập cho từng dự án.
Tạo và kích hoạt bằng module venv có sẵn:
# Tạo môi trường ảo trong thư mục .venv
python -m venv .venv
# Kích hoạt — Linux/macOS
source .venv/bin/activate
# Kích hoạt — Windows (PowerShell)
.venv\Scripts\Activate.ps1
# Sau khi kích hoạt, dấu nhắc có tiền tố (.venv)
# Thoát môi trường:
deactivate
Khi đã kích hoạt, python và pip trỏ vào môi trường ảo, không đụng tới Python hệ thống. Hãy thêm .venv/ vào .gitignore — không commit môi trường ảo lên git.
pip và requirements.txt
pip là trình quản lý gói của Python, lấy thư viện từ PyPI (Python Package Index).
# Cài một gói
pip install requests
# Cài phiên bản cụ thể
pip install "requests==2.31.0"
# Nâng cấp
pip install --upgrade requests
# Gỡ
pip uninstall requests
# Liệt kê gói đã cài
pip list
Để dự án tái lập được trên máy khác, ghi danh sách phụ thuộc vào requirements.txt:
# Ghi mọi gói (kèm phiên bản chính xác) ra file
pip freeze > requirements.txt
# Trên máy mới: cài lại đúng bộ phụ thuộc
pip install -r requirements.txt
Một requirements.txt điển hình:
requests==2.31.0
pandas>=2.0,<3.0
python-dotenv
Ghim phiên bản (==) cho bản phát hành để đảm bảo tái lập; dùng khoảng (>=,<) khi muốn linh hoạt cập nhật bản vá.
Cấu trúc một dự án nhỏ
Gộp tất cả lại, một dự án Python gọn gàng thường trông như sau:
myapp/
├── .venv/ # môi trường ảo (KHÔNG commit)
├── .gitignore
├── README.md
├── requirements.txt
├── src/
│ └── myapp/
│ ├── __init__.py
│ ├── main.py # điểm vào, có if __name__ == "__main__"
│ ├── toan_hoc.py
│ └── chuoi/
│ ├── __init__.py
│ └── dinh_dang.py
└── tests/
└── test_toan_hoc.py
Quy trình khởi tạo điển hình:
mkdir myapp && cd myapp
python -m venv .venv
source .venv/bin/activate
pip install requests
pip freeze > requirements.txt
echo ".venv/" >> .gitignore
Nguyên tắc chung: code nguồn nằm trong một package có tên rõ ràng, test tách riêng, phụ thuộc khai báo trong requirements.txt, và mọi thứ chạy trong môi trường ảo. Đây là khung sườn bạn sẽ mở rộng dần khi dự án lớn lên.
Tóm tắt
- Hàm đóng gói logic tái sử dụng:
def, tham số vị trí/từ khoá/mặc định,return, docstring. Tránh dùng list/dict làm giá trị mặc định. *args/**kwargscho số lượng đối số linh hoạt; toán tử*/**còn dùng để trải đối số khi gọi hàm.- Phạm vi biến theo quy tắc LEGB;
global/nonlocalđể ghi đè cấp ngoài; closure ghi nhớ biến của hàm bao quanh. lambdavà hàm bậc cao (map,filter,sortedvớikey) giúp viết phong cách hàm; decorator bao bọc hành vi; generator vớiyieldxử lý dữ liệu lười, tiết kiệm bộ nhớ.- Module là file
.py, package là thư mục có__init__.py; dùngimportđể tái sử dụng. - venv cô lập thư viện; pip cài gói; requirements.txt giúp dự án tái lập trên máy khác.
Tự kiểm tra
- Vì sao không nên dùng
def f(x, ds=[])vớidslà list mặc định? Cách sửa đúng là gì? - Phân biệt
*argsvà**kwargs. Khi gọi hàm,*và**làm gì với một list và một dict? - Giải thích quy tắc LEGB. Khi nào bạn cần
nonlocalthay vìglobal? - Khác biệt cốt lõi giữa một hàm dùng
returnvà một generator dùngyieldlà gì? Khi nào generator có lợi? - Decorator là gì, và
functools.wrapsgiải quyết vấn đề nào? - Liệt kê các bước tạo môi trường ảo, cài
requests, và ghi lại phụ thuộc để máy khác cài lại được.
Đọc tiếp
Bài viết liên quan
Vì sao Python là ngôn ngữ số một của data engineer: vai trò trong pipeline (ingest/transform/orchestrate), hệ sinh thái thư viện (pandas/polars/pyarrow/sqlalchemy), quản lý môi trường (venv/uv/poetry), và khi nào dùng Python vs SQL/Spark.
Lớp, kế thừa, đa hình, dunder methods, dataclass, type hints và nguyên tắc viết code sạch.
Exception handling, context manager (with), đọc/ghi file, JSON/CSV và logging đúng cách.
Kết nối CSDL với SQLAlchemy, gọi REST API với requests/httpx, và lập trình bất đồng bộ asyncio.