Mở đầu: Từ mảng động JavaScript đến Scripting trong AI

Đối với đa số lập trình viên phát triển web, JavaScript (JS) là ngôn ngữ thống trị tuyệt đối nhờ mô hình vạn vật bất đồng bộ dựa trên Event Loop và khả năng chạy trực tiếp trên trình duyệt. Tuy nhiên, khi bước chân vào lãnh địa của Trí Tuệ Nhân Tạo (AI) và Học Máy (Machine Learning), Python mới là tiêu chuẩn công nghiệp không thể chối cãi. Điều này dễ dẫn đến tâm lý e ngại vì phải làm quen với một cú pháp mới và cách quản lý công cụ hoàn toàn xa lạ.

Đừng lo lắng! Bài học này sẽ giúp bạn bắc một cây cầu tư duy vững chắc từ các khái niệm JS quen thuộc sang cú pháp tương đương của Python. Bạn sẽ thấy rằng việc học Python thực ra rất dễ dàng khi chúng ta đối chiếu trực tiếp các cấu trúc lệnh, cơ chế phân phối gói thư viện, và đặc biệt là cách xử lý vùng nhớ giữa hai ngôn ngữ. Nắm chắc bài học này sẽ giúp bạn tự tin viết các script đầu tiên và cấu hình môi trường sạch sẽ để huấn luyện AI.


✅ Trước khi bắt đầu: bạn cần gì trên máy
Cả series này chỉ cần Python 3.9 trở lên. Mở terminal và kiểm tra:

python3 --version

Nếu máy in ra một dòng như Python 3.11.6 thì bạn đã đủ điều kiện, bỏ qua phần còn lại của khung này. Nếu nó báo command not found thì bạn chưa có Python:

macOS: brew install python3 · Windows: tải bộ cài từ python.org và nhớ tick "Add Python to PATH" ở bước đầu (bỏ qua ô này là nguyên nhân phổ biến nhất khiến terminal không thấy Python sau khi cài) · Linux: sudo apt install python3 python3-venv.

Một lưu ý nhỏ nhưng hay gây bối rối: trên macOS và Linux, lệnh là python3 chứ không phải python. Trên Windows thường là python. Bài này viết python3; nếu bạn dùng Windows thì bỏ số 3 đi.

1. Môi trường runtime: Python Interpreter vs Node.js

Điểm tương đồng lớn nhất giữa Python và JavaScript là cả hai đều là ngôn ngữ thông dịch (interpreted) với kiểu dữ liệu động (dynamic typing). Nghĩa là bạn viết code rồi chạy trực tiếp, không có bước biên dịch riêng như C++ hay Java, và một biến có thể giữ số ở dòng này rồi giữ chuỗi ở dòng sau. Tư duy chạy code của bạn từ Node.js chuyển sang Python gần như không phải đổi gì.

Khác biệt đáng để bạn quan tâm ngay chỉ có một: Python chạy chậm hơn JavaScript đáng kể ở các phép tính thuần trong vòng lặp. Đổi lại, nó gắn cực kỳ tốt với các thư viện viết bằng C/C++ — và như mục dưới sẽ thấy, đó chính là lý do toàn bộ ngành AI chọn Python dù nó chậm.

Phần nội tại bên dưới không cần thiết để làm được bài này. Nó ở đây cho bạn nào tò mò, và có thể bỏ qua hoàn toàn mà không ảnh hưởng gì tới các bài sau.

🔬 Bên trong: V8 và CPython dịch code theo hai cách khác nhau
Node.js dùng V8 của Google. V8 dịch JavaScript thành bytecode (bộ phận tên Ignition), rồi với những đoạn code chạy lặp lại nhiều lần thì nó dịch tiếp thành mã máy tối ưu (bộ phận TurboFan). Kỹ thuật vừa chạy vừa dịch này gọi là JIT (Just-In-Time Compilation), và nó là lý do JavaScript nhanh một cách đáng ngạc nhiên với một ngôn ngữ động.

