Học bằng cách đọc theo thứ tự

itertools và functools — Bộ công cụ lặp và kết hợp hàm

Học các công thức lặp combinations/product/chain/accumulate, cố định đối số với partial, và memoize đệ quy với @lru_cache để tăng tốc, qua ví dụ thật.

itertoolstập các hàm xây iterable mới từ iterable đã có (kết hợp, nối, tích lũy), còn functoolstập các hàm biến đổi chính bản thân hàm (gắn đối số, memoize). Cả hai đều có thể thay thế pattern for loop + tự theo dõi state trong một dòng, làm code ngắn đi rất nhiều.

Iterable — giá trị bạn có thể lặp qua

Hầu hết hàm itertools nhận "iterable" làm input và trả về iterable mới, nên đáng để xác định iterable là gì trước. Iterable (thứ bạn có thể viết bên phải for x in ___:) là bất cứ gì bạn có thể truyền cho vòng for, list(...), sum(...), hoặc map(...). list / tuple / str / dict / set / range, cộng với generator bạn tự viết (hàm dùng yield), tất cả đều hoạt động như iterable trong Python.

Các iterable phổ biến
list[1, 2, 3]tuple(1, 2, 3)str"abc"dict{"a": 1}set{1, 2, 3}rangerange(5)
Thứ bạn có thể đặt sau for x in là iterable. Các kiểu built-in như list / tuple / str / dict / set / range đều là iterable, và generator tự xây với yield cũng vậy.

itertools — bộ công cụ kết hợp và lặp

itertoolsmodule tập hợp các hàm xây iterable mới từ iterable đã có. Bốn hàm bạn dùng nhiều nhất là combinations (kết hợp không thứ tự), product (tích Descartes qua nhiều tập), chain (nối các iterable thành một), và accumulate (tổng cộng dồn hoặc tích cộng dồn). Biết bốn hàm này cho phép bạn viết lại vòng for lồng nhauvòng lặp tự theo dõi state gọn hơn nhiều.

Bốn hàm cốt lõi của itertools
combinationsKết hợp không thứ tựproductTất cả cặp giữa tậpchainNối các iterableaccumulateTổng / tích cộng dồn
combinations cho cặp / bộ ba không thứ tự, product cho tất cả cặp giữa các tập, chain để nối các iterable, và accumulate để giữ state cộng dồn khi quét. Cả bốn trả về iterator, nên hãy bọc với list(...) để vật chất hóa.
HàmInput → OutputVí dụ
combinations(it, k)Kết hợp k phần tử (không thứ tự)[A,B,C] → (A,B), (A,C), (B,C)
permutations(it, k)Hoán vị k phần tử (có thứ tự)[A,B] → (A,B), (B,A)
product(*its)Tích Descartes qua các tập[S,M] × [red,blue] → 4 cặp
chain(*its)Nối nhiều iterable[1,2] + [3,4] → [1,2,3,4]
accumulate(it)Cộng dồn từ trái (mặc định là sum)[10,20,30] → [10, 30, 60]

Liệt kê các cặp hai đồ uống chọn từ ba lựa chọntất cả SKU size × màu. combinations cho cặp không thứ tự, product cho tích Descartes giữa các tập.

① Hãy import combinationsproduct từ itertools

② Từ list đồ uống ["coffee", "tea", "juice"], hãy xây các cặp 2 với combinations, vật chất hóa, và in dưới dạng Cặp: ◯

③ Hãy xây toàn bộ tổ hợp size ["S", "M", "L"] × màu ["red", "blue"] với product, vật chất hóa, và in dưới dạng Tất cả SKU: ◯

(Nếu code chạy đúng, phần giải thích sẽ hiện ra.)

Python Editor

Chạy code để xem đầu ra

Nối hai ngày dữ liệu doanh số và tính tổng cộng dồn. chain ghép nhiều iterable thành mộtaccumulate quét từ trái, mang theo giá trị cộng dồn.

① Hãy import chainaccumulate từ itertools

② Hãy nối doanh số ngày 1 [120, 80] và doanh số ngày 2 [200, 150, 90] với chain, vật chất hóa, và in dưới dạng Tất cả doanh số: ◯

③ Hãy áp dụng accumulate cho kết quả nối, vật chất hóa, và in dưới dạng Tổng cộng dồn: ◯

Python Editor

Chạy code để xem đầu ra

functools.partial — gắn đối số trước

