Hàm reduce() của Python xuất phát từ thế giới lập trình hàm. Lập trình hàm (FP) là một mô hình lập trình trong đó chương trình tạo ra kết quả bằng cách áp dụng các hàm lên dữ liệu bất biến.
Một mẫu phổ biến trong phong cách này là "gấp" (fold), tức thu gọn một dãy về một kết quả duy nhất. Ví dụ, gấp danh sách số [2, 4, 5, 3] theo phép cộng để ra 14 qua các bước kế tiếp: [2, 4, 5, 3] → [6, 5, 3] → [11, 3] → 14.
reduce() khái quát hóa ý tưởng này. Nó áp dụng một phép toán nhị nguyên lên một iterable cho đến khi chỉ còn lại kết quả.
Trong bài viết này, tôi sẽ khám phá các thành phần chính của reduce() trong Python và đưa ra một số ví dụ thực tế. Nếu bạn cần ôn lại các kiến thức cơ bản về Python, tôi khuyến nghị xem qua các tài nguyên sau:
Python bao gồm các hàm lập trình hàm khác trong Python như map() và filter().
Các cấu trúc chức năng như map() và filter() đã có từ Python 1.0. Guido van Rossum không thích chúng, chỉ ra rằng reduce() khó đọc, và vòng lặp for hầu như luôn dễ hiểu hơn. Trong Python 3.0, theo PEP 3100, các nhà phát triển đã loại reduce() khỏi nhóm built-in và chuyển nó sang mô-đun functools. Việc chuyển sang functools về bản chất đã giáng nó xuống thành một công cụ ngách.
Vì sao dùng reduce()?
Phần lớn thời gian, tôi thấy sử dụng một hàm dựng sẵn hoặc vòng lặp có lẽ là lựa chọn tốt hơn. Tuy nhiên, reduce() vẫn là lựa chọn tốt trong một số trường hợp.
Pipeline hàm. Xâu chuỗi một loạt (có thể động) các biến đổi theo cách gọn gàng.
Gấp đại số. Dùng cho các phép toán có phần tử đồng nhất tự nhiên, như hợp tập hợp với tập rỗng hoặc thao tác bitmask với số không.
Gấp tùy chỉnh khi không có hàm dựng sẵn. Định nghĩa cách trộn riêng theo miền bài toán khi không có sẵn hàm dựng sẵn.
Bộ tích lũy có cấu trúc. Theo dõi đồng thời nhiều trạng thái bên trong một hàm tích lũy tùy chỉnh.
Cách hoạt động của reduce() trong Python
Hãy khám phá cơ chế của reduce().
Chữ ký hàm cơ bản của reduce() là
functools.reduce(function, iterable,[initializer])Được hỗ trợ bởi
Hàm reduce() nhận hai đối số bắt buộc và một đối số tùy chọn thứ ba.
function: một hàm nhị nguyên chỉ định cách kết hợp hai phần tử,
iterable: dãy hoặc iterable cần gấp, như list hoặc tuple, initializer (tùy chọn): giá trị khởi đầu để gieo hàm.
Ví dụ từng bước:
[1, 3, 2, 7] → [4, 2, 7].
[4, 2, 7] → [6, 7].
[6, 7] → [13].
Kết quả cuối cùng là 13.
Ví dụ reduce() đơn giản
Các ví dụ nhỏ sau minh họa cơ chế cách dùng reduce().
from functools importreduce
numbers =[2,4,6]
product =reduce(lambda x, y: x * y, numbers)# ((2 x 4) x 6) = 8 x 6 = 48
min_value =reduce(lambda x, y: x if x < y else y, numbers)# 2
words =['dog','cat','tree','pony']
str_concat =reduce(lambda x, y: x + y, words)# "dogcattreepony"Được hỗ trợ bởi
Giá trị khởi tạo (Initializer)
Không có initializer, reduce() lấy phần tử đầu tiên của iterable làm giá trị bắt đầu. Nếu iterable rỗng, reduce() ném ra TypeError. Để mã bền vững, hãy cung cấp initializer để định nghĩa hành vi với đầu vào rỗng.
from functools importreduce
words =[]# Error: reduce() of empty iterable with no initial value
str_concat =reduce(lambda x, y: x + y, words)# Correct: use empty string initializer
str_concat =reduce(lambda x, y: x + y, words,"")Được hỗ trợ bởi
Initializer cũng gieo kết quả bằng một container rỗng. Chẳng hạn, bạn có thể nối các từ thành một danh sách phẳng các ký tự.
from functools importreduce
words =['reduce','is','fun']
chars_list =reduce(lambda acc, word: acc +list(word), words,[])print(chars_list)# ['r', 'e', 'd', 'u', 'c', 'e', 'i', 's', 'f', 'u', 'n']Được hỗ trợ bởi
Để khám phá thêm Python và thao tác dữ liệu, dưới đây là một số lựa chọn tôi khuyến nghị.
Đến giờ, chúng ta dùng các hàm lambda để định nghĩa toán tử nhị nguyên.
Bạn cũng có thể dùng các toán tử từ mô-đun operator. Mô-đun operatorchứa các phiên bản hàm của những toán tử và lời gọi phương thức thông dụng. Ví dụ, thay vì x + y, dùng operator.add(x, y). Điều này cho phép bạn truyền các toán tử dựng sẵn (và hiệu quả) vào reduce() mà không cần viết lambda.
from functools importreduceimport operator as op
numbers =[2,4,6]
total =reduce(op.add, numbers,0)# instead of reduce(lambda x,y: x + y, numbers)Được hỗ trợ bởi
Lựa chọn thứ ba là viết một hàm tùy chỉnh. Đây là lựa chọn tốt khi không có toán tử dựng sẵn hoặc hàm quá phức tạp đối với lambda.
Ví dụ, giả sử bạn muốn loại bỏ trùng lặp khỏi một danh sách nhưng giữ nguyên thứ tự xuất hiện đầu tiên. Bạn có thể định nghĩa một reducer chỉ thêm một phần tử vào danh sách nếu nó chưa xuất hiện trước đó.
from functools importreduce
items =['the','wild','wild','world','is','the','wide','world','is','the','world']defdedup(acc, x):if x notin acc:# O(n) membership test
acc.append(x)return acc
unique =reduce(dedup, items,[])# ['the', 'wild', 'world', 'is', 'wide']Được hỗ trợ bởi
Để có thêm ý tưởng về loại bỏ trùng lặp, hãy tham khảo hướng dẫn này.
Python reduce() gọi hàm của chúng ta một lần cho mỗi phần tử. Với một danh sách có một triệu phần tử, tức là một triệu lời gọi hàm, mỗi lần tạo một frame, xử lý đối số và cập nhật đếm tham chiếu. Điều này gây thêm chi phí đáng kể.
Ngược lại, một hàm dựng sẵn như sum chỉ thực hiện một lời gọi vào hàm C và thực hiện hàng triệu phép cộng trong vòng lặp C. Sự khác biệt này có thể khiến hàm dựng sẵn nhanh hơn hàng bậc độ lớn.
Vấn đề về tính cục bộ bộ nhớ đệm và hiệu quả CPU
Reduce cũng chịu ảnh hưởng bởi tính cục bộ bộ nhớ đệm và hiệu quả CPU.
Một CPU hiện đại có thể thực thi vài tỷ lệnh mỗi giây, nhưng truy cập RAM chậm hơn hàng bậc độ lớn. Để bù đắp, CPU hiện đại có bộ nhớ đệm (L1, L2, L3) lưu trữ dữ liệu để truy cập nhanh.
Các bộ nhớ đệm này tận dụng hai kiểu cục bộ.
Cục bộ theo thời gian: dữ liệu vừa dùng nhiều khả năng sẽ được dùng lại.
Cục bộ theo không gian: dữ liệu gần một địa chỉ vừa dùng có khả năng sớm được dùng.
Trái lại, mỗi bước của reduce() liên quan đến việc lần theo con trỏ để tìm phần tử tiếp theo và các lời gọi hàm Python, làm phá vỡ tính cục bộ và khiến CPU bị chững. Các hàm dựng sẵn và hàm vector hóa tránh được vấn đề này bằng cách chạy trong các vòng lặp C chặt chẽ.
Để ôn lại cách viết mã Python đúng chuẩn và hiệu quả, hãy xem:
Mã C tối ưu. Các hàm dựng sẵn chạy vòng lặp trong mã C tối ưu, không phải Python. Điều đó tránh chi phí mà reduce() phải gánh. Lợi thế tốc độ này tăng lên khi đầu vào lớn.
Dễ đọc. Các hàm dựng sẵn có tên mô tả (sum, min) nên ý đồ rõ ràng. Một lời gọi reduce() khiến bạn phải phân tích hàm đang được gấp.
Vòng lặp:
Hiệu năng. Vòng lặp thường chạy chậm hơn hàm dựng sẵn nhưng nhanh hơn reduce().
Dễ đọc. Tương tự hàm dựng sẵn, vòng lặp thường dễ đọc hơn reduce(). Gọi reduce() buộc bạn diễn giải một biểu thức chức năng, trong khi vòng lặp mang tính Pythonic hơn.
itertools.accumulate()
Thư viện itertools của Python cung cấp một tập hợp iterator hiệu năng như count(), product() và combinations(). Một hàm itertools hữu ích là itertools.accumulate(). Tương tự reduce(), nó gấp một hàm lên một iterable. Tuy nhiên, accumulate() lưu lại các giá trị trung gian của phép tính, không chỉ kết quả cuối.
Ví dụ,
import itertools, operator
from functools importreducelist(itertools.accumulate([1,2,3,4], initial=0))# [0, 1, 3, 6, 10]reduce(operator.add,[1,2,3,4],0)# 10Được hỗ trợ bởi
Accumulate hữu ích khi bạn cần tổng chạy hoặc giá trị nhỏ nhất/lớn nhất chạy. Chẳng hạn, bạn có thể muốn biết tháng có nhiệt độ tối đa theo từng tháng.
Những lỗi thường gặp khi dùng reduce()
Khi bạn dùng reduce(), hãy lưu ý các lỗi sau.
Ưu tiên các lựa chọn đơn giản hơn như hàm dựng sẵn hoặc vòng lặp. Dành reduce() cho những lúc thật sự cần.
Xử lý iterable rỗng. Luôn dùng initializer phù hợp để tránh lỗi với đầu vào rỗng.
Coi chừng vấn đề bộ nhớ. Đừng cố ép reduce() vào các tình huống mà cách tiếp cận generator hoặc streaming sẽ hiệu quả hơn.
Tránh các lambda rắc rối. Dùng các hàm từ mô-đun operator khi có thể. Lambda, đặc biệt với các phép không kết hợp, có thể làm giảm độ rõ ràng.
Ưu tiên rõ ràng hơn khéo léo.
Thực tiễn tốt và hướng dẫn cho Python reduce()
Như bất kỳ công cụ nào, reduce() cũng có các thực tiễn tốt. Nếu bạn đã quyết định reduce() là công cụ phù hợp, dưới đây là một số hướng dẫn khi sử dụng.
Thiết kế reducer trước
Xác định hợp đồng trong một câu. "Kết hợp các khóa dictionary bằng cách cộng dồn số đếm theo khóa."
Giữ tính kết hợp nếu có thể. Điều này cho phép bạn dễ dàng song song hóa và kiểm thử.
Xác định phần tử đồng nhất và dùng nó làm initializer. Ví dụ, với tổng, phần tử đồng nhất là 0. Với min là math.inf, và với phép hợp set là set(). Điều này giúp mã bền vững và tránh TypeErrors.
Giữ đơn giản và dễ đọc
Một thao tác rõ ràng. Không có tác dụng phụ.
Đặt tên mô tả cho reducer.
Tài liệu
Trong docstring, ghi lại phần tử đồng nhất và hành vi với đầu vào rỗng, giả định tính kết hợp, chính sách lỗi.
Kiểm thử
Unit test các trường hợp biên: iterable rỗng, kiểu trộn, giá trị cực đoan.
Kiểm thử tính kết hợp.
Giám sát hiệu năng
Benchmark với đầu vào nhỏ và lớn. So sánh với hàm dựng sẵn và vòng lặp.
Nếu tốc độ quan trọng, hãy cân nhắc tiền xử lý, gom lô và chuyển các phép tính nặng sang NumPy hoặc pandas.
Ứng dụng nâng cao và tình huống thực tế cho Python reduce()
Với các nhược điểm đã nêu, có thể thấy reduce() không có giá trị thực. Trái lại, reduce() có nhiều trường hợp sử dụng hữu ích.
Xử lý cấu trúc lồng nhau
Các thao tác kiểu cơ sở dữ liệu
Pipeline xử lý dữ liệu
Ứng dụng MapReduce
Xử lý cấu trúc lồng nhau
Reduce cung cấp cách gọn gàng để duyệt các cấu trúc dữ liệu lồng nhau, như đối tượng JSON, bằng cách gấp một chuỗi khóa vào các lần tra cứu liên tiếp.
import json
from functools importreduceimport operator
data = json.loads('''
{
"user": {
"id": "ABC123",
"name": "Alice",
"email": "[email protected]",
"profile": {
"address": {
"city": "San Francisco",
"zip": "94103"
},
"age": 34,
"skills": ["Python", "Data Science", "Machine Learning"]
}
}
}
''')# Example lookups with reduce + operator.getitem
city =reduce(operator.getitem,["user","profile","address","city"], data)print(city)# "San Francisco"
user_id =reduce(operator.getitem,["user","id"], data)print(user_id)# "ABC123"
age =reduce(operator.getitem,["user","profile","age"], data)print(age)# 34Được hỗ trợ bởi
Dùng reduce() là hợp lý ở đây. JSON được lồng rất sâu: user → profile → address → city. Thay vì xâu chuỗi tra cứu thủ công, hãy biểu diễn đường dẫn như một danh sách các khóa. Sau đó dùng reduce(operator.getitem, path, data) để duyệt. Cách này giúp mã tổng quát, dễ đọc và tái sử dụng.
Pipeline xử lý dữ liệu
Reduce có thể điều khiển pipeline xử lý dữ liệu bằng cách truyền dữ liệu qua một chuỗi biến đổi. Mỗi hàm xử lý một bước, và pipeline là kết quả của việc áp dụng chúng theo thứ tự. Dưới đây là một pipeline nhỏ tiền xử lý chuỗi văn bản trước khi đưa vào mô hình NLP.
from functools importreduceimport re
# Define preprocessing stepsdefstrip_punctuation(s):return re.sub(r"[^\w\s]","", s)defto_lower(s):return s.lower()defremove_stopwords(s):
stops ={"the","is","a","of"}return" ".join(word for word in s.split()if word notin stops)defstem_words(s):# trivial "stemmer": cut off 'ing'return" ".join(word[:-3]if word.endswith("ing")else word for word in s.split())
pipeline =[
strip_punctuation,
to_lower,
remove_stopwords,
stem_words,]# Input data
text ="The quick brown fox is Jumping over a log."# Apply pipeline with reduce
processed =reduce(lambda acc, f: f(acc), pipeline, text)print(processed)# quick brown fox jump over logĐược hỗ trợ bởi
Xử lý lỗi trong ứng dụng phức tạp
Quay lại ví dụ JSON lồng nhau. Hiện tại, lời gọi trực tiếp reduce(operator.getitem, …) sẽ ném KeyError hoặc TypeError nếu thiếu khóa hoặc gặp kiểu không phải dict. Để mã an toàn hơn, hãy định nghĩa một hàm trợ giúp bọc operator.getitem trong khối try/except và trả về giá trị mặc định khi xảy ra lỗi.
Đây là một phương án cho hàm trợ giúp.
defdeep_get(data, keys, default=None):"""Traverse nested dicts/lists safely with reduce."""try:returnreduce(operator.getitem, keys, data)except(KeyError, IndexError, TypeError):return defaultĐược hỗ trợ bởi
Bây giờ, thay các lần tra cứu ví dụ để dùng hàm mới của chúng ta thay vì một reduce() không bọc
# Example lookups
city = deep_get(data,["user","profile","address","city"], default="Unknown City")print(city)# "San Francisco"
user_id = deep_get(data,["user","id"], default="N/A")print(user_id)# "ABC123"
age = deep_get(data,["user","profile","age"], default="N/A")print(age)# 34# Example with missing key
phone = deep_get(data,["user","profile","phone"], default="No phone")print(phone)# "No phone"Được hỗ trợ bởi
Biến đổi dữ liệu nhiều bước với map() và filter()
Bạn có thể kết hợp reduce() với các công cụ hàm khác như map() và filter() để xây dựng các biến đổi dữ liệu nhiều bước. Dưới đây là pipeline tiền xử lý NLP trước đó viết theo phong cách hàm.
from functools importreduceimport re
# Define preprocessing stepsdefstrip_punctuation(s):return re.sub(r"[^\w\s]","", s)defto_lower(s):return" ".join(map(str.lower, s.split()))defremove_stopwords(s):
stops ={"the","is","a","of"}return" ".join(filter(lambda w: w notin stops, s.split()))defstem_words(s):# trivial "stemmer": cut off 'ing'return" ".join(map(lambda w: w[:-3]if w.endswith("ing")else w, s.split()))# Pipeline of transformations
pipeline =[
strip_punctuation,
to_lower,
remove_stopwords,
stem_words,]# Input data
text ="The quick brown fox is Jumping over a log."# Apply pipeline with reduce
processed =reduce(lambda acc, f: f(acc), pipeline, text)print(processed)# quick brown fox jump over logĐược hỗ trợ bởi
Các biến đổi gồm:
Loại dấu câu. "The quick brown fox is Jumping over a log." → "The quick brown fox is Jumping over a log"
Chuyển chữ thường
"The quick brown fox is Jumping over a log" → "the quick brown fox is jumping over a log"
Loại bỏ stop word. "the quick brown fox is jumping over a log" → "quick brown fox jumping over log"
Rút gọn (stem) từ
"quick brown fox jumping log" → "quick brown fox jump log"
Để khám phá thêm ý tưởng lập trình hàm và vector hóa, chúng tôi khuyến nghị các bài viết DataCamp này.
Để dùng reduce() tốt, bạn nên hiểu nó phù hợp thế nào trong hệ sinh thái Python hiện đại. Hãy tìm hiểu cách nó song hành với NumPy và pandas, cách nó là nền tảng cho hệ thống song song và phân tán, và cách nó tương tác với các công cụ hiện đại như bộ phân tích tĩnh.
numpy và pandas
NumPy và pandas hoạt động bằng mã C tối ưu, nên đừng lặp lại chức năng của chúng với reduce(). Tuy nhiên, reduce() là lựa chọn tốt cho các pipeline có bước động. Ví dụ, bạn có thể hợp thành nhiều biến đổi NumPy trên một mảng.
from functools importreduceimport numpy as np
defstandardize(x):return(x - x.mean())/(x.std()+1e-9)defclip(x):return np.clip(x,0,1)deflog(x):return np.log1p(x)
x = np.array([1,500,40.5,100,250.45])
funcs =[standardize, clip, log]
y =reduce(lambda a, f: f(a), funcs, x)# x is ndarrayĐược hỗ trợ bởi
Các framework tính toán song song và phân tán
Reduce là trung tâm của các framework tính toán song song và phân tán. Các hệ thống này hoạt động bằng cách chia tập dữ liệu thành các phân vùng, xử lý các phân vùng đó song song, rồi kết hợp các kết quả cục bộ thành một câu trả lời. Bước "kết hợp" chính là phép gấp (reduction).
Để phép giảm hoạt động đúng trên một cụm, hãy đảm bảo các điều kiện sau.
Tính kết hợp (Associativity). Phép toán phải cho cùng kết quả bất kể cách nhóm. Điều này cho phép hợp nhất kết quả cục bộ theo bất kỳ thứ tự nào qua mạng.
Phần tử đồng nhất (Identity element). Phép toán phải có một initializer không ảnh hưởng đến kết quả cuối. Điều này đảm bảo tính đúng đắn khi phân vùng rỗng hoặc kích thước không đều.
Nếu các thuộc tính này không thỏa, phép giảm sẽ chậm (vì không thể song song hóa an toàn) hoặc sai (vì kết quả phụ thuộc vào thứ tự đánh giá).
Quy trình này cần một phép toán có tính kết hợp và phần tử đồng nhất rõ ràng. Nếu không, bạn sẽ có mã chậm hoặc kết quả sai.
Công cụ phân tích tĩnh
Bộ phân tích tĩnh (như mypy, Pyright, ruff, hoặc bandit) là công cụ kiểm tra mã mà không chạy nó. Các công cụ này bắt lỗi, áp quy tắc phong cách và kiểm tra tính đúng đắn kiểu.
Bộ phân tích tĩnh gặp khó với reduce(). Trong quá trình gấp, kiểu bộ tích lũy có thể khác với kiểu phần tử, và suy luận kiểu trở nên rối rắm.
Xem đoạn mã này.
from functools importreducedefadd_chars(acc, word):
acc.extend(word)# extend adds each character of the stringreturn acc
chars =reduce(add_chars,["hi","ok"],[])print(chars)# ['h', 'i', 'o', 'k']Được hỗ trợ bởi
Mặc dù mã này chạy ổn, một bộ phân tích tĩnh có thể phàn nàn vài điều.
Kiểu phần tử của initializer [] là mơ hồ.
Các bộ phân tích có thể giả định kiểu bộ tích lũy và kiểu phần tử trùng nhau.
Để mã rõ ràng hơn với con người và bộ phân tích, hãy thêm gợi ý kiểu (type hints).
from functools importreducefrom typing import List
defadd_chars(acc: List[str], word:str)-> List[str]:
acc.extend(word)# extend adds each character of the stringreturn acc
chars: List[str]=reduce(add_chars,["hi","ok"],[])print(chars)# ['h', 'i', 'o', 'k']Được hỗ trợ bởi
Giờ đây reducer thể hiện rõ
Bộ tích lũy là List[str].
Mỗi phần tử là str.
Kiểu trả về là list[str].
Với các gợi ý này, bộ phân tích tĩnh có thể kiểm tra nghiêm ngặt đoạn mã.
Kết luận
Reduce xuất phát từ lập trình hàm, nơi việc gấp các tập hợp thành một kết quả duy nhất là ý tưởng cốt lõi. Dù không còn là công cụ hạng nhất trong Python, nó vẫn có chỗ đứng. Khi bạn cần các pipeline linh hoạt, phép gấp tùy chỉnh hoặc các thao tác không khớp gọn với hàm sẵn có, reduce() là công cụ mạnh. Dùng cẩn trọng, nó hòa nhập tốt với hệ sinh thái Python rộng hơn và vẫn là công cụ thực tế trong đúng bối cảnh.
Một số liên kết Python liên quan mà bạn có thể thấy hữu ích.
Nâng cao kỹ năng về dữ liệu, khám phá cách thao tác và trực quan hóa dữ liệu, đồng thời áp dụng phân tích nâng cao để đưa ra quyết định dựa trên dữ liệu.
Hàm
reduce()của Python xuất phát từ thế giới lập trình hàm. Lập trình hàm (FP) là một mô hình lập trình trong đó chương trình tạo ra kết quả bằng cách áp dụng các hàm lên dữ liệu bất biến.Một mẫu phổ biến trong phong cách này là "gấp" (fold), tức thu gọn một dãy về một kết quả duy nhất. Ví dụ, gấp danh sách số [2, 4, 5, 3] theo phép cộng để ra 14 qua các bước kế tiếp: [2, 4, 5, 3] → [6, 5, 3] → [11, 3] → 14.
reduce()khái quát hóa ý tưởng này. Nó áp dụng một phép toán nhị nguyên lên một iterable cho đến khi chỉ còn lại kết quả.Trong bài viết này, tôi sẽ khám phá các thành phần chính của
reduce()trong Python và đưa ra một số ví dụ thực tế. Nếu bạn cần ôn lại các kiến thức cơ bản về Python, tôi khuyến nghị xem qua các tài nguyên sau:Lịch sử
Python bao gồm các hàm lập trình hàm khác trong Python như
map()vàfilter().Các cấu trúc chức năng như
map()vàfilter()đã có từ Python 1.0. Guido van Rossum không thích chúng, chỉ ra rằngreduce()khó đọc, và vòng lặpforhầu như luôn dễ hiểu hơn. Trong Python 3.0, theo PEP 3100, các nhà phát triển đã loạireduce()khỏi nhóm built-in và chuyển nó sang mô-đunfunctools. Việc chuyển sangfunctoolsvề bản chất đã giáng nó xuống thành một công cụ ngách.Vì sao dùng reduce()?
Phần lớn thời gian, tôi thấy sử dụng một hàm dựng sẵn hoặc vòng lặp có lẽ là lựa chọn tốt hơn. Tuy nhiên,
reduce()vẫn là lựa chọn tốt trong một số trường hợp.Cách hoạt động của reduce() trong Python
Hãy khám phá cơ chế của
reduce().Chữ ký hàm cơ bản của
reduce()làHàm
reduce()nhận hai đối số bắt buộc và một đối số tùy chọn thứ ba.function: một hàm nhị nguyên chỉ định cách kết hợp hai phần tử,iterable: dãy hoặc iterable cần gấp, như list hoặc tuple,initializer(tùy chọn): giá trị khởi đầu để gieo hàm.Ví dụ từng bước:
Kết quả cuối cùng là 13.
Ví dụ reduce() đơn giản
Các ví dụ nhỏ sau minh họa cơ chế cách dùng
reduce().Giá trị khởi tạo (Initializer)
Không có initializer,
reduce()lấy phần tử đầu tiên của iterable làm giá trị bắt đầu. Nếu iterable rỗng,reduce()ném raTypeError. Để mã bền vững, hãy cung cấp initializer để định nghĩa hành vi với đầu vào rỗng.Initializer cũng gieo kết quả bằng một container rỗng. Chẳng hạn, bạn có thể nối các từ thành một danh sách phẳng các ký tự.
Để khám phá thêm Python và thao tác dữ liệu, dưới đây là một số lựa chọn tôi khuyến nghị.
Định nghĩa reducer
Đến giờ, chúng ta dùng các hàm lambda để định nghĩa toán tử nhị nguyên.
Bạn cũng có thể dùng các toán tử từ mô-đun
operator. Mô-đunoperatorchứa các phiên bản hàm của những toán tử và lời gọi phương thức thông dụng. Ví dụ, thay vì x + y, dùngoperator.add(x, y). Điều này cho phép bạn truyền các toán tử dựng sẵn (và hiệu quả) vàoreduce() mà không cần viết lambda.Lựa chọn thứ ba là viết một hàm tùy chỉnh. Đây là lựa chọn tốt khi không có toán tử dựng sẵn hoặc hàm quá phức tạp đối với lambda.
Ví dụ, giả sử bạn muốn loại bỏ trùng lặp khỏi một danh sách nhưng giữ nguyên thứ tự xuất hiện đầu tiên. Bạn có thể định nghĩa một reducer chỉ thêm một phần tử vào danh sách nếu nó chưa xuất hiện trước đó.
Để có thêm ý tưởng về loại bỏ trùng lặp, hãy tham khảo hướng dẫn này.
Hiệu năng của Python reduce()
reduce()có những vấn đề về hiệu năng.Chi phí gọi hàm
Python
reduce()gọi hàm của chúng ta một lần cho mỗi phần tử. Với một danh sách có một triệu phần tử, tức là một triệu lời gọi hàm, mỗi lần tạo một frame, xử lý đối số và cập nhật đếm tham chiếu. Điều này gây thêm chi phí đáng kể.Ngược lại, một hàm dựng sẵn như
sumchỉ thực hiện một lời gọi vào hàm C và thực hiện hàng triệu phép cộng trong vòng lặp C. Sự khác biệt này có thể khiến hàm dựng sẵn nhanh hơn hàng bậc độ lớn.Vấn đề về tính cục bộ bộ nhớ đệm và hiệu quả CPU
Reduce cũng chịu ảnh hưởng bởi tính cục bộ bộ nhớ đệm và hiệu quả CPU.
Một CPU hiện đại có thể thực thi vài tỷ lệnh mỗi giây, nhưng truy cập RAM chậm hơn hàng bậc độ lớn. Để bù đắp, CPU hiện đại có bộ nhớ đệm (L1, L2, L3) lưu trữ dữ liệu để truy cập nhanh.
Các bộ nhớ đệm này tận dụng hai kiểu cục bộ.
Trái lại, mỗi bước của
reduce()liên quan đến việc lần theo con trỏ để tìm phần tử tiếp theo và các lời gọi hàm Python, làm phá vỡ tính cục bộ và khiến CPU bị chững. Các hàm dựng sẵn và hàm vector hóa tránh được vấn đề này bằng cách chạy trong các vòng lặp C chặt chẽ.Để ôn lại cách viết mã Python đúng chuẩn và hiệu quả, hãy xem:
Các lựa chọn thay thế cho Python reduce()
Hàm dựng sẵn:
reduce()phải gánh. Lợi thế tốc độ này tăng lên khi đầu vào lớn.sum,min) nên ý đồ rõ ràng. Một lời gọireduce()khiến bạn phải phân tích hàm đang được gấp.Vòng lặp:
reduce().reduce(). Gọireduce()buộc bạn diễn giải một biểu thức chức năng, trong khi vòng lặp mang tính Pythonic hơn.itertools.accumulate()Thư viện
itertoolscủa Python cung cấp một tập hợp iterator hiệu năng nhưcount(),product()vàcombinations(). Một hàmitertoolshữu ích làitertools.accumulate(). Tương tựreduce(), nó gấp một hàm lên một iterable. Tuy nhiên,accumulate() lưu lại các giá trị trung gian của phép tính, không chỉ kết quả cuối.Ví dụ,
Accumulate hữu ích khi bạn cần tổng chạy hoặc giá trị nhỏ nhất/lớn nhất chạy. Chẳng hạn, bạn có thể muốn biết tháng có nhiệt độ tối đa theo từng tháng.
Những lỗi thường gặp khi dùng reduce()
Khi bạn dùng
reduce(), hãy lưu ý các lỗi sau.reduce()cho những lúc thật sự cần.reduce()vào các tình huống mà cách tiếp cận generator hoặc streaming sẽ hiệu quả hơn.operatorkhi có thể. Lambda, đặc biệt với các phép không kết hợp, có thể làm giảm độ rõ ràng.Thực tiễn tốt và hướng dẫn cho Python reduce()
Như bất kỳ công cụ nào,
reduce()cũng có các thực tiễn tốt. Nếu bạn đã quyết địnhreduce()là công cụ phù hợp, dưới đây là một số hướng dẫn khi sử dụng.Thiết kế reducer trước
minlàmath.inf, và vớiphép hợp setlàset(). Điều này giúp mã bền vững và tránhTypeErrors.Giữ đơn giản và dễ đọc
Tài liệu
Kiểm thử
Giám sát hiệu năng
Ứng dụng nâng cao và tình huống thực tế cho Python reduce()
Với các nhược điểm đã nêu, có thể thấy
reduce()không có giá trị thực. Trái lại,reduce()có nhiều trường hợp sử dụng hữu ích.Xử lý cấu trúc lồng nhau
Reduce cung cấp cách gọn gàng để duyệt các cấu trúc dữ liệu lồng nhau, như đối tượng JSON, bằng cách gấp một chuỗi khóa vào các lần tra cứu liên tiếp.
Dùng
reduce()là hợp lý ở đây. JSON được lồng rất sâu:user → profile → address → city. Thay vì xâu chuỗi tra cứu thủ công, hãy biểu diễn đường dẫn như một danh sách các khóa. Sau đó dùngreduce(operator.getitem, path, data)để duyệt. Cách này giúp mã tổng quát, dễ đọc và tái sử dụng.Pipeline xử lý dữ liệu
Reduce có thể điều khiển pipeline xử lý dữ liệu bằng cách truyền dữ liệu qua một chuỗi biến đổi. Mỗi hàm xử lý một bước, và pipeline là kết quả của việc áp dụng chúng theo thứ tự. Dưới đây là một pipeline nhỏ tiền xử lý chuỗi văn bản trước khi đưa vào mô hình NLP.
Xử lý lỗi trong ứng dụng phức tạp
Quay lại ví dụ JSON lồng nhau. Hiện tại, lời gọi trực tiếp
reduce(operator.getitem, …)sẽ némKeyErrorhoặcTypeErrornếu thiếu khóa hoặc gặp kiểu không phải dict. Để mã an toàn hơn, hãy định nghĩa một hàm trợ giúp bọcoperator.getitemtrong khối try/except và trả về giá trị mặc định khi xảy ra lỗi.Đây là một phương án cho hàm trợ giúp.
Bây giờ, thay các lần tra cứu ví dụ để dùng hàm mới của chúng ta thay vì một
reduce()không bọcBiến đổi dữ liệu nhiều bước với map() và filter()
Bạn có thể kết hợp
reduce()với các công cụ hàm khác nhưmap()vàfilter()để xây dựng các biến đổi dữ liệu nhiều bước. Dưới đây là pipeline tiền xử lý NLP trước đó viết theo phong cách hàm.Các biến đổi gồm:
"The quick brown fox is Jumping over a log" → "the quick brown fox is jumping over a log"
"quick brown fox jumping log" → "quick brown fox jump log"
Để khám phá thêm ý tưởng lập trình hàm và vector hóa, chúng tôi khuyến nghị các bài viết DataCamp này.
Tích hợp với hệ sinh thái Python hiện đại
Để dùng
reduce()tốt, bạn nên hiểu nó phù hợp thế nào trong hệ sinh thái Python hiện đại. Hãy tìm hiểu cách nó song hành với NumPy và pandas, cách nó là nền tảng cho hệ thống song song và phân tán, và cách nó tương tác với các công cụ hiện đại như bộ phân tích tĩnh.numpy và pandas
NumPy và pandas hoạt động bằng mã C tối ưu, nên đừng lặp lại chức năng của chúng với
reduce(). Tuy nhiên,reduce()là lựa chọn tốt cho các pipeline có bước động. Ví dụ, bạn có thể hợp thành nhiều biến đổi NumPy trên một mảng.Các framework tính toán song song và phân tán
Reduce là trung tâm của các framework tính toán song song và phân tán. Các hệ thống này hoạt động bằng cách chia tập dữ liệu thành các phân vùng, xử lý các phân vùng đó song song, rồi kết hợp các kết quả cục bộ thành một câu trả lời. Bước "kết hợp" chính là phép gấp (reduction).
Để phép giảm hoạt động đúng trên một cụm, hãy đảm bảo các điều kiện sau.
Nếu các thuộc tính này không thỏa, phép giảm sẽ chậm (vì không thể song song hóa an toàn) hoặc sai (vì kết quả phụ thuộc vào thứ tự đánh giá).
Quy trình này cần một phép toán có tính kết hợp và phần tử đồng nhất rõ ràng. Nếu không, bạn sẽ có mã chậm hoặc kết quả sai.
Công cụ phân tích tĩnh
Bộ phân tích tĩnh (như mypy, Pyright, ruff, hoặc bandit) là công cụ kiểm tra mã mà không chạy nó. Các công cụ này bắt lỗi, áp quy tắc phong cách và kiểm tra tính đúng đắn kiểu.
Bộ phân tích tĩnh gặp khó với
reduce(). Trong quá trình gấp, kiểu bộ tích lũy có thể khác với kiểu phần tử, và suy luận kiểu trở nên rối rắm.Xem đoạn mã này.
Mặc dù mã này chạy ổn, một bộ phân tích tĩnh có thể phàn nàn vài điều.
Để mã rõ ràng hơn với con người và bộ phân tích, hãy thêm gợi ý kiểu (type hints).
Giờ đây reducer thể hiện rõ
List[str].str.list[str].Với các gợi ý này, bộ phân tích tĩnh có thể kiểm tra nghiêm ngặt đoạn mã.
Kết luận
Reduce xuất phát từ lập trình hàm, nơi việc gấp các tập hợp thành một kết quả duy nhất là ý tưởng cốt lõi. Dù không còn là công cụ hạng nhất trong Python, nó vẫn có chỗ đứng. Khi bạn cần các pipeline linh hoạt, phép gấp tùy chỉnh hoặc các thao tác không khớp gọn với hàm sẵn có,
reduce()là công cụ mạnh. Dùng cẩn trọng, nó hòa nhập tốt với hệ sinh thái Python rộng hơn và vẫn là công cụ thực tế trong đúng bối cảnh.Một số liên kết Python liên quan mà bạn có thể thấy hữu ích.