Trình thông dịch Python mặc định tên là CPython (viết bằng C, nên có chữ C). Nó cũng dịch code thành bytecode — bạn sẽ thấy các tệp .pyc xuất hiện trong thư mục __pycache__ sau lần chạy đầu — nhưng không có bước dịch tiếp thành mã máy tối ưu. Bytecode được thực thi tuần tự trên một máy ảo. Đó là gốc rễ của việc Python chậm hơn ở vòng lặp thuần.
🧠 Global Interpreter Lock (GIL) — Giới hạn đa luồng của Python
Một khái niệm cực kỳ quan trọng trong CPython là GIL (Global Interpreter Lock). Đây là một cơ chế khóa phân luồng để đảm bảo chỉ có duy nhất một luồng (thread) được kiểm soát bộ xử lý Python tại một thời điểm. Điều này đồng nghĩa với việc Python không thể thực hiện đa luồng thực thụ (multithreading) trên các tác vụ ngốn CPU (CPU-bound tasks).

Tuy nhiên, tại sao Python vẫn thống trị mảng AI/Deep Learning vốn cực kỳ ngốn CPU/GPU? Đó là nhờ các thư viện tính toán tensor nền tảng như NumPy và PyTorch không thực sự được viết bằng Python — phần lõi của chúng viết bằng C++CUDA (ngôn ngữ của NVIDIA để chạy tính toán trên card đồ hoạ). Khi PyTorch nhân hai ma trận, nó đẩy toàn bộ dữ liệu xuống tầng mã máy C++ chạy đa luồng bên ngoài sự quản lý của CPython. Nên phép tính đó lách hoàn toàn qua khoá GIL.

Nói cách khác: Python trong AI không làm việc tính toán. Nó chỉ ra lệnh. Việc nặng do C++/CUDA làm, và GIL không với tới đó được. Đây là lý do một ngôn ngữ "chậm" lại là ngôn ngữ của ngành AI.
📦 Tensor là gì? — thuật ngữ quan trọng nhất của cả series
Từ tensor vừa xuất hiện ở trên, và nó sẽ theo bạn suốt 19 bài còn lại (Bài 4 có tên là "Tensor & Autograd"), nên hãy nắm nó ngay từ đây.

Tensor chỉ là một mảng số nhiều chiều. Hết. Không có gì huyền bí. Bạn đã dùng nó trong JavaScript rồi, chỉ là chưa gọi tên:
  • 5 — một số lẻ. Tensor 0 chiều (còn gọi là scalar).
  • [1, 2, 3] — một mảng phẳng. Tensor 1 chiều (vector).
  • [[1, 2], [3, 4]] — mảng của mảng. Tensor 2 chiều (matrix, ma trận).
  • Mảng 3 chiều, 4 chiều… vẫn là tensor. Số chiều gọi là rank.
Một cách hình dung đời thường: một bức ảnh màu là một tensor 3 chiều — chiều cao × chiều rộng × 3 kênh màu (đỏ, xanh lục, xanh dương). Mỗi ô chứa một số từ 0 đến 255. Khi bạn nghe "đưa ảnh vào mô hình", nghĩa thật là đưa cái khối số đó vào.

Hai ví dụ để thấy ranh giới. Bảng điểm 100 học sinh × 5 môn một tensor 2 chiều: toàn số, kích thước đều nhau, nên nhân/cộng ma trận được. Còn một danh sách 100 hồ sơ kiểu { name: "An", age: 20, note: "giỏi" } không phải tensor: nó chứa chữ, và các phần tử không cùng dạng. Muốn dùng được trong AI, ta phải chuyển nó thành số trước — và đó chính là việc mà các bài về embeddings (Bài 8) làm.

Vì sao AI chỉ nói chuyện bằng tensor? Vì card đồ hoạ được thiết kế để làm một việc cực nhanh: cộng và nhân những khối số đều đặn, hàng nghìn ô cùng lúc. Mọi thứ trong deep learning — ảnh, câu văn, âm thanh — đều bị quy về tensor để dùng được sức mạnh đó.
💡 Đối chiếu Hệ sinh thái Công cụ (Package & Env Management)
Khi quản lý mã nguồn và thư viện, bạn có thể đối chiếu trực tiếp các công cụ tương đương giữa thế giới JS và Python như sau:
  • npm vs pip: npm install tương đương với pip install. Kho lưu trữ PyPI (Python Package Index) đóng vai trò tương tự npmjs.org.
  • package.json vs requirements.txt: Thay vì liệt kê dependencies trong file JSON, Python sử dụng file text đơn giản requirements.txt chứa danh sách các thư viện kèm phiên bản (ví dụ: numpy==1.24.3).
  • node_modules vs venv (Virtual Environment): Đây là điểm khác biệt lớn. Node.js tự động tạo thư mục node_modules cục bộ trong thư mục dự án để cô lập dependencies. Python thì mặc định cài đặt thư viện vào thư mục hệ thống toàn cục (global package). Để tránh xung đột phiên bản giữa các dự án khác nhau (dependency hell), bạn bắt buộc phải tạo môi trường ảo độc lập bằng công cụ venv.

