Skip to content

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*argskeyword-only**kwargs. Vi phạm thứ tự này là SyntaxError.

*args**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=5

Lambda — 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) * 3

Higher-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ập

Closure đượ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)
# 5

functools.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 bag

Sai 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 increment

Sai 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 result

Under the Hood

LEGB scope resolution

Python tìm biến theo thứ tự L → E → G → B:

ScopeViết tắtPhạm vi
LocalLBiến trong function hiện tại
EnclosingEBiến trong function bao ngoài (closures)
GlobalGBiến ở module level
Built-inBprint, 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"

globalnonlocal 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_VALUE

Vớ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_VALUE

Performance: 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_cache cho 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=i hoặc factory function trong loops
  • [ ] Dùng nonlocal khi cần modify closure variable

Defaults và scope

  • [ ] Không dùng mutable default arguments (list, dict, set)
  • [ ] Sử dụng None pattern: def f(items: list | None = None)
  • [ ] Tránh global trong production code
  • [ ] Không shadow built-in names (list, dict, type, id)

Patterns

  • [ ] Dùng functools.partial thay vì wrapper lambda cho đóng băng tham số
  • [ ] Dùng functools.wraps trong 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