Giao diện
Functions & Lambdas — Hàm là first-class citizen
Một buổi sáng, bạn review một PR và thấy một function 200 dòng làm ba việc: validate input, gọi API, và format response. Bạn comment “tach ra đi” và nhận lại: “tach như nào?”. Câu hỏi đó không phải về cú pháp def — nó là về thiết kế function: mỗi function làm một việc, nhận đầu vào rõ ràng, trả kết quả dự đoán được.
Trong Python, functions là first-class objects — chúng có thể được gán vào biến, truyền làm tham số, trả về từ function khác, và lưu trong data structures. Đây không phải tính năng fancy — đây là nền tảng của decorators, callbacks, và hầu hết các design patterns trong Python.
Bài này đi từ cơ bản (def, lambda, *args/**kwargs) đến chiến lược thiết kế function sạch, closures, và cách CPython biên dịch function bên dưới.
Bức tranh tư duy
Hãy nghĩ về function như một chiếc máy trong nhà máy.
Mỗi máy (function) có đầu vào rõ ràng (tham số), thực hiện một công đoạn cụ thể (logic), và trả ra sản phẩm (return value). Máy tốt là máy làm một việc duy nhất và làm tốt. Một chiếc máy vừa đúc nhựa, vừa cắt kim loại, vừa sơn sản phẩm là chiếc máy khó bảo trì, khó thay thế linh kiện, và chắc chắn sẽ hỏng ở công đoạn bạn không ngờ.
Lambda là chiếc máy mini dùng một lần — như máy dán nhãn tạm thời trên dây chuyền. Tiện cho thao tác đơn giản (key function trong sorted), nhưng nếu bạn cần máy có tên, có manual (docstring), và có thể sửa chữa — hãy dùng def.
Closure là chiếc máy “nhớ” cài đặt từ lần cấu hình trước. Bạn cấu hình nhiệt độ lò nung một lần, và từ đó mọi sản phẩm đi qua đều được nung ở nhiệt độ đó — kể cả khi người cấu hình đã rời nhà máy (enclosing scope đã kết thúc).
Cốt lõi kỹ thuật
Cấu trúc function và các loại tham số
python
def process_order(
order_id: str, # positional or keyword
*items: str, # variable positional
priority: int = 0, # keyword-only (sau *)
**metadata: str # variable keyword
) -> dict[str, object]:
"""Xử lý đơn hàng với số lượng items linh hoạt.
Args:
order_id: Mã đơn hàng.
items: Danh sách mã sản phẩm.
priority: Độ ưu tiên (0 = thường, 1 = gấp).
metadata: Thông tin bổ sung tùy ý.
Returns:
Dict chứa kết quả xử lý.
"""
return {
"order_id": order_id,
"items": items, # tuple
"priority": priority,
"metadata": metadata, # dict
}
# Gọi với nhiều cách
process_order("ORD-1", "SKU-A", "SKU-B", priority=1, source="web")Thứ tự tham số bắt buộc: positional → *args → keyword-only → **kwargs. Vi phạm thứ tự này là SyntaxError.
*args và **kwargs trong thực tế
python
# *args — nhận số lượng tham số không cố định
def calculate_total(*prices: float, tax_rate: float = 0.1) -> float:
subtotal = sum(prices)
return subtotal * (1 + tax_rate)
calculate_total(100.0, 250.5, 89.9, tax_rate=0.08)
# **kwargs — nhận keyword arguments tùy ý
def create_config(**overrides: str | int | bool) -> dict[str, str | int | bool]:
defaults: dict[str, str | int | bool] = {
"host": "localhost",
"port": 8080,
"debug": False,
}
return {**defaults, **overrides}
config = create_config(port=3000, debug=True)
# {'host': 'localhost', 'port': 3000, 'debug': True}Unpacking — giải nén dữ liệu
python
# Giải nén list/tuple vào function
coordinates = [10.5, 20.3, 30.1]
def plot_3d(x: float, y: float, z: float) -> None:
print(f"({x}, {y}, {z})")
plot_3d(*coordinates) # tương đương plot_3d(10.5, 20.3, 30.1)
# Giải nén dict vào keyword arguments
settings = {"host": "0.0.0.0", "port": 443}
def connect(host: str, port: int) -> None:
print(f"Connecting to {host}:{port}")
connect(**settings)
# Extended unpacking (Python 3)
first, *middle, last = [1, 2, 3, 4, 5]
# first=1, middle=[2, 3, 4], last=5Lambda — khi nào dùng, khi nào không
Lambda phù hợp cho một biểu thức duy nhất được dùng ngay tại chỗ, điển hình là tham số key trong sorted, min, max.
python
# ✅ Lambda phù hợp: key function ngắn gọn
orders = [("ORD-1", 250.0), ("ORD-2", 100.0), ("ORD-3", 500.0)]
sorted_orders = sorted(orders, key=lambda o: o[1])
# ✅ Lambda phù hợp: callback đơn giản
from concurrent.futures import ThreadPoolExecutor
executor = ThreadPoolExecutor(max_workers=4)
future = executor.submit(lambda: fetch_data("https://api.example.com"))
# ❌ Lambda không phù hợp: gán vào biến (PEP 8 khuyến cáo không làm)
process = lambda x: x.strip().lower().replace(" ", "_") # dùng def thay thế
# ✅ Thay thế bằng def
def normalize_key(text: str) -> str:
return text.strip().lower().replace(" ", "_")
# ❌ Lambda không phù hợp: logic phức tạp
result = map(lambda x: x ** 2 if x > 0 else abs(x) * 3, numbers)
# ✅ Dùng def cho logic có điều kiện
def transform_value(x: int) -> int:
if x > 0:
return x ** 2
return abs(x) * 3Higher-order functions
Function nhận function làm tham số hoặc trả về function — nền tảng của functional programming trong Python.
python
from typing import Callable
# Function trả về function (factory pattern)
def make_multiplier(factor: int) -> Callable[[int], int]:
def multiplier(x: int) -> int:
return x * factor
return multiplier
double = make_multiplier(2)
triple = make_multiplier(3)
double(10) # 20
triple(10) # 30
# Function nhận function làm tham số
def apply_to_all(
items: list[int],
transform: Callable[[int], int],
predicate: Callable[[int], bool] | None = None,
) -> list[int]:
if predicate:
items = [x for x in items if predicate(x)]
return [transform(x) for x in items]
result = apply_to_all(
[1, -2, 3, -4, 5],
transform=lambda x: x ** 2,
predicate=lambda x: x > 0,
)
# [1, 9, 25]Closures
Closure là function "bắt giữ" biến từ enclosing scope. Biến được giữ sống ngay cả khi enclosing function đã return.
python
def make_counter(start: int = 0) -> Callable[[], int]:
"""Factory tạo counter với state riêng."""
count = start
def increment() -> int:
nonlocal count # khai báo: sử dụng biến từ enclosing scope
count += 1
return count
return increment
counter_a = make_counter()
counter_b = make_counter(100)
counter_a() # 1
counter_a() # 2
counter_b() # 101 — state độc lậpClosure được CPython lưu trong __closure__ — một tuple các cell objects:
python
add_five = make_multiplier(5) # từ ví dụ trước
print(add_five.__closure__)
# (<cell at 0x...: int object at 0x...>,)
print(add_five.__closure__[0].cell_contents)
# 5functools.partial — "đóng băng" một số tham số
python
from functools import partial
def send_email(
to: str, subject: str, body: str, priority: str = "normal"
) -> None:
print(f"[{priority}] To: {to}, Subject: {subject}")
# Tạo phiên bản chuyên biệt
send_alert = partial(send_email, priority="high", subject="ALERT")
send_report = partial(send_email, subject="Daily Report")
send_alert(to="ops@company.com", body="Server down")
send_report(to="team@company.com", body="All systems normal")
# partial giữ metadata để debug
print(send_alert.func) # <function send_email at ...>
print(send_alert.args) # ()
print(send_alert.keywords) # {'priority': 'high', 'subject': 'ALERT'}Thực chiến
Tình huống 1: Retry decorator với configurable policy
python
import time
from typing import Callable, ParamSpec, TypeVar
from functools import wraps
P = ParamSpec('P')
R = TypeVar('R')
def retry(
max_attempts: int = 3,
delay: float = 1.0,
exceptions: tuple[type[Exception], ...] = (Exception,),
) -> Callable[[Callable[P, R]], Callable[P, R]]:
"""Decorator retry với exponential backoff."""
def decorator(func: Callable[P, R]) -> Callable[P, R]:
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
last_exception: Exception | None = None
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except exceptions as exc:
last_exception = exc
if attempt < max_attempts:
wait = delay * (2 ** (attempt - 1))
time.sleep(wait)
raise last_exception # type: ignore[misc]
return wrapper
return decorator
@retry(max_attempts=3, delay=0.5, exceptions=(ConnectionError, TimeoutError))
def fetch_user_profile(user_id: int) -> dict:
"""Gọi external API với retry tự động."""
response = http_client.get(f"/users/{user_id}")
response.raise_for_status()
return response.json()Tình huống 2: Pipeline builder với higher-order functions
python
from typing import Callable, TypeVar
T = TypeVar('T')
def compose(*funcs: Callable[[T], T]) -> Callable[[T], T]:
"""Compose nhiều functions thành một pipeline."""
def pipeline(value: T) -> T:
for func in funcs:
value = func(value)
return value
return pipeline
# Xây dựng text processing pipeline
def strip_whitespace(text: str) -> str:
return text.strip()
def lowercase(text: str) -> str:
return text.lower()
def remove_special_chars(text: str) -> str:
return "".join(c for c in text if c.isalnum() or c == " ")
normalize = compose(strip_whitespace, lowercase, remove_special_chars)
normalize(" Hello, World! ") # "hello world"Sai lầm điển hình
Sai lầm 1: Mutable default arguments
Đây là bug kinh điển nhất trong Python. Default value được evaluate một lần duy nhất lúc định nghĩa function, không phải mỗi lần gọi.
python
# SAI: list được share giữa các lần gọi
def add_item(item: str, bag: list[str] = []) -> list[str]:
bag.append(item)
return bag
print(add_item("apple")) # ['apple']
print(add_item("banana")) # ['apple', 'banana'] — BUG!
# ĐÚNG: None pattern
def add_item(item: str, bag: list[str] | None = None) -> list[str]:
if bag is None:
bag = []
bag.append(item)
return bagSai lầm 2: Late binding trong closures
python
# SAI: Tất cả lambdas tham chiếu cùng một biến i
callbacks = [lambda: i for i in range(5)]
print([cb() for cb in callbacks]) # [4, 4, 4, 4, 4]
# Lambda capture REFERENCE đến i, không phải VALUE.
# Khi loop kết thúc, i = 4 cho tất cả.
# ĐÚNG CÁCH 1: default argument capture value
callbacks = [lambda i=i: i for i in range(5)]
print([cb() for cb in callbacks]) # [0, 1, 2, 3, 4]
# ĐÚNG CÁCH 2: factory function
def make_callback(n: int) -> Callable[[], int]:
return lambda: n
callbacks = [make_callback(i) for i in range(5)]Sai lầm 3: Lạm dụng lambda
python
# SAI: Lambda nhiều dòng, khó đọc, không có tên
transform = lambda x: (
x.strip()
.lower()
.replace(" ", "_")
.replace("-", "_")
if isinstance(x, str) else str(x)
)
# ĐÚNG: def với tên rõ ràng và docstring
def to_snake_case(value: str | object) -> str:
"""Chuyển giá trị thành snake_case string."""
text = str(value).strip().lower()
return text.replace(" ", "_").replace("-", "_")Sai lầm 4: Không dùng nonlocal khi modify closure variable
python
# SAI: UnboundLocalError
def make_counter() -> Callable[[], int]:
count = 0
def increment() -> int:
count += 1 # Python coi count là local vì có assignment!
return count
return increment
# make_counter()() # UnboundLocalError
# ĐÚNG: khai báo nonlocal
def make_counter() -> Callable[[], int]:
count = 0
def increment() -> int:
nonlocal count
count += 1
return count
return incrementSai lầm 5: return trong finally ghi đè return trong try
python
# SAI: finally luôn thắng
def confusing() -> str:
try:
return "from try"
finally:
return "from finally" # giá trị này được trả về!
confusing() # "from finally"
# ĐÚNG: không return trong finally
def correct() -> str:
result = "default"
try:
result = "from try"
finally:
pass # cleanup, không return
return resultUnder the Hood
LEGB scope resolution
Python tìm biến theo thứ tự L → E → G → B:
| Scope | Viết tắt | Phạm vi |
|---|---|---|
| Local | L | Biến trong function hiện tại |
| Enclosing | E | Biến trong function bao ngoài (closures) |
| Global | G | Biến ở module level |
| Built-in | B | print, len, int, etc. |
python
x = "global"
def outer() -> None:
x = "enclosing"
def inner() -> None:
x = "local"
print(x) # L: "local"
inner()
print(x) # E: "enclosing"
outer()
print(x) # G: "global"global và nonlocal là hai keyword cho phép bạn ghi đè quy tắc LEGB — nhưng hãy hạn chế sử dụng global trong production code vì nó tạo implicit dependency giữa function và module state.
CPython biên dịch function như thế nào
Mỗi function trong CPython được biên dịch thành một code object chứa bytecode. Bạn có thể inspect:
python
def add(a: int, b: int) -> int:
return a + b
code = add.__code__
print(code.co_varnames) # ('a', 'b') — local variables
print(code.co_consts) # (None,) — constants
print(code.co_stacksize) # 2 — max stack depth
import dis
dis.dis(add)
# 0 LOAD_FAST 0 (a)
# 2 LOAD_FAST 1 (b)
# 4 BINARY_ADD
# 6 RETURN_VALUEVới closure, CPython dùng LOAD_DEREF thay vì LOAD_FAST để truy cập biến từ cell object:
python
def make_adder(n: int) -> Callable[[int], int]:
def adder(x: int) -> int:
return x + n
return adder
dis.dis(make_adder(5))
# 0 LOAD_FAST 0 (x)
# 2 LOAD_DEREF 0 (n) # <-- truy cập qua closure cell
# 4 BINARY_ADD
# 6 RETURN_VALUEPerformance: function call overhead
Function call trong CPython có overhead không nhỏ: tạo frame object, push/pop stack, resolve scope. Với tight loops trên dữ liệu lớn, overhead này cộng dồn đáng kể.
Một số chiến lược giảm overhead:
- Dùng
map()với built-in function thay vì list comprehension với function call (vì built-ins được gọi ở C level) functools.lru_cachecho expensive pure functions- Inline logic trong hot loops thay vì gọi function con (nhưng chỉ khi profiler xác nhận đây là bottleneck)
Checklist ghi nhớ
✅ Checklist triển khai
Thiết kế function
- [ ] Mỗi function làm một việc duy nhất (Single Responsibility)
- [ ] Tên function là verb + noun:
calculate_tax,fetch_user_profile - [ ] Tham số không quá 5 — nếu nhiều hơn, gom vào dataclass/TypedDict
- [ ] Mọi function có type annotations đầy đủ
Lambda và closures
- [ ] Lambda chỉ dùng cho logic một biểu thức, không gán vào biến
- [ ] Nhớ late binding: dùng
i=ihoặc factory function trong loops - [ ] Dùng
nonlocalkhi cần modify closure variable
Defaults và scope
- [ ] Không dùng mutable default arguments (list, dict, set)
- [ ] Sử dụng
Nonepattern:def f(items: list | None = None) - [ ] Tránh
globaltrong production code - [ ] Không shadow built-in names (
list,dict,type,id)
Patterns
- [ ] Dùng
functools.partialthay vì wrapper lambda cho đóng băng tham số - [ ] Dùng
functools.wrapstrong mọi decorator - [ ] Keyword-only arguments (sau
*) cho tham số cấu hình
Bài tập luyện tập
Bài 1: Function pipeline (Foundation)
Viết một function pipe(value, *functions) nhận một giá trị và một chuỗi functions, áp dụng tuần tự từ trái sang phải. Thêm type annotations đầy đủ.
Gợi ý
python
from typing import Callable, TypeVar
T = TypeVar('T')
def pipe(value: T, *functions: Callable[[T], T]) -> T:
for func in functions:
value = func(value)
return value
result = pipe(
" Hello World ",
str.strip,
str.lower,
lambda s: s.replace(" ", "-"),
)
# "hello-world"Bài 2: Rate limiter với closure (Intermediate)
Implement một make_rate_limiter(max_calls, period_seconds) trả về một function kiểm tra xem đã vượt giới hạn chưa. Dùng closure để lưu state (danh sách timestamps của các lần gọi gần nhất).
Gợi ý
python
import time
from typing import Callable
def make_rate_limiter(
max_calls: int, period_seconds: float
) -> Callable[[], bool]:
"""Trả về function: True nếu được phép gọi, False nếu vượt limit."""
call_times: list[float] = []
def is_allowed() -> bool:
nonlocal call_times
now = time.monotonic()
# Loại bỏ các lần gọi ngoài window
call_times = [t for t in call_times if now - t < period_seconds]
if len(call_times) >= max_calls:
return False
call_times.append(now)
return True
return is_allowed
limiter = make_rate_limiter(max_calls=3, period_seconds=1.0)
print(limiter()) # True
print(limiter()) # True
print(limiter()) # True
print(limiter()) # False — vượt 3 calls/second🧠 Quiz
Đoạn code sau in ra gì?
python
funcs = []
for x in range(3):
funcs.append(lambda: x)
print([f() for f in funcs])- [ ]
[0, 1, 2] - [x]
[2, 2, 2] - [ ]
[0, 0, 0] - [ ]
SyntaxError
Giải thích: Lambda capture reference đến biến x, không phải value. Khi loop kết thúc, x = 2 và tất cả lambdas đều trả về 2. Sửa bằng lambda x=x: x.
🧠 Quiz
Default argument [] trong def f(items=[]) được evaluate khi nào?
- [x] Một lần duy nhất, lúc định nghĩa function
- [ ] Mỗi lần gọi function
- [ ] Lúc import module
- [ ] Lúc Python compile bytecode
Giải thích: Default values được evaluate một lần khi def statement được thực thi. Đó là lý do list [] được share giữa các lần gọi, gây bug. Dùng None pattern để tránh.
Liên kết học tiếp
Từ khóa: function, lambda, closure, LEGB scope, nonlocal, global, *args, **kwargs, functools.partial, higher-order function, first-class citizen, bytecode, code object