仅用7行Python代码,完美演绎MapReduce并行运算编程模型

MapReduce 是一种用于大规模数据集的并行运算编程模型,分为 Map(映射)和 Reduce(归约)两个步骤。Py2 时代,map() 和 reduce() 都是标准函数。不知为何,Py3 把 reduce() 藏到了标准模块 functools 中,只保留了 map() 在标准函数库内。借助于 Python 的标准进程模块(不熟悉进程模块的读者,可先阅读文后的两段参考资料),以及map() 和 reduce() 函数,我们可以非常容易地搭建一个 MapReduce 框架。

下面,我们以计算整数列表各元素的平方和为例,演示 MapReduce 并行运算编程模型。

# -*- coding: utf-8 -*-

from functools import reduce
import multiprocessing as mp

def power(x): # 返回x的平方
    return pow(x, 2)

def adder(a, b): # 求和
    return a + b

def map_reduce(dataset, processes=4): # 默认进程池最多4个进程
    with mp.Pool(processes=processes) as mpp: # 创建进程池mpp
        d = mpp.map(power, dataset) # 使用进程池Pool的映射方法,计算整数列表各元素的平方,返回列表
        result = reduce(adder, d, 0) # 规约计算结果
        return result # 返回计算结果

if __name__ == '__main__':
    dataset = range(10000) # 生成[0,10000)区间的整数列表(实际是生成器)作为数据集
    print(map_reduce(dataset))

一个听起来非常繁琐的概念,我们只用了十几行代码就讲清楚了。事实上,如果将求和改用lambda 函数,这个代码还可以写得更简洁。需要说明一点:不能在进程池的 map() 方法中使用lambda 函数,因为在 Pool 实例化之前就应定义好这个函数——这就是所谓的可序列化(pickle)。

from functools import reduce
import multiprocessing as mp

def power(x):
    return pow(x, 2)

if __name__ == '__main__':
    with mp.Pool(processes=4) as mpp:
        print(reduce(lambda a,b:a+b, mpp.map(power, range(10000)), 0))

最终,我们仅用7行Python代码,就完美地演绎了MapReduce并行运算编程模型。


参考资料1. 创建、启动和管理进程

Process 类是 multiprocessing 模块的子进程类,用于创建、启动和管理子进程。Process 和线程模块 treading.Thread 的 API 几乎完全相同。Process 类用来描述一个进程对象。创建子进程的时候,只需要传入进程函数和函数的参数即可完成 Process 实例化。

下面这段代码,主进程启动了两个子进程,然后等待用户的键盘输入以结束程序。主进程结束后,子进程也随之结束。

# -*- coding: utf-8 -*-

import os, time
import multiprocessing as mp

def sub_process(name, delay):
    """进程函数"""
    
    while True:
        time.sleep(delay)
        print('我是子进程%s,进程id为%s'%(name, os.getpid()))

if __name__ == '__main__':
    print('主进程(%s)开始,按任意键结束本程序'%os.getpid())
    
    p_a = mp.Process(target=sub_process, args=('A', 1))
    p_a.daemon = True  # 设置子进程为守护进程
    p_a.start()
    
    p_b = mp.Process(target=sub_process, args=('B', 2))
    p_b.daemon = True  # 如果子进程不是守护进程,主进程结束后子进程可能成为僵尸进程
    p_b.start()
        
    input() # 利用input函数阻塞主进程。这是常用的调试手段之一。

参考资料2. 进程池的典型应用

和线程一样,处理并行任务时,处理效率和进程数量并不总是成正比——当进程数量超过一定限度后,完成任务所需时间反倒会延长。进程池提供了一个保持合理进程数量的方案,但合理进程数量则需要根据硬件状况及运行状况来确定。

multiprocessing.Pool(n) 用于创建n个进程的进程池供用户调用。如果进程池任务不满,则新的进程请求会被立即执行;如果进程池任务已满,则新的请求将等待至有可用进程才被执行。向进程池提交任务有两种方式:

  • apply_async(func[, args[, kwds[, callback]]])
    非阻塞式提交。即使进程池已满,也会接受新的任务,不会阻塞主进程。新任务将处于等待状态。
  • apply(func[, args[, kwds]])
    阻塞式提交。若进程池已满,则主进程阻塞,直至有空闲进成可用

下面的代码,演示了进程池的典型用法。读者可自行尝试阻塞式提交和非阻塞式提交两种方法的差异。

# -*- coding: utf-8 -*-

import time
import multiprocessing as mp

def power(x, a=2):
    """进程函数:幂函数"""
    
    time.sleep(1)
    print('%d的%d次方等于%d'%(x, a, pow(x, a)))

def demo():
    mpp = mp.Pool(processes=4)
    
    for item in [2,3,4,5,6,7,8,9]:
        mpp.apply_async(power, (item, )) # 非阻塞式提交新任务
        #mpp.apply(power, (item, )) # 阻塞式提交新任务
    
    mpp.close() # 关闭进程池,意味着不再接受新的任务
    print('主进程走到这里,正在等待子进程结束')
    mpp.join() # 等待所有子进程结束
    print('程序结束')

if __name__ == '__main__':
    demo()

后记

近期有很多朋友通过私信咨询有关Python学习问题。为便于交流,我在CSDN的app上创建了“Python作业辅导”大本营,面向Python初学者,为大家提供咨询服务、辅导Python作业。欢迎有兴趣的同学使用微信扫码加入。

在这里插入图片描述

从博客到公众号,每一篇、每一题、每一句、每一行代码,都坚持原创,绝不复制抄袭,这是我坚守的原则。如果喜欢,请关注我的微信公众号“Python作业辅导员”。

在这里插入图片描述

你可能感兴趣的:(python论道)