通过 Python 中的 map() 和 reduce() 函数理解 MapReduce

427 阅读5分钟

简介

从前一篇文章介绍的 MapReduce 的概念,我们知道 mapper 是用来处理每一个数据的,

一堆数据,分成一小份一小份,不断的发给 mapper,mapper 处理完就 emit 给 reducer。

reducer 每收到一个 result,就在之前的结果上加一(即 result += 1),则不断的累加,最终得到总的统计结果。

Python 中的 map 函数原型是 map(mapper_func, iterable, ...)

这个 map() 跟我们另一篇文章中介绍的 Master Program(也即 mapred 程序)的作用非常相似,其第一个参数 mapper_func 跟 MapReduce 概念中的 mapper 作用类似,后面的 iterable 则是相当于输入数据(比如 Stdin)

这个 iterable 是可以是单个元素本身,也可以是可遍历的列表类元素,比如 list, dictionary, Go 中的 channel 之类的。

只要它是 iterable,那么就可以每次 feed 一个元素给 mapper_func。

我们来看一个简单的例子:

def square(x):
    return x * x

numbers = [1, 2, 3, 4, 5]

# Apply the square function to each number in the list
squared_numbers = map(square, numbers)

# Convert map object to list to see results
print(list(squared_numbers))

输出结果: [1, 4, 9, 16, 25]

可以看到 square 对 numbers 列表中的每个元素进行处理,最后由 map() 函数收集结果,并再次返回了一个列表。

lambda 语法更简洁:

numbers = [1, 2, 3, 4, 5]
squared_numbers = map(lambda x: x * x, numbers)
print(list(squared_numbers))

mapper 还可以同时应用到多个列表

numbers1 = [1, 2, 3]
numbers2 = [10, 20, 30]

result = map(lambda x, y: x * y, numbers1, numbers2)

print(list(result))

输出结果:[10, 40, 90]

mapper 虽然应用到多个列表,但是 map() 作为唯一的收集者,最终只返回一个列表作为结果。

reduce 函数

map() 函数是 Python 原生自带的,reduce 函数则是标准库 functools 中的。它实现了一个叫 folding 或 reduction 的功能。

它的目的就是不断的累加,直到最终返回一个累加后的总值。reduce 非常有 functional programming 的味道。

reduce() is useful when you need to apply a function to an iterable and reduce it to a single cumulative value.

reduce 函数原型:
functools.reduce(reducer_func, iterable[, initializer])

它的内部实现可能是这样:

def reduce(reducer_func, iterable, initializer=None):
    it = iter(iterable)
    if initializer is None:
        value = next(it)
    else:
        value = initializer
    for element in it:
        value = function(value, element)
    return value

initializer 就是初始值,假如我们要处理的 iterable 是一组 integer,那么 initializer 默认就是 0。

function 的作用就是接收一个 new_value,把这个 new_value 累加到 old_value 上,并返回累加后的结果。这个累加后的结果又作为下一次的 old_value。而 reduce 的作用就是把 iterable 中的元素一个个的发给 function 去累加。

接下来我们看如何把 map 和 reduce 两个函数结合起来,实现一个 MapReduce 效果:

from functools import reduce

def add(x, y):
    return x + y

numbers = [1, 2, 3, 4, 5]

squared_numbers = map(lambda x: x * x, numbers)

sum_of_squares = reduce(add, squared_numbers)

print(sum_of_squares)

输出结果:55

Word Counter

Word Counter 是 MapReduce 世界中最常见的实战案例,我们接下来实现一个简单的 word counter

from functools import reduce
from collections import Counter

text = ["hello world", "hello", "world hello"]

# Map phase: Split each line into words
word_lists = map(lambda line: line.split(), text)

# Flatten the list of lists into a single list of words
words = reduce(lambda x, y: x + y, word_lists)

# Reduce phase: Count the occurrences of each word
word_counts = Counter(words)

# Output the word count
print(word_counts)

输出结果:Counter({'hello': 3, 'world': 2})

点评与分析:

mapper 决定了数据以什么样的形式发给 reducer,比如是把输入的每个数据处理成 <hello, 2>, <world, 1> 这样的键值对,还是 text.split() 这样的只是把 string 分割成一个个 word, 发给 reducer。

reducer 则决定了怎么把一个个收到的数据“累加”成一个最终的结果。比如,上述的程序里就是把收到的 word 不断的拼接,组成一个长串。

Counter 这个函数对一个字符串中的所有 word 统计数量。简单来说就是,前面的 map, reduce 都是为了把数据整合成 Counter 所能接收的格式。

reduce 再度思考:

其实 reduce 不仅可以累加,还可以进行任意操作,我们只需要记住它是进行二元操作即可,比如 old_value + new_value, old_value * new_value, old_value < new_value, old_value > new_value 等等。

>>> from functools import reduce

>>> numbers = [3, 5, 2, 4, 7, 1]

>>> # Minimum
>>> reduce(lambda a, b: a if a < b else b, numbers)
1

>>> # Maximum
>>> reduce(lambda a, b: a if a > b else b, numbers)
7

警告:

从 reduce 的工作原理我们可以直到,它的性能好不了,因为它需要不断的调用函数。而且,它的可读性也不好。

如果我们想要它所能实现的上述功能,我们完全可以使用其他效率更高的库函数,比如:sum(), all(), any(), max(), min(), len(), math.prod()。尤其要注意,即便我们一定要使用 reduce,也要避免很复杂的自定义函数。

最后,我们再来看一个 Python 的 MapReduce 例子加深理解:

from collections import defaultdict

def mapper(word):
    return word, 1

def reducer(kv_pair):
    key, values = kv_pair
    return key, sum(values)

def map_reduce(input_list, mapper, reducer):
    map_results = map(mapper, input_list)
    shuffler = defaultdict(list)
    for key, value in map_results:
        shuffler[key].append(value)
    return map(reducer, shuffler.items())

if __name__ == "__main__":
    words = "hello how are you and you how old are you".split(" ")
    result = list(map_reduce(words, mapper, reducer))
    print(result)

我画了一个示意图来展示整个流程:

CleanShot 2024-10-24 at 22.57.26@2x.png 全文完!

如果你喜欢我的文章,欢迎关注我的微信公众号 deliverit