Cơ chế của venv chỉ đơn giản là tạo ra một bản sao gọn nhẹ của trình thông dịch Python và sửa đổi các biến môi trường PATHVIRTUAL_ENV của shell để trình thông dịch ưu tiên tìm kiếm thư viện trong thư mục venv cục bộ của dự án trước (thay vì tìm trong sys.path toàn cục).

Để khởi tạo một môi trường ảo cục bộ trong thư mục dự án của bạn (tương đương với việc có một thư mục cô lập dependencies riêng), hãy chạy các lệnh sau trong terminal:

Terminal
# The word "venv" appears TWICE here, in two different roles:
#   -m venv  = run Python's standard module named `venv`
#   venv     = name of the DIRECTORY to create (.venv or env work just as well)
python3 -m venv venv

# Activate on macOS / Linux
source venv/bin/activate

# Activate on Windows (PowerShell)
# venv\Scripts\Activate.ps1

# Sign it worked: your shell prompt gains a (venv) prefix
#   (venv) you@machine myproject %
# A stronger check — the path must point inside your project's venv:
which python        # macOS/Linux  -> .../myproject/venv/bin/python
# where python      # Windows

# From now on, pip installs into venv/ instead of the system
pip install numpy pandas

# When you are done
deactivate
⚡ Ba điều về venv hay làm người mới mất buổi tối đầu tiên
Một: kích hoạt không "dính" qua các terminal. Mỗi lần bạn mở một cửa sổ terminal mới, hoặc đóng máy rồi mở lại, bạn phải chạy lại source venv/bin/activate. Đây không phải lỗi — kích hoạt chỉ là sửa biến môi trường của đúng phiên terminal đó. Triệu chứng khi quên: ModuleNotFoundError: No module named 'numpy' dù bạn nhớ rõ mình đã cài numpy hôm qua.

Hai: đừng commit thư mục venv lên git. Nó là bản sao của trình thông dịch cộng toàn bộ thư viện, nặng hàng trăm MB và chỉ chạy được trên đúng máy của bạn. Thêm venv/ vào .gitignore — vai trò của nó giống hệt node_modules/.

Ba: cách "chia sẻ" thư viện cho người khác. Vì venv không được commit, bạn xuất danh sách thư viện ra file: pip freeze > requirements.txt. Người khác clone dự án về sẽ tạo venv của riêng họ rồi chạy pip install -r requirements.txt. Đây chính là cặp package.json + npm install mà bạn đã quen, chỉ khác tên.
Khái niệm JavaScript (Node.js) Python (CPython)
Quản lý thư viện npm (Node Package Manager) pip (Package Installer for Python)
Cô lập thư viện node_modules (mặc định tạo cục bộ) venv / conda (phải kích hoạt môi trường ảo chủ động)
Tệp khai báo thư viện package.json requirements.txt
Cơ chế thực thi JIT Compilation (V8 Engine) Interpreter / Bytecode VM

2. Kiểu dữ liệu & Cơ chế tham chiếu qua lăng kính JS

Cú pháp tối thiểu để đọc được mọi đoạn code trong series này

Trước khi nói về kiểu dữ liệu, cần dọn ba khác biệt cú pháp sẽ khiến bạn dừng lại ngay ở dòng code Python đầu tiên. Đây không phải mẹo hay — thiếu chúng thì các đoạn code phía sau đọc không nổi.

Một: Python không có dấu ngoặc nhọn. Khối lệnh được xác định bằng thụt lề. Chỗ nào trong JavaScript bạn viết { thì Python viết dấu hai chấm : rồi thụt vào; chỗ nào đóng } thì Python chỉ cần thụt ra. Điều này không phải quy ước cho đẹp mà là cú pháp: thụt lề sai là lỗi chương trình, không phải lỗi thẩm mỹ.