functools.partial xây một hàm mới với một số đối số đã được khóa sẵn. Khi bạn cần truyền hàm với đối số đã gắn cho callback hoặc hàm bậc cao, partial giúp bạn khỏi viết các hàm wrapper một dòng kiểu def wrapper(...): .... Bất cứ chỗ nào bạn gọi cùng một hàm lặp đi lặp lại với biến thể nhỏ trong đối số, partial cắt bỏ tiếng ồn wrapper và làm code dễ đọc hơn.

from functools import partial

def format_with_unit(price, unit):
    return f"{price}{unit}"

# Xây hàm mới với đơn vị "USD" được gắn sẵn
to_usd = partial(format_with_unit, unit="USD")

# Áp dụng cho từng giá — chỉ cần truyền price
print(to_usd(100))   # 100USD
print(to_usd(200))   # 200USD
print(to_usd(300))   # 300USD
partial hoạt động thế nào
power(base, exp)Trả về base ** exppartial(power, exp=2)Khóa exp=2square(3) → 9square(5) → 25
partial(function, arg=value) trả về hàm mới với một số đối số đã khóa. Khóa exp=2 trên power(base, exp) và bạn được hàm square chỉ cần base.

Dùng partial để khóa exp trên power(base, exp) và rút ra hàm square / cube chuyên dụng.

① Hãy import partial từ functools

② Hãy định nghĩa def power(base, exp): trả về `base ** exp

③ Với partial, hãy xây hàm `square` với `exp=2` được khóa. Gọi square(3) và square(5) rồi in dưới dạng 3 bình phương: ◯ / 5 bình phương: ◯

④ Với partial, hãy xây hàm `cube` với `exp=3` được khóa. Gọi cube(2) rồi in dưới dạng 2 lập phương: ◯

Python Editor

Chạy code để xem đầu ra

functools.lru_cache — cache kết quả với memoize

"Tôi muốn tái sử dụng kết quả của một hàm tốn kém được gọi lặp với cùng đối số" — xuất hiện bất cứ khi nào output của hàm được xác định bởi input và tốc độ là quan trọng. Tự xây nghĩa là tự quản dict cache của riêng bạn, nhưng @lru_cache làm điều đó trong một dòng decorator.

@lru_cachedecorator cache giá trị trả về của hàm. Khi gọi lại với cùng đối số, nó trả về giá trị cache trực tiếp, bỏ qua tính toán lại. LRU (Least Recently Used — bỏ các entry không dùng lâu nhất) bỏ entry cache cũ nên bạn có thể giới hạn dùng bộ nhớ với cái gì đó như maxsize=128.

lru_cache hoạt động thế nào
fib(30) lần đầuChạy thân hàmCache kết quả+ trả giá trịfib(30) lần haiĐọc từ cache→ Trả ngay lập tức
Lần gọi đầu miss cache, nên thân hàm chạy và kết quả được lưu dưới dạng (args → giá trị trả về). Các lần gọi sau với cùng args trả về ngay từ cache — thân hàm không bao giờ chạy.

Đừng thêm vào hàm có side effect

@lru_cache giả định "cùng đối số → cùng giá trị trả về". Áp dụng nó cho hàm có side effect (đọc file / ghi DB / trả về thời gian hiện tại) hoặc kết quả thay đổi cho cùng input, và bạn sẽ có bug kết quả đầu tiên cứ trả về mãi mãi. Chỉ áp dụng cho hàm thuần (cùng input → cùng output, không side effect).

Tăng tốc Fibonacci đệ quy với @lru_cache. Ngay cả ở fib(30), số lần gọi đã rất lớn nếu không cache, nhưng với decorator nó chạy ngay.

① Hãy import lru_cache từ functools

② Hãy định nghĩa hàm fib(n) được decorate với @lru_cache(maxsize=128) (trả về n nếu n < 2, ngược lại trả về fib(n-1) + fib(n-2))

③ Hãy in fib(30) dưới dạng fib(30): ◯

④ Hãy in fib(50) dưới dạng fib(50): ◯ (không cache thì sẽ không hoàn thành trong thời gian hợp lý)

⑤ Hãy in maxsize: ◯ dùng fib.cache_info().maxsize

Python Editor

Chạy code để xem đầu ra
QUIZ

Kiểm tra kiến thức

Hãy trả lời từng câu hỏi một.

Câu 1Cách ngắn nhất để xây tích Descartes của [1, 2, 3][A, B] là gì?

Câu 2partial(f, x=10) trả về cái gì?

Câu 3Tại sao @lru_cache tăng tốc Fibonacci đệ quy ngây thơ?

Câu 4Khi gọi accumulate([10, 20, 30]), bạn nhận được gì?