Chuyển đến nội dung chính

Python reduce(): Hướng dẫn đầy đủ

Tìm hiểu khi nào và cách sử dụng reduce() của Python. Bao gồm ví dụ thực tế và các thực tiễn tốt nhất.
· 11 phút đọc

Khám phá cùng AI

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ằ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])

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 import reduce

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"

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 import reduce

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, "")

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 import reduce

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']

Để 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ô-đun  operator chứ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 import reduce
import operator as op

numbers = [2, 4, 6]
total = reduce(op.add, numbers, 0)  # instead of reduce(lambda x,y: x + y, numbers)

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 import reduce


items = ['the', 'wild', 'wild', 'world', 'is', 'the', 'wide', 'world', 'is', 'the', 'world']


def dedup(acc, x):
    if x not in acc:  # O(n) membership test
        acc.append(x)
    return acc




unique = reduce(dedup, items, [])  # ['the', 'wild', 'world', 'is', 'wide']

Để 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ư 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:

Các lựa chọn thay thế cho Python reduce()

Hàm dựng sẵn:

  • 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 import reduce

list(itertools.accumulate([1, 2, 3, 4], initial=0)) # [0, 1, 3, 6, 10]
reduce(operator.add, [1, 2, 3, 4], 0) # 10

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 import reduce
import 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

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 import reduce
import re

# Define preprocessing steps
def strip_punctuation(s):
    return re.sub(r"[^\w\s]", "", s)

def to_lower(s):
    return s.lower()

def remove_stopwords(s):
    stops = {"the", "is", "a", "of"}
    return " ".join(word for word in s.split() if word not in stops)

def stem_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

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.

def deep_get(data, keys, default=None):
    """Traverse nested dicts/lists safely with reduce."""
    try:
        return reduce(operator.getitem, keys, data)
    except (KeyError, IndexError, TypeError):
        return default

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"

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 import reduce
import re

# Define preprocessing steps
def strip_punctuation(s):
    return re.sub(r"[^\w\s]", "", s)

def to_lower(s):
    return " ".join(map(str.lower, s.split()))

def remove_stopwords(s):
    stops = {"the", "is", "a", "of"}
    return " ".join(filter(lambda w: w not in stops, s.split()))

def stem_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á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.

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.

from functools import reduce
import numpy as np

def standardize(x):
    return (x - x.mean()) / (x.std() + 1e-9)

def clip(x):
    return np.clip(x, 0, 1)

def log(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á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 import reduce

def add_chars(acc, word):
    acc.extend(word)   # extend adds each character of the string
    return acc

chars = reduce(add_chars, ["hi", "ok"], [])
print(chars)  # ['h', 'i', 'o', 'k']

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 import reduce
from typing import List

def add_chars(acc: List[str], word: str) -> List[str]:
    acc.extend(word)   # extend adds each character of the string
    return acc

chars: List[str] = reduce(add_chars, ["hi", "ok"], [])
print(chars)  # ['h', 'i', 'o', 'k']

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.

Python reduce() FAQs

reduce() làm gì?

Nó lặp lại việc áp dụng một hàm hai đối số lên một iterable để thu gọn nó thành một kết quả duy nhất.

reduce() được định nghĩa ở đâu?

Trước phiên bản 3, nó là một hàm dựng sẵn. Kể từ đó, nó nằm trong mô-đun functools.

Tôi nên truyền những loại hàm nào vào reduce()?

Các hàm đơn giản, có tính kết hợp và được ghi chép rõ ràng. Tránh tác dụng phụ và logic không kết hợp.

Nó so sánh với itertools.accumulate() như thế nào?

reduce() chỉ trả về kết quả cuối, còn accumulate() trả ra mọi kết quả trung gian.

Khi nào tôi nên dùng initializer?

Dùng initializer khi iterable có thể rỗng.

Chủ đề

Liên quan