cu_phap_doi_chieu
// ===== JavaScript =====
function greet(name) {
  if (name) {
    console.log(`Hello ${name}`);
  } else {
    console.log("Hello there");
  }
}

const scores = [8, 9, 10];
for (const s of scores) {
  console.log(s);
}
cu_phap_doi_chieu.py
# ===== Python — same logic, not a single curly brace =====
def greet(name):            # JS `{` becomes a colon `:`
    if name:
        print(f"Hello {name}")     # console.log becomes print
    else:
        print("Hello there")
                            # no `}` — dedenting is what ends the block

scores = [8, 9, 10]
for s in scores:
    print(s)

Hai: print() thay cho console.log(), và f-string thay cho template literal. Trong JavaScript bạn nhúng biến bằng dấu backtick và ${name}. Python làm y hệt nhưng đặt chữ f ngay trước dấu nháy và dùng ngoặc nhọn trần: f"Hello {name}". Quên chữ f là lỗi rất hay gặp — lúc đó Python in ra nguyên văn chuỗi Hello {name} thay vì giá trị, và nó không báo lỗi gì cả. Bạn sẽ thấy f-string được dùng trong dự án thực hành ở mục 4.

Ba: bốn kiểu dữ liệu tập hợp, viết ra trông thế nào. Bảng đối chiếu ở dưới nói về bản chất bên trong, nhưng trước hết hãy xem mặt chữ của chúng — đặc biệt là cách truy cập dict, vì đây là chỗ khác JavaScript nhất:

bon_kieu_tap_hop.py
# LIST — like a JS Array. Mutable, ordered.
scores = [8, 9, 10]
scores.append(7)        # JS: scores.push(7)
print(len(scores))      # JS: scores.length  -> 4

# DICT — like a JS Object/Map.
# But you INDEX IT WITH BRACKETS, never with a dot!
student = {"name": "An", "age": 20}
print(student["name"])  # 'An'   ← correct
# print(student.name)   # AttributeError! Python dicts have no dot syntax
student["grade"] = "12A"  # adding a new key

# TUPLE — like a list but FROZEN. Written with parentheses.
point = (10.5, 20.3)
# point[0] = 99         # TypeError: cannot reassign
x, y = point            # unpacking into two variables — very common in Python

# SET — unique items, no order. Like a JS Set.
tags = {"ai", "python", "ai"}
print(tags)             # {'ai', 'python'} — the duplicate is dropped
🕳️ Cạm bẫy: {} rỗng là dict, không phải set
Dấu ngoặc nhọn dùng cho cả dictset, nên trường hợp rỗng bị nhập nhằng — và Python giải quyết bằng cách ưu tiên dict. x = {} tạo ra một dict rỗng, chứ không phải set rỗng. Muốn set rỗng phải viết x = set().

Kiểm tra nhanh bằng type(x) — hàm này trả về kiểu của biến và sẽ là công cụ chẩn đoán bạn dùng nhiều nhất trong tuần đầu học Python.

Mutable và Immutable — nền tảng của mọi lỗi dữ liệu ngầm

Cú pháp dữ liệu của Python rất giống JavaScript, tuy nhiên Python phân biệt rạch ròi giữa các kiểu dữ liệu có thể thay đổi (Mutable) và không thể thay đổi (Immutable) ngay từ lõi thiết kế để tối ưu hóa hiệu năng bộ nhớ. Sự phân biệt này không phải chuyện học thuật: nó là nguyên nhân trực tiếp của hai cạm bẫy ở cuối mục này, và cả hai đều thuộc loại "không báo lỗi, chỉ cho kết quả sai".

Giờ nhìn sâu hơn vào bốn kiểu vừa xem, lần này ở góc độ bên trong chúng hoạt động thế nào:

  • List (danh sách tuần tự): Tương đương mảng Array trong JS. Bản chất bên dưới của List là một mảng động (Dynamic Array) tự động mở rộng kích thước (over-allocation) khi đầy để tránh phải cấp phát bộ nhớ lại liên tục.
  • Dict (Dictionary): Dạng bảng băm key-value, tương đương Object/Map của JS. Từ Python 3.6 trở đi, Dict được cấu trúc tối ưu hóa bộ nhớ và bảo toàn thứ tự chèn phần tử bằng cách sử dụng một mảng chỉ mục 1D gọn nhẹ song song với mảng lưu trữ giá trị thực.
  • Tuple: Một dạng danh sách cố định chỉ đọc (Immutable List) được bọc bởi dấu ngoặc đơn (val1, val2). Tuple do tính bất biến nên có hiệu năng truy xuất nhanh hơn List và có thể được dùng làm khóa (key) trong Dict (vì giá trị của nó có mã hash cố định), điều mà List không làm được.
