Chương 04 · Control Flow & Functions

Luồng điều khiển và hàm

Python xây dựng luồng điều khiển quanh hai trục: iterable (for đi qua mọi thứ có thể duyệt) và structural pattern matching (match/case từ 3.10+). Hàm thì cực kỳ linh hoạt — default args, *args/**kwargs, positional-only, keyword-only, closure, lambda. Chương này gỡ rối từng cơ chế, đặc biệt LEGB scope và cái bẫy mutable default đã được nhắc ở Chương 2.

Độ dài: ~1000 dòng Bài tập: 5 Quiz: 7 Prerequisites: Chương 1-3
🎯 Mục tiêu chương
  • Viết if/elif/else đúng PEP 8 indent, hiểu Python không có ternary ?: mà dùng x if cond else y.
  • Master match/case — structural pattern matching, hơn hẳn switch của ngôn ngữ khác: match shape data, sequence, mapping, class.
  • Hiểu for x in iterable dùng được cho list/tuple/string/dict/file/generator; dùng enumerate, zip đúng cách.
  • Biết khi nào dùng while thay vì for; break/continue để giảm nesting.
  • Hiểu else clause của loop — đặc thù Python, chạy khi loop xong không bị break.
  • Viết function với def đúng convention: type hints + docstring + return.
  • Phân biệt default args, keyword args, *args, **kwargs, positional-only /, keyword-only *.
  • Master LEGB scope, global vs nonlocal, closure pattern.
  • Hiểu lambda chỉ là 1 expression — dùng đúng chỗ (sorted key, filter), không lạm dụng.
  • Định nghĩa chính xác pass-by-object-reference — Python không phải pass-by-value, cũng không phải pass-by-reference.
Flat is better than nested. — Zen of Python

1. if / elif / else

Python không dùng dấu ngoặc nhọn {} để gom khối — mà dùng indent. PEP 8 quy ước 4 space (không phải tab). Indent quyết định khối nào thuộc về if. Sau điều kiện là dấu hai chấm :, không có then.

Cú pháp if/elif/else
def classify(score: int) -> str:
    if score >= 90:
        return 'A'
    elif score >= 80:
        return 'B'
    elif score >= 70:
        return 'C'
    else:
        return 'F'

# Lưu ý: `elif` viết liền — KHÔNG có `else if` như JS/C.
# Đây là từ khóa riêng, tiết kiệm 1 cấp indent.

1.1. Không có ?: — dùng conditional expression

Python không có toán tử ternary cond ? a : b. Thay vào đó là conditional expression với cú pháp tự nhiên hơn: a if cond else b — đọc như câu tiếng Anh.

Conditional expression
age = 20
status = 'adult' if age >= 18 else 'minor'
# status == 'adult'

# Nested (đọc khó — nên tránh):
grade = 'A' if score >= 90 else 'B' if score >= 80 else 'C'

# Khi tránh: chuyển sang if/elif/else 4 dòng cho dễ đọc.

1.2. Truthy / Falsy trong điều kiện

Như đã nói Chương 2: if x: không cần so sánh tường minh với None hay rỗng. Python tự coi các giá trị sau là falsy: None, False, số 0 (0, 0.0, 0j), chuỗi/list/tuple/dict/set rỗng. Còn lại là truthy.

Pythonic check
# Anti-pattern (Java-ish):
if len(items) > 0 and items != None:
    ...

# Pythonic:
if items:
    ...

# Cảnh báo: đừng dùng truthy khi cần phân biệt None với 0/rỗng!
def connect(timeout=None):
    if timeout:           # BUG: timeout=0 cũng bị bỏ qua
        ...
    if timeout is not None:   # ĐÚNG: chỉ check None
        ...
⚠️ Pitfall — falsy vs None

if x:if x is not None: không tương đương khi x có thể là 0, 0.0, "", []. Trong API có argument optional, luôn kiểm tra is None để tránh confuse "không truyền" với "truyền giá trị falsy".

2. match / case — Structural pattern matching (3.10+)

Trước Python 3.10, không có cú pháp switch. Từ 3.10, PEP 634 mang đến structural pattern matching — mạnh hơn switch của Java/C/JS rất nhiều: không chỉ so sánh giá trị mà còn match cấu trúc dữ liệubind biến.

match cơ bản — như switch
def http_status(code: int) -> str:
    match code:
        case 200:
            return 'OK'
        case 301 | 302:           # OR pattern
            return 'Redirect'
        case 404:
            return 'Not Found'
        case _:                  # wildcard — như `default`
            return 'Unknown'

2.1. Match sequence (list / tuple)

Đây là điểm khác biệt lớn với switch: match có thể nhìn vào cấu trúc của list/tuple. Dùng *rest để gom phần còn lại.

Sequence pattern
def describe_list(lst: list) -> str:
    match lst:
        case []:
            return 'rỗng'
        case [x]:
            return f'một phần tử: {x}'
        case [x, y]:
            return f'hai phần tử: {x}, {y}'
        case [1, 2, *rest]:        # bắt đầu bằng 1, 2 — phần dư vào rest
            return f'bắt đầu 1,2 — rest = {rest}'
        case [first, *_, last]:    # _ = bỏ qua
            return f'đầu = {first}, cuối = {last}'

describe_list([])              # 'rỗng'
describe_list([1, 2, 3, 4])     # 'bắt đầu 1,2 — rest = [3, 4]'
describe_list([5, 6, 7, 8])     # 'đầu = 5, cuối = 8'

2.2. Match mapping (dict)

Match key trong dict, bind value vào biến. Pattern chỉ cần subset của dict.

Mapping pattern
def handle_event(event: dict) -> str:
    match event:
        case {'type': 'click', 'x': x, 'y': y}:
            return f'click tại ({x}, {y})'
        case {'type': 'keypress', 'key': key}:
            return f'phím {key}'
        case {'type': t}:
            return f'event chưa xử lý: {t}'
        case _:
            return 'không phải event dict'

handle_event({'type': 'click', 'x': 10, 'y': 20, 'meta': True})
# 'click tại (10, 20)' — pattern match dù dict có thêm key 'meta'

2.3. Match class (dataclass / instance)

Mạnh nhất: match shape của instance. Dùng tên class + đặt vị trí/keyword pattern lên field.

Class pattern
from dataclasses import dataclass

@dataclass
class Point:
    x: int
    y: int

def where(p: Point) -> str:
    match p:
        case Point(x=0, y=0):
            return 'gốc tọa độ'
        case Point(x=0, y=y_val):     # bind y vào y_val
            return f'trục Y, y={y_val}'
        case Point(x=x_val, y=0):
            return f'trục X, x={x_val}'
        case Point():
            return f'điểm thường: ({p.x}, {p.y})'

where(Point(0, 5))     # 'trục Y, y=5'

2.4. Guard với if

Thêm điều kiện sau pattern bằng if — gọi là guard.

Guard clause
match point:
    case Point(x=x, y=y) if x == y:
        print('trên đường y=x')
    case Point(x=x, y=y) if x > 0 and y > 0:
        print('góc phần tư I')
    case _:
        print('khác')

match khác switch như thế nào?

switch trong C/Java/JS chỉ so sánh bằng giá trị (thường là số nguyên hay chuỗi). match Python so sánh cấu trúc: shape của list, key của dict, class + field, hợp pattern (|), guard. Đặc biệt — bind biến ngay trong pattern. Hệ quả: thay được rất nhiều chuỗi if isinstance(x, ...): ....

3. Vòng lặp for

for trong Python không phải for (i=0; i<n; i++) như C/Java. Mà là for x in iterable — duyệt qua mọi iterable: list, tuple, string, dict, set, file, generator, range...

for x in iterable
# Duyệt list
for name in ['An', 'Bình', 'Cường']:
    print(name)

# Duyệt string — từng ký tự
for ch in 'python':
    print(ch)

# Duyệt range — tương đương for(i=0; i<5; i++)
for i in range(5):           # 0, 1, 2, 3, 4
    print(i)

for i in range(1, 10, 2):     # start=1, stop=10, step=2 → 1, 3, 5, 7, 9
    print(i)

# Duyệt dict — KEY (mặc định)
prices = {'apple': 10, 'banana': 5}
for k in prices:           # 'apple', 'banana'
    print(k)

for k, v in prices.items():   # key + value
    print(k, v)

# Duyệt file — từng dòng (không cần load toàn bộ file)
with open('data.txt') as f:
    for line in f:
        print(line.strip())

3.1. enumeratezip

Hai built-in cực kỳ thông dụng cho loop pattern. enumerate ghép index + value; zip ghép song song nhiều iterable.

enumerate + zip
# Anti-pattern (C-style):
names = ['An', 'Bình', 'Cường']
for i in range(len(names)):
    print(i, names[i])

# Pythonic:
for i, name in enumerate(names):
    print(i, name)
# 0 An
# 1 Bình
# 2 Cường

# enumerate có start tùy chỉnh:
for i, name in enumerate(names, start=1):
    print(i, name)
# 1 An
# 2 Bình
# 3 Cường

# zip — duyệt song song
ages = [22, 25, 28]
for name, age in zip(names, ages):
    print(name, age)
# An 22
# Bình 25
# Cường 28

# zip dừng ở iterable ngắn nhất.
# Python 3.10+ thêm zip(..., strict=True) — raise nếu độ dài khác nhau.
for a, b in zip([1,2,3], ['a','b'], strict=True):
    print(a, b)
# ValueError: zip() argument 2 is shorter than argument 1

4. Vòng lặp while

while cond: chạy đến khi cond falsy. Trong Python, while ít dùng hơn for nhiều — vì hầu hết bài toán đều có iterable phù hợp. while chủ yếu cho: chờ user input, đọc network/stream chưa biết độ dài, polling.

while khi không biết số lần lặp
# Đọc input đến khi nhập 'quit'
while True:
    cmd = input('> ')
    if cmd == 'quit':
        break
    process(cmd)

# Newton-Raphson — lặp đến khi hội tụ
def sqrt(n: float, eps: float=1e-10) -> float:
    x = n
    while abs(x*x - n) > eps:
        x = (x + n/x) / 2
    return x

# Walrus := (3.8+) cho while-loop với phép gán inline
while (line := f.readline()):
    print(line)
# Đọc đến khi readline() trả chuỗi rỗng (EOF) — falsy → dừng.

5. break, continue

Như mọi ngôn ngữ: break thoát loop ngay, continue bỏ qua phần còn lại của iteration hiện tại. Pattern hay dùng: early continue / early break để giảm nesting.

Early continue giảm nesting
# Anti-pattern — nested sâu
for user in users:
    if user.is_active:
        if user.age >= 18:
            if user.country == 'VN':
                send_email(user)

# Pythonic — early continue (flat is better than nested):
for user in users:
    if not user.is_active:
        continue
    if user.age < 18:
        continue
    if user.country != 'VN':
        continue
    send_email(user)

6. else clause của loop — đặc thù Python

Python cho phép gắn else vào forwhile. Quy tắc: else chạy khi loop kết thúc tự nhiên — tức là không bị break. Cú pháp này gây bối rối với người mới (lúc đầu tưởng "else chạy nếu loop không vào lần nào") nên Raymond Hettinger đề xuất tên hay hơn: nobreak. Tên đó rõ hơn nhiều.

else của for — "nobreak"
# Tìm số nguyên tố
def is_prime(n: int) -> bool:
    if n < 2:
        return False
    for i in range(2, int(n**0.5) + 1):
        if n % i == 0:
            return False
    return True

# Viết lại dùng for-else:
def find_divisor(n: int) -> int | None:
    for i in range(2, n):
        if n % i == 0:
            print(f'tìm thấy {i}')
            return i
    else:
        print('không có divisor (prime)')
        return None

# Quan sát hành vi:
for x in []:
    print(x)
else:
    print('done')
# In: 'done' — loop xong (không vào lần nào), không break.

for x in [1, 2, 3]:
    if x == 2:
        break
else:
    print('done')
# KHÔNG in gì — loop bị break.
📘 Dùng for-else khi nào?

Pattern điển hình: tìm phần tử trong iterable — nếu không tìm thấy thì làm gì đó. Hoặc kiểm tra điều kiện toàn iterable thỏa. Có thể tránh hoàn toàn bằng cách dùng any(), all(), hoặc return trong loop + xử lý sau loop. Nhưng khi đọc code Python, gặp else sau for/while phải nhớ nghĩa "nobreak".

7. Function với def

Định nghĩa function trong Python bằng def + tên + dấu ngoặc + parameters + dấu hai chấm. PEP 484 (3.5+) khuyến khích thêm type hints. Convention: function quan trọng có docstring đặt ngay sau def, bao bằng """...""".

def với type hints + docstring
def add(a: int, b: int) -> int:
    """Trả về tổng của hai số nguyên.

    Args:
        a: số hạng thứ nhất.
        b: số hạng thứ hai.

    Returns:
        Tổng a + b.

    Examples:
        >>> add(2, 3)
        5
    """
    return a + b

# Truy cập docstring qua thuộc tính __doc__
print(add.__doc__)
# help(add) cũng in ra docstring

Convention docstring phổ biến: Google style (như trên), NumPy style, reStructuredText. Chọn 1 và nhất quán toàn project. Tool sphinx, pdoc dựa vào docstring sinh tài liệu HTML tự động.

8. Default args và keyword args

Python cho phép đặt giá trị mặc định cho parameter. Khi gọi, có thể truyền theo vị trí (positional) hoặc theo tên (keyword).

Default args + keyword args
def greet(name: str, greeting: str = 'Hi') -> str:
    return f'{greeting}, {name}!'

greet('An')                  # 'Hi, An!'
greet('An', 'Hello')         # 'Hello, An!'
greet(name='An')             # 'Hi, An!' — keyword
greet(greeting='Hi', name='An') # 'Hi, An!' — keyword đảo thứ tự OK

8.1. Cái bẫy mutable default

Đã nhắc Chương 2, nhưng đây là bug tuần với người mới Python — đáng lặp lại. Default value được tính 1 lần tại thời điểm def, sau đó chia sẻ qua các lần gọi.

BUG — mutable default
def append_to(item, lst=[]):     # ⚠️ default = list rỗng dùng chung!
    lst.append(item)
    return lst

append_to(1)   # [1]
append_to(2)   # [1, 2]    ← tích lũy giữa các lần gọi!
append_to(3)   # [1, 2, 3]

# Sửa: dùng None làm sentinel, khởi tạo bên trong function.
def append_to(item, lst=None):
    if lst is None:
        lst = []
    lst.append(item)
    return lst

append_to(1)   # [1]
append_to(2)   # [2]    ← OK, mỗi lần gọi là list mới
⚠️ Khi nào "tận dụng" mutable default?

Có một use case (cũ): cache cấp function. Nhưng từ functools.lru_cache (đã có sẵn, Chương 7), không có lý do gì dùng mutable default cho cache nữa. Luôn dùng None cho default của list/dict/set.

9. *args**kwargs

Khi không biết trước có bao nhiêu argument, dùng *args để gom positional dư vào tuple, và **kwargs để gom keyword dư vào dict. Tên args/kwargs chỉ là convention — bạn có thể dùng tên khác, nhưng đừng.

*args và **kwargs
def log(*args, **kwargs):
    print('positional:', args)    # tuple
    print('keyword:', kwargs)     # dict

log(1, 2, 3, name='An', age=22)
# positional: (1, 2, 3)
# keyword: {'name': 'An', 'age': 22}

# Kết hợp với normal args:
def make_request(method, url, *headers, **params):
    ...
make_request('GET', '/api/users', 'X-Auth', 'X-Trace', page=1, limit=10)
# method='GET', url='/api/users'
# headers=('X-Auth', 'X-Trace')
# params={'page': 1, 'limit': 10}

9.1. Unpacking — chiều ngược lại

*** không chỉ dùng trong định nghĩa function. Trong call site, chúng unpack ngược lại — tách tuple/list ra positional, tách dict ra keyword.

Unpacking ở call site
def add(a, b, c):
    return a + b + c

nums = [1, 2, 3]
add(*nums)                  # 6 — unpack list thành 3 positional

opts = {'a': 1, 'b': 2, 'c': 3}
add(**opts)                 # 6 — unpack dict thành keyword

# Pattern phổ biến: forward args qua wrapper
def debug_call(fn, *args, **kwargs):
    print(f'calling {fn.__name__}({args}, {kwargs})')
    return fn(*args, **kwargs)

10. Positional-only / và keyword-only *

Python 3.8+ cho phép thiết kế API rõ ràng hơn bằng 2 ký hiệu đặc biệt trong signature:

  • / — mọi tham số trước nópositional-only: caller không được dùng tên.
  • * — mọi tham số sau nókeyword-only: caller bắt buộc gọi bằng tên.
/ và * trong signature
def f(pos_only, /, both, *, kw_only):
    ...

f(1, 2, kw_only=3)        # OK
f(1, both=2, kw_only=3)   # OK
f(pos_only=1, both=2, kw_only=3)
# TypeError: f() got some positional-only arguments passed as keyword
f(1, 2, 3)
# TypeError: f() takes 2 positional arguments but 3 were given

10.1. Tại sao thiết kế API như vậy?

Positional-only — bảo vệ refactor

Nếu caller đang gọi f(name='An'), bạn muốn đổi tên parameter từ name sang user_name sẽ break tất cả caller. Dùng / để không cho phép caller gắn với tên, bạn có thể đổi tên tự do về sau. Built-in nhiều function dùng pattern này: len(obj, /), str.format_map(mapping, /).

Keyword-only — chống "boolean argument hell"

Function với nhiều flag boolean dễ đọc khó: open('f.txt', True, False, None, True) — flag thứ 3 là gì? Bắt caller dùng keyword: open('f.txt', mode='r', buffering=False, encoding=None, errors='strict') tự document. Đặt * trước các flag để ép keyword-only.

API design pattern
def connect(host, port, /, *, timeout=30, retries=3):
    """host, port phải positional. timeout, retries phải keyword."""
    ...

connect('localhost', 5432, timeout=60)   # OK
connect('localhost', 5432, 60, 5)
# TypeError: connect() takes 2 positional arguments

11. Scope LEGB

Python tìm tên biến theo thứ tự LEGB:

CấpLà gìVí dụ
LocalBên trong function hiện tạibiến local của function
EnclosingFunction bao ngoài (nested)biến của outer function
GlobalModule level (file .py)biến top-level, hằng PI = 3.14
Built-inBuilt-in namespacelen, print, range

Python tìm theo thứ tự đó. Tìm thấy ở cấp nào dùng cấp đó.

LEGB lookup
x = 'global'             # G

def outer():
    x = 'enclosing'       # E (đối với inner)

    def inner():
        x = 'local'        # L
        print(x)
    inner()
    print(x)

outer()
# local
# enclosing
print(x)
# global

11.1. globalnonlocal

Bên trong function, gán biến tạo ra biến local mới (đè LEGB). Nếu muốn gán biến cấp ngoài, dùng global (cho G) hoặc nonlocal (cho E — enclosing).

global vs nonlocal
count = 0

def increment_local():
    count = count + 1      # UnboundLocalError
    # count xuất hiện ở vế trái → Python coi là local; nhưng vế phải đọc count chưa khởi tạo.

def increment_global():
    global count
    count += 1
increment_global()
print(count)                # 1

def outer():
    x = 10
    def inner():
        nonlocal x             # bind biến của outer, KHÔNG phải global
        x += 1
    inner()
    print(x)                # 11
⚠️ Hạn chế dùng global

global thường là code smell. Function thay đổi global biến → khó test, khó reasoning, race condition trong multithread. Thay vào đó: pass argument vào, return value ra. Dùng class nếu cần state. nonlocal chấp nhận hơn vì state vẫn nằm trong factory function (closure pattern).

12. Closure

Khi một function nested được trả về (hoặc lưu lại) và nó tham chiếu biến của function bao ngoài, ta có closure. Inner function "đóng gói" (close over) các biến đó — chúng sống tiếp cùng inner function, kể cả khi outer đã return.

Counter qua closure
def make_counter():
    count = 0
    def inc():
        nonlocal count        # cần để có thể GÁN lại count (binding outer)
        count += 1
        return count
    return inc

c1 = make_counter()
c1()                       # 1
c1()                       # 2
c1()                       # 3

c2 = make_counter()        # instance độc lập
c2()                       # 1, không chia sẻ với c1

Closure xem được biến nó capture qua thuộc tính __closure__. Mỗi cell trong tuple là một biến.

Khám phá __closure__
c1.__closure__
# (<cell at 0x...: int object at 0x...>,)
c1.__closure__[0].cell_contents
# 3 — giá trị count hiện tại

12.1. Closure capture binding, không phải value

Một bẫy quen thuộc — closure trong loop capture binding chứ không phải giá trị tại thời điểm tạo. Cách Python xử lý khác JavaScript một chút.

Closure trong loop — pitfall
funcs = []
for i in range(3):
    funcs.append(lambda: i)

[f() for f in funcs]
# [2, 2, 2]   ← tất cả thấy `i` cuối loop

# Fix 1: capture qua default argument (eager)
funcs = []
for i in range(3):
    funcs.append(lambda i=i: i)

[f() for f in funcs]
# [0, 1, 2]

# Fix 2: dùng factory function (mỗi iteration tạo binding mới)
def make(x):
    return lambda: x
funcs = [make(i) for i in range(3)]
[f() for f in funcs]
# [0, 1, 2]

13. lambda — Anonymous function

lambda là cú pháp viết function ngắn gọn, không có tên. Hạn chế cứng: chỉ chứa 1 biểu thức — không có statement, không có return (tự return giá trị biểu thức), không gán biến (=), không if/for dạng statement.

Lambda
# lambda params: expression
square = lambda x: x * x
square(5)                   # 25

# Tương đương:
def square(x):
    return x * x

# Có thể có if/else expression (conditional expression):
abs_val = lambda x: -x if x < 0 else x

# Use case: callback ngắn cho sorted/filter/map
users = [{'name': 'An', 'age': 22}, {'name': 'Bình', 'age': 19}]
sorted(users, key=lambda u: u['age'])
# [{'name': 'Bình', 'age': 19}, {'name': 'An', 'age': 22}]

list(filter(lambda x: x % 2 == 0, range(10)))
# [0, 2, 4, 6, 8]
📘 Khi nào dùng lambda?

Chỉ khi function cực ngắn, dùng 1 lần, ngay tại chỗ. Nếu cần đặt tên cho lambda (f = lambda x: ...), gần như luôn nên dùng def — có tên rõ ràng, có thể thêm docstring, type hints. PEP 8 thậm chí khuyến nghị KHÔNG gán lambda vào biến:

# PEP 8 khuyến nghị TRÁNH:
add = lambda a, b: a + b
# Dùng def:
def add(a, b):
    return a + b

13.1. operator module — thay lambda đơn giản

Cho các lambda dạng lambda x: x.attr, lambda x: x[k], stdlib có sẵn operator.attrgetter, operator.itemgetter — nhanh hơn và rõ nghĩa hơn.

operator module
from operator import itemgetter, attrgetter

# Thay vì:
sorted(users, key=lambda u: u['age'])
# Dùng:
sorted(users, key=itemgetter('age'))

# itemgetter còn lấy nhiều field (trả tuple — sort theo nhiều khóa)
sorted(users, key=itemgetter('age', 'name'))

14. Pass-by-object-reference

Câu hỏi cổ điển: Python pass-by-value hay pass-by-reference? — Cả hai đều sai. Python dùng pass-by-object-reference (hay "call by sharing"). Khi gọi function, parameter nhận tham chiếu đến cùng object với argument. Hệ quả phụ thuộc mutable hay immutable (đã xem Chương 2).

Mutate vs reassign
def mutate(lst):
    lst.append(99)             # mutate IN-PLACE → caller thấy thay đổi

a = [1, 2, 3]
mutate(a)
print(a)                    # [1, 2, 3, 99]

def reassign(lst):
    lst = [9, 9, 9]            # gán lại biến LOCAL → caller KHÔNG thấy

b = [1, 2, 3]
reassign(b)
print(b)                    # [1, 2, 3]

def try_modify_int(n):
    n += 1                  # int immutable → tạo object mới, gán biến local

x = 10
try_modify_int(x)
print(x)                    # 10 — không đổi

Mô hình mental đúng

Hãy nghĩ: tất cả biến Python đều là label dán lên object. Truyền argument = dán thêm label cùng object. append sửa object (tất cả label cùng thấy). = gỡ label dán sang object khác (chỉ ảnh hưởng local label, không ảnh hưởng caller).

Bài tập

Bài 1 — FizzBuzz với match

Viết hàm fizzbuzz(n: int) -> list[str] trả về list FizzBuzz từ 1 đến n. Quy tắc: bội của 3 → 'Fizz'; bội của 5 → 'Buzz'; bội của cả 3 và 5 → 'FizzBuzz'; còn lại → chuỗi của số đó.

Yêu cầu: dùng match/case thay vì chuỗi if/elif.

Đáp án
def fizzbuzz(n: int) -> list[str]:
    result = []
    for i in range(1, n + 1):
        match (i % 3, i % 5):
            case (0, 0):
                result.append('FizzBuzz')
            case (0, _):
                result.append('Fizz')
            case (_, 0):
                result.append('Buzz')
            case _:
                result.append(str(i))
    return result

fizzbuzz(15)
# ['1', '2', 'Fizz', '4', 'Buzz', 'Fizz', '7', '8', 'Fizz',
#  'Buzz', '11', 'Fizz', '13', '14', 'FizzBuzz']

Mẹo: pattern (0, 0) match trước (0, _)match đánh giá theo thứ tự case — cụ thể trước, tổng quát sau.

Bài 2 — Decorator trace (skeleton cho Chương 7)

Viết trace(fn) trả về một function bao quanh fn: trước khi gọi, in '→ tên(args, kwargs)'; sau khi gọi, in '← tên = result'. Áp dụng được lên function bất kỳ thông qua cú pháp @trace.

@trace
def add(a, b):
    return a + b

add(2, 3)
# → add((2, 3), {})
# ← add = 5
Đáp án
def trace(fn):
    def wrapper(*args, **kwargs):
        print(f'→ {fn.__name__}({args}, {kwargs})')
        result = fn(*args, **kwargs)
        print(f'← {fn.__name__} = {result}')
        return result
    return wrapper

@trace
def add(a, b):
    return a + b

# `@trace` tương đương: add = trace(add)

Đây mới là skeleton. Decorator "thực chiến" cần functools.wraps(fn) để giữ metadata (__name__, __doc__) của fn. Chương 7 sẽ đi sâu — bao gồm decorator với tham số, lớp decorator, functools.lru_cache.

Bài 3 — parse_command dùng match

Viết parse_command(parts: list[str]) -> str mô phỏng REPL nhỏ. Quy tắc:

  • ['add', x, y]f'thêm {x} = {y}'
  • ['del', id]f'xóa {id}'
  • ['list']'liệt kê'
  • ['quit'] hoặc ['exit']'tạm biệt'
  • Khác → 'lệnh không hợp lệ'
Đáp án
def parse_command(parts: list[str]) -> str:
    match parts:
        case ['add', x, y]:
            return f'thêm {x} = {y}'
        case ['del', item_id]:
            return f'xóa {item_id}'
        case ['list']:
            return 'liệt kê'
        case ['quit'] | ['exit']:
            return 'tạm biệt'
        case _:
            return 'lệnh không hợp lệ'

parse_command(['add', 'apple', '10'])  # 'thêm apple = 10'
parse_command(['del', '42'])             # 'xóa 42'
parse_command(['list'])                  # 'liệt kê'
parse_command(['quit'])                  # 'tạm biệt'
parse_command(['xyz'])                   # 'lệnh không hợp lệ'

Lưu ý: trong case ['add', x, y], xybiến mới được bind. Nếu muốn match giá trị hằng số, dùng dot-name (case ['add', VALUE] bắt buộc VALUE phải là tên có dot, ví dụ Cmd.ADD).

Bài 4 — apply(funcs, value) — function composition

Viết apply(funcs: list, value) áp dụng tuần tự danh sách function lên value: kết quả của function i được dùng làm input cho function i+1. Trả về kết quả cuối.

apply([str.upper, len], 'hello')    # len('HELLO') = 5
apply([str, len], 12345)            # len('12345') = 5
apply([], 42)                       # 42 (không function nào → trả nguyên)
Đáp án
def apply(funcs: list, value):
    for fn in funcs:
        value = fn(value)
    return value

# Hoặc dùng functools.reduce:
from functools import reduce
def apply(funcs, value):
    return reduce(lambda v, fn: fn(v), funcs, value)

Pattern này là nền tảng của pipeline trong functional programming. Không có function composition operator (| như Elixir hay . như Haskell) trong Python core — nhưng dễ tự viết.

Bài 5 — make_accumulator() qua closure

Viết make_accumulator() trả về một function. Mỗi lần gọi function đó với value, cộng dồn vào tổng nội tại và trả về tổng hiện tại.

acc = make_accumulator()
acc(5)     # 5
acc(10)    # 15
acc(7)     # 22

acc2 = make_accumulator()
acc2(1)    # 1 — instance độc lập, không share với acc
Đáp án
def make_accumulator():
    total = 0
    def add(value):
        nonlocal total
        total += value
        return total
    return add

Tại sao cần nonlocal total? Vì total += value tương đương total = total + value — Python sẽ coi total là local của add, dẫn đến UnboundLocalError. nonlocal bảo: hãy bind total ở scope enclosing (outer function).

Nếu thay bằng container mutable, không cần nonlocal:

def make_accumulator():
    total = [0]                   # list — mutable
    def add(value):
        total[0] += value         # mutate, không reassign biến
        return total[0]
    return add

Cách này hoạt động cả ở Python 2 (không có nonlocal) nhưng kém Pythonic. Ở Python 3 thuần, dùng nonlocal.

Quiz

Q1

Code sau in ra cái gì?

for x in []:
    print(x)
else:
    print('done')
Xem đáp án
✓ Đáp án

In done. else sau for/while chạy khi loop kết thúc tự nhiên — tức là không bị break. Ở đây iterable rỗng, loop kết thúc ngay mà không có break, nên else chạy.

Đây là điểm gây bối rối cho người mới — nhiều người tưởng else nghĩa là "khi loop không vào lần nào". Tên đề xuất hay hơn của Raymond Hettinger: nobreak.

Q2

Cho function dưới, gọi f() ba lần liên tiếp trả về gì?

def f(x=[]):
    x.append(1)
    return x
Xem đáp án
✓ Đáp án

[1], [1, 1], [1, 1, 1]. Default x=[] được tính 1 lần khi def được thực thi — sau đó chia sẻ cho mọi lần gọi không truyền x. Mỗi append mutate vào cùng list.

Fix: dùng None làm sentinel, khởi tạo list bên trong:

def f(x=None):
    if x is None:
        x = []
    x.append(1)
    return x
Q3

Với def f(*, a, b): ..., gọi f(1, 2) có hoạt động không?

Xem đáp án
✓ Đáp án

Không. Dấu * đứng đầu ép ab phải truyền theo keyword. Gọi f(1, 2) raise:

TypeError: f() takes 0 positional arguments but 2 were given

Phải gọi f(a=1, b=2). Đây là pattern thiết kế API "keyword-only" — bắt caller dùng tên rõ ràng, tránh truyền sai thứ tự.

Q4

Khai báo globalnonlocal bên trong function khác nhau ở điểm gì?

Xem đáp án
✓ Đáp án

Cả hai cho phép gán (bind) biến ở scope ngoài bên trong function, nhưng target khác:

  • global x — bind biến ở scope G (module-level). Tạo biến mới ở module nếu chưa tồn tại.
  • nonlocal x — bind biến ở scope E (enclosing), tức outer function gần nhất. Không tạo mới — nếu không có ở enclosing, raise SyntaxError.

Hệ quả: nonlocal chỉ có nghĩa trong nested function. Ở top-level function thuần, muốn sửa global thì phải dùng global.

Q5

Lambda có chứa statement được không (ví dụ if, for, return)?

Xem đáp án
✓ Đáp án

Không. Lambda chỉ chứa 1 biểu thức (expression). Không có return (giá trị biểu thức tự được trả), không có if/for/while dạng statement, không gán biến (=).

Nhưng conditional expression (a if cond else b) là expression — dùng được:

abs_val = lambda x: -x if x < 0 else x

Tương tự, list/dict/generator comprehension là expression — dùng được trong lambda. Khi cần logic phức tạp hơn, chuyển sang def.

Q6

Trong match/case, có thể bind biến ngay trong pattern không?

Xem đáp án
✓ Đáp án

— đây chính là điểm mạnh của structural pattern matching so với switch truyền thống.

match point:
    case Point(x=0, y=y_val):       # bind y > biến y_val
        print(f'trục Y, y={y_val}')
    case [first, *rest]:           # bind first và rest
        ...

Cảnh báo: trong pattern, tên đơn (như y_val) luôn được hiểu là biến mới bind, không phải so sánh giá trị. Muốn so sánh giá trị hằng, dùng dot-name (Status.OK) hoặc literal trực tiếp (0, 'OK').

Q7

Cho def f(a, b, /, c, d, *, e): .... Hai cách gọi sau có hợp lệ không?

  1. f(1, 2, 3, 4, e=5)
  2. f(1, b=2, c=3, d=4, e=5)
Xem đáp án
✓ Đáp án
  1. f(1, 2, 3, 4, e=5)hợp lệ. a=1, b=2 (positional, OK vì là positional-only); c=3, d=4 (positional, được phép vì nằm giữa /*); e=5 (keyword, bắt buộc vì sau *).
  2. f(1, b=2, c=3, d=4, e=5)không hợp lệ. b nằm trước / nên là positional-only — không được truyền bằng tên. Raise: TypeError: f() got some positional-only arguments passed as keyword arguments: 'b'.

Tóm tắt rule: tham số trước / chỉ positional. Tham số giữa /* được cả positional và keyword. Tham số sau * chỉ keyword.

Tổng kết

Sau Chương 4, bạn nên đã master:

  • if/elif/else: indent 4 space, không có ternary ?: mà có conditional expression a if cond else b.
  • match/case (3.10+): structural pattern matching — sequence, mapping, class, guard, OR pattern, bind biến.
  • for x in iterable: enumerate cho index, zip để duyệt song song, zip(..., strict=True) kiểm tra độ dài bằng nhau.
  • while: dùng khi không biết trước số lần lặp (input, polling, hội tụ).
  • break/continue: early continue giảm nesting (Flat is better than nested).
  • else clause của loop: chạy khi không break — "nobreak".
  • Function với def: type hints + docstring + return là convention chuyên nghiệp.
  • Default args + cái bẫy mutable default: luôn dùng None làm sentinel.
  • *args/**kwargs: gom positional/keyword dư + unpack ngược ở call site.
  • /*: positional-only / keyword-only cho API design.
  • LEGB scope: Local → Enclosing → Global → Built-in; global vs nonlocal.
  • Closure: function nhớ scope tạo ra nó; counter, accumulator, factory pattern.
  • Lambda: 1 expression, dùng đúng chỗ (sorted key, filter, map); không gán vào biến.
  • Pass-by-object-reference: mutate ảnh hưởng caller, reassign không.

Kết nối

  • Chương 3 (Data Structures) — củng cố tuple/list/dict/set để hiểu sequence/mapping pattern trong match.
  • Chương 5 (OOP in Python) — method chính là function gắn với object; self chỉ là quy ước, không phải keyword; @property dùng decorator pattern.
  • Chương 6 (Iterators, Generators & Itertools) — generator function với yield là biến thể đặc biệt của function; tận dụng triết lý "Pythonic for loop" của chương này.
  • Chương 7 (Decorators & Context Managers) — đi sâu decorator (đã thấy skeleton ở Bài tập 2); functools.wraps, functools.lru_cache, decorator có tham số.
  • JavaScript Chương 3 (Functions, Scope & Closures) — so sánh: JS có 4 cách viết function và this 4 rule phức tạp; Python đơn giản hơn (1 cách def, không có this) nhưng có match/case mạnh hơn JS switch.