Python 4 — Hàm, Module & Môi trường ảo

13 thg 7, 2026 4 lượt xem
#python
#module
#ham
#venv
#pip

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ử *** 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:

  1. Local — bên trong hàm hiện tại.
  2. Enclosing — hàm bao ngoài (với hàm lồng nhau).
  3. Global — cấp module.
  4. 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____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, pythonpip 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/**kwargs cho 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.
  • lambda và hàm bậc cao (map, filter, sorted với key) giúp viết phong cách hàm; decorator bao bọc hành vi; generator với yield xử 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ùng import để 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

  1. Vì sao không nên dùng def f(x, ds=[]) với ds là list mặc định? Cách sửa đúng là gì?
  2. Phân biệt *args**kwargs. Khi gọi hàm, *** làm gì với một list và một dict?
  3. Giải thích quy tắc LEGB. Khi nào bạn cần nonlocal thay vì global?
  4. Khác biệt cốt lõi giữa một hàm dùng return và một generator dùng yield là gì? Khi nào generator có lợi?
  5. Decorator là gì, và functools.wraps giải quyết vấn đề nào?
  6. 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

Python 5 — OOP & Clean Code

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.

13 thg 7, 2026 4

Lớp, kế thừa, đa hình, dunder methods, dataclass, type hints và nguyên tắc viết code sạch.

13 thg 7, 2026 3

Exception handling, context manager (with), đọc/ghi file, JSON/CSV và logging đúng cách.

13 thg 7, 2026 3

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.

13 thg 7, 2026 3