🧠 Tiết kiệm bộ nhớ với Generator Expressions
Trong JavaScript, khi ta xử lý mảng lớn bằng các hàm .map() hay .filter(), hệ thống luôn khởi tạo một mảng mới trung gian hoàn toàn trên bộ nhớ RAM.

Ở Python, ngoài List Comprehension [x**2 for x in data] (tạo mảng mới), chúng ta có Generator Expression bằng cách thay ngoặc vuông bằng ngoặc đơn: (x**2 for x in data). Generator sử dụng cơ chế đánh giá lười (Lazy Evaluation), chỉ tính toán giá trị tiếp theo khi được yêu cầu (yield). Điều này giúp xử lý hàng triệu bản ghi dữ liệu mà không tốn thêm một byte RAM nào, cực kỳ tối ưu cho các luồng nạp dữ liệu (data pipelines) của AI.
🕳️ Cạm bẫy: Sao chép tham chiếu vùng nhớ (Reference Passing)
Cả Python và JavaScript đều truyền tham chiếu vùng nhớ đối với các kiểu dữ liệu phức tạp (Mutable). Lỗi phổ biến nhất của lập trình viên là gán biến trực tiếp b = a rồi thay đổi giá trị trong b, dẫn đến việc dữ liệu gốc trong a bị sửa đổi ngầm mà không hề biết.

Hãy xem ví dụ đối chiếu trực quan dưới đây để hiểu cách sao chép dữ liệu an toàn:

copy_check.py
# ❌ WRONG: plain assignment shares the reference, so the original changes too
list_a = [1, 2, 3]
list_b = list_a  # copies only the memory address, not the contents
list_b.append(4)
print(list_a)    # [1, 2, 3, 4] — the original was modified behind your back!

# ✅ RIGHT: make a shallow copy with .copy()
list_c = [1, 2, 3]
list_d = list_c.copy()
list_d.append(4)
print(list_c)    # [1, 2, 3] — the original is safe

Đối với các đối tượng từ điển phức tạp chứa nhiều tầng lồng nhau (nested dictionary), bạn bắt buộc phải sử dụng thư viện chuẩn copy và hàm deepcopy() để nhân bản độc lập toàn bộ các vùng nhớ con:

deep_copy_example.py
import copy

# A nested structure: the dict holds a list inside it
original = {"name": "AI Model", "layers": [128, 64, 32]}

# deepcopy clones every level, not just the outer dict
cloned = copy.deepcopy(original)
cloned["layers"].append(16)

print(original["layers"]) # [128, 64, 32]       — untouched
print(cloned["layers"])   # [128, 64, 32, 16]   — changed independently
🕳️ Cạm bẫy kinh điển: Tham số mặc định Mutable (Mutable Default Argument)
Đây là lỗi khiến gần như MỌI lập trình viên JS đều dính phải ít nhất một lần khi mới học Python, vì mô hình tinh thần hoàn toàn khác nhau giữa hai ngôn ngữ. Trong JavaScript, tham số mặc định function f(arr = []) {"{"}...{"}"} được TẠO MỚI mỗi lần hàm được gọi. Trong Python, biểu thức mặc định (def f(arr=[]):) chỉ được đánh giá ĐÚNG MỘT LẦN, tại thời điểm hàm được định nghĩa — không phải mỗi lần gọi. Vì List là Mutable, mọi lần gọi hàm sau đó đều dùng CHUNG một list đó, tích luỹ dữ liệu từ lần gọi trước.
mutable_default_bug.py
# ❌ WRONG: the default list is "remembered" across separate calls
def add_log(entry, history=[]):
    history.append(entry)
    return history

print(add_log("request-1"))  # ['request-1'] — looks fine
print(add_log("request-2"))  # ['request-1', 'request-2'] — WRONG! expected ['request-2']
# history=[] is created ONCE when def runs; every later call reuses that same list

# ✅ RIGHT: default to None, then build a fresh list inside the body
def add_log_fixed(entry, history=None):
    if history is None:
        history = []  # a brand-new list on every call
    history.append(entry)
    return history

print(add_log_fixed("request-1"))  # ['request-1']
print(add_log_fixed("request-2"))  # ['request-2'] — correct, independent of the last call

Lỗi này đặc biệt nguy hiểm trong code xử lý dữ liệu AI vì nó thường "im lặng" — không ném exception nào cả, chỉ âm thầm tích luỹ dữ liệu rác qua nhiều lần gọi hàm (ví dụ một hàm tiền xử lý batch dữ liệu dùng list mặc định để "cache" kết quả tạm), khiến kết quả huấn luyện sai lệch mà rất khó truy ra nguyên nhân gốc.

3. Lập trình bất đồng bộ: Asyncio vs Event Loop

Trong JavaScript, lập trình bất đồng bộ (Asynchronous Programming) là cơ chế mặc định. Trình duyệt hoặc Node.js chạy một vòng lặp sự kiện (Event Loop) liên tục ở tầng nền để xử lý các sự kiện click, truy vấn mạng hoặc đọc file không chặn (non-blocking) thông qua Call Stack, Microtask Queue (Promises) và Macrotask Queue.

Trái lại, Python về bản chất là ngôn ngữ đồng bộ tuần tự (Synchronous - Blocking). Khi chạy phép toán nặng, luồng bị chặn hoàn toàn. Để chạy bất đồng bộ, Python có thư viện chuẩn asyncio, xây trên một thứ gọi là coroutine.

Coroutine là gì? Nói gọn: một hàm có thể tạm dừng giữa đường để nhường chỗ cho việc khác, rồi quay lại tiếp tục đúng chỗ đã dừng. Trong Python bạn tạo nó bằng async def, và điểm tạm dừng là chỗ có từ khoá await. Bạn đã dùng đúng khái niệm này trong JavaScript rồi — async function của JS cũng là một coroutine, chỉ khác tên gọi.

🕳️ Khác biệt lớn nhất với JS: gọi coroutine KHÔNG làm nó chạy
Đây là chỗ gần như mọi lập trình viên JS mắc lần đầu, và triệu chứng của nó rất khó đoán vì code không báo lỗi — nó chỉ đơn giản là không làm gì.

Trong JavaScript, gọi fetchData() là hàm bắt đầu chạy ngay; bạn nhận về một Promise đại diện cho công việc đang diễn ra. Trong Python, gọi fetch_data() không chạy một dòng nào trong thân hàm. Nó trả về một đối tượng coroutine — như một tờ phiếu ghi "việc cần làm", nằm im cho tới khi có ai đó thực sự đem đi thực hiện.

Ai đem đi thực hiện? Hoặc await (nếu bạn đang ở trong một coroutine khác), hoặc asyncio.run() ở cấp ngoài cùng. Đó chính là nghĩa cụ thể của câu "Event Loop trong Python không tự chạy ngầm": JavaScript luôn có một Event Loop sẵn sàng nhận việc, còn Python thì bạn phải tự dựng nó lên bằng asyncio.run().

Dấu hiệu nhận biết khi mắc lỗi này: Python in ra cảnh báo RuntimeWarning: coroutine 'fetch_data' was never awaited và hàm của bạn im lặng không chạy. Thấy dòng đó nghĩa là bạn quên await hoặc quên asyncio.run().
goi_coroutine.py
import asyncio

async def fetch_data():
    print("Fetching data...")
    return 42

# ❌ Called the JavaScript way: NOTHING is printed
result = fetch_data()
print(result)      # <coroutine object fetch_data at 0x104f...>  ← just the "ticket"

# ✅ The ticket has to be handed to someone who runs it
result = asyncio.run(fetch_data())
print(result)      # prints "Fetching data..." then 42
⚠️ Cạm bẫy: Chặn đứng Event Loop bằng mã đồng bộ (Blocking the Loop)
Nếu bạn gọi một tác vụ đồng bộ chặn luồng (như vòng lặp tính toán nặng CPU, hoặc hàm dừng hệ thống time.sleep(5)) bên trong một hàm async của Python, toàn bộ Event Loop của asyncio sẽ bị treo cứng. Mọi tác vụ async chạy song song khác sẽ bị đóng băng. Để tránh điều này, bạn phải sử dụng các thư viện async tương thích (như aiohttp cho mạng), hoặc chuyển tác vụ nặng sang chạy ở thread riêng biệt.

Cách khắc phục cụ thể (từ Python 3.9 trở lên): dùng asyncio.to_thread() để đẩy hàm chặn luồng sang một thread nền, giải phóng Event Loop tiếp tục xử lý các tác vụ async khác trong lúc chờ:

fix_blocking_call.py
import asyncio
import time

def cpu_heavy_task():
    time.sleep(3)  # stands in for heavy CPU work, or an old library with no async API
    return "Heavy work done"

async def ticker():
    for i in range(5):
        print(f"Tick {i} — the Event Loop is still running...")
        await asyncio.sleep(0.5)

async def main():
    # ❌ Calling cpu_heavy_task() directly here would freeze ticker() for 3 seconds.
    # ✅ Hand it to a separate thread; the Event Loop stays free to run ticker():
    result_task = asyncio.create_task(asyncio.to_thread(cpu_heavy_task))
    ticker_task = asyncio.create_task(ticker())
    result = await result_task
    print(result)
    await ticker_task

asyncio.run(main())
# Output: "Tick 0..4" keeps printing every 0.5s while cpu_heavy_task() runs for 3s

Hãy cùng so sánh cấu trúc mã viết async/await giữa JavaScript và Python:

async_compare.js
// JavaScript version (Node.js)
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));

async function fetchMetadata() {
  console.log("Download starting...");
  await delay(1000); // non-blocking wait
  console.log("Download finished.");
  return { status: 200 };
}

// Just call it — JS already has an Event Loop running in the background
fetchMetadata();
async_compare.py
# Python version (asyncio)
import asyncio

async def fetch_metadata():
    print("Download starting...")
    await asyncio.sleep(1) # non-blocking wait, the equivalent of delay()
    print("Download finished.")
    return {"status": 200}

# You must start the Event Loop yourself
result = asyncio.run(fetch_metadata())

4. Dự án thực hành: Công cụ thu thập và chuẩn hóa dữ liệu thô

Để biến lý thuyết thành thực tiễn, chúng ta sẽ xây dựng một script Python hoàn chỉnh chạy trong môi trường ảo venv. Script làm ba việc theo thứ tự: tự tạo một tệp raw_feedback.json chứa vài phản hồi thô của người dùng (bạn không cần chuẩn bị file đầu vào nào — script tự sinh ra để bài này chạy được ngay), đọc lại tệp đó rồi làm sạch từng dòng văn bản, và ghi kết quả ra cleaned_feedback.csv.

Việc "làm sạch" ở đây rất đơn giản — cắt khoảng trắng thừa hai đầu và chuyển hết về chữ thường — nhưng cấu trúc ba bước đọc → biến đổi → ghi chính là bộ xương của mọi data pipeline mà bạn sẽ gặp lại suốt series, kể cả khi dữ liệu là hàng triệu tài liệu cho hệ thống RAG ở Bài 14.

data_cleaner.py
import json
import csv
import os

def clean_text(text):
    """
    Basic text cleaning: trim surrounding whitespace and lowercase.
    """
    # Real-world data always contains empty cells and wrong types. A text helper
    # that does not guard against them breaks the pipeline at record 10,000.
    if not isinstance(text, str):
        return ""
    return text.strip().lower()

def run_cleaning_pipeline(input_path, output_path):
    print("--- Starting the data cleaning pipeline ---")

    if not os.path.exists(input_path):
        print(f"Error: file {input_path} does not exist.")
        return

    # `with` closes the file on the way out, even if an error is raised inside
    # the block. It is Python's version of the try/finally you write by hand in
    # JavaScript. And encoding="utf-8" is not optional: drop it and accented
    # text turns into garbage characters on Windows.
    with open(input_path, "r", encoding="utf-8") as file:
        raw_data = json.load(file)

    cleaned_records = []

    for index, record in enumerate(raw_data):
        # .get(key, fallback) returns the fallback instead of raising when the
        # key is missing — the safe way to read data that came from outside.
        user_id = record.get("id", index)
        raw_comment = record.get("comment", "")

        cleaned_records.append({
            "user_id": user_id,
            "cleaned_comment": clean_text(raw_comment)
        })

    # Write the cleaned rows out as CSV, ready for the NLP lessons later on.
    with open(output_path, "w", newline="", encoding="utf-8") as csv_file:
        fieldnames = ["user_id", "cleaned_comment"]
        writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
        writer.writeheader()
        writer.writerows(cleaned_records)

    print(f"Done. Wrote {len(cleaned_records)} cleaned records to {output_path}")

# Everything below runs only when this file is executed directly,
# not when it is imported from another module.
if __name__ == "__main__":
    # The sample input is generated here so the lesson runs with no setup.
    # These comments stay in Vietnamese on purpose: they are the DATA being
    # cleaned, and they demonstrate why encoding="utf-8" matters above.
    mock_data = [
        {"id": 101, "comment": "  Mô hình AI chạy RẤT NHANH!  "},
        {"id": 102, "comment": "Tôi Cần hỗ Trợ kỹ thuật gấp...   "},
        {"id": 103, "comment": "   Tuyệt VỜI, 10 điểm. "}
    ]

    with open("raw_feedback.json", "w", encoding="utf-8") as f:
        json.dump(mock_data, f, ensure_ascii=False, indent=2)

    run_cleaning_pipeline("raw_feedback.json", "cleaned_feedback.csv")

Khối code trên dài, nhưng chỉ có ba chỗ đáng để bạn dừng lại đọc kỹ. Thứ nhất là if not isinstance(text, str): return "" trong clean_text — dữ liệu thật luôn có ô trống hoặc sai kiểu, và một hàm xử lý văn bản không tự vệ sẽ làm vỡ cả pipeline ở đúng bản ghi thứ 10.000. Thứ hai là with open(...) as f: từ khoá with tự đóng file khi ra khỏi khối, kể cả khi có lỗi xảy ra giữa đường — nó là bản Python của try/finally mà bạn hay viết tay trong JavaScript. Thứ ba là encoding="utf-8" ở mọi lệnh mở file: bỏ nó đi thì tiếng Việt có dấu sẽ thành ký tự rác trên Windows, và đây là lỗi tốn thời gian một cách vô ích nhất khi xử lý dữ liệu tiếng Việt.

Để chạy dự án thực hành này trên máy tính của bạn, hãy thực hiện theo đúng các bước quy chuẩn sau:

  1. Mở terminal tại thư mục chứa file code và khởi tạo môi trường ảo: python3 -m venv venv.
  2. Kích hoạt môi trường ảo: source venv/bin/activate (đối với macOS/Linux) hoặc venv\Scripts\activate (đối với Windows).
  3. Chạy script Python: python data_cleaner.py.
  4. Kiểm tra thư mục dự án. Bạn sẽ thấy hai tệp mới: raw_feedback.json — dữ liệu thô do chính script tự sinh ra ở bước đầu — và cleaned_feedback.csv chứa các bình luận đã được cắt khoảng trắng và chuyển về chữ thường. Mở cả hai ra so sánh cạnh nhau là cách nhanh nhất để thấy đúng những gì hàm clean_text đã làm.

Tóm tắt bài học & Cầu nối kiến thức

🔑 Bài học đạt được:
  • Đạt được: Làm chủ cú pháp Python cơ bản và sự khác biệt về kiểu dữ liệu so với JavaScript.
  • Đạt được: Biết cách thiết lập môi trường ảo venv và quản lý thư viện bằng pip thay thế cho npm/node_modules.

Cầu nối bài tiếp theo: Có được tư duy viết mã Python, bài học tiếp theo sẽ đưa ta bước vào thế giới toán học nền tảng của Trí tuệ nhân tạo: các phép toán Vector, Ma trận và Đạo hàm được lập trình trực quan bằng Python thuần túy.

Tải file code thực hành minh họa bài học

File Python data_cleaner.py — script xử lý và chuẩn hóa dữ liệu mẫu dùng trong bài học này (chạy python data_cleaner.py, không cần cài thêm gì):

Tải về data_cleaner.py

📖 Tài liệu tham khảo

Bài viết liên quan trong series

Bài 2: Đại số Tuyến tính & Đạo hàm qua dòng lệnh Quay lại Lộ trình Kỹ Sư AI Thực Chiến

Bình luận