当前位置: 首页 > 科技观察

简单易用:用3行代码,让Python数据处理脚本快4倍

时间:2023-03-19 14:31:13 科技观察

Python是一种非常适合处理数据和自动化重复性任务的编程语言。对数据进行预处理,Python非常适合这个工作。比如你需要调整几十万张图片的大小,用Python是没有问题的!您几乎总能找到一个可以轻松进行数据操作的Python库。然而,虽然Python易于学习和使用,但它并不是运行速度最快的语言。默认情况下,Python程序使用一个CPU作为单个进程运行。但是,如果你近几年配置过电脑,一般都是四核处理器,也就是有4个CPU。这意味着在你等待Python脚本完成数据处理工作的同时,你的电脑75%甚至更多的计算资源实际上是闲置在那里的!今天我(作者AdamGeitgey——译者注)将教你如何通过并行运行Python函数来充分利用计算机的全部处理能力。得益于Python的concurrent.futures模块,我们只需要3行代码就可以将一个普通的数据处理脚本变成一个可以并行处理数据的脚本,速度提升了4倍。正常的Python数据处理方法假设我们有一个装满图像数据的文件夹,并且想使用Python为每个图像创建缩略图。这是一个简短的脚本,它使用Python的内置glob函数获取文件夹中所有JPEG图像的列表,然后使用Pillow图像处理库为每个图像保存大小为128像素的缩略图:importglobimportosfromPILimportImagedefmake_image_thumbnail(filename):#thumbnailWill被命名为"_thumbnail.jpg"base_filename,file_extension=os.path.splitext(filename)thumbnail_filename=f"{base_filename}_thumbnail{file_extension}"#Createandsavethumbnailimage=Image.open(filename)image.thumbnail(size=(128,128))image.save(thumbnail_filename,"JPEG")returnthumbnail_filename#循环文件夹中所有JPEG图片,为每张图片创建缩略图forimage_fileinglob.glob("*.jpg"):thumbnail_file=make_image_thumbnail(image_file)print(f"Athumbnailfor{image_file}wassavedas{thumbnail_file}")这个脚本遵循一个你在数据处理脚本中经常看到的简单模式:首先获取你想要处理的文件(或其他数据)写一个辅助函数,它可以处理上述文件的单个数据使用for循环调用辅助函数,一次处理一个数据。让我们用一个包含1000张JPEG图像的文件夹来测试这个脚本,看看它运行需要多长时间:$timepython3thumbnails_1.pyAthumbnailfor1430028941_4db9dedd10.jpgwassavedas1430028941_4db9dedd10_thumbnail.jpg[...sysabout1000morelinesofoutput...]real0m65s运行程序花了9.4秒7.4秒但计算机真正的工作强度是多少?让我们再次运行程序,看看程序运行时的活动监视器:75%的计算机处理资源处于空闲状态!这里发生了什么事?出现这个问题的原因是我的电脑有4个CPU,而Python只用了一个。所以程序只是试图使用其中一个CPU,而其他3个什么都不做。所以我需要一种方法将工作负载分成4个独立的部分,我可以并行处理这些部分。幸运的是,Python中有一种方法可以让我们轻松搞定!尝试创建多个进程下面是一种允许我们并行处理数据的方法:1.将JPEG文件分成4个小块。2.运行4个独立的Python解释器实例。3.让每个Python实例处理这4条数据中的一条。4.合并这四部分的处理结果,得到最终的结果列表。在4个独立的CPU上运行的4个Python副本应该能够处理比一个CPU多4倍的工作,对吗?最重要的是,Python已经为我们完成了困难的部分。我们只需告诉它我们要运行哪个函数以及要使用多少个实例,剩下的就由它来完成。整个过程我们只需要改动3行代码。首先,我们需要导入Python中内置的concurrent.futures库:importconcurrent.futures接下来,我们需要告诉Python启动4个额外的Python实例。我们通过让Python创建一个进程池来做到这一点:withconcurrent.futures.ProcessPoolExecutor()asexecutor:默认情况下,它将为您计算机上的每个CPU创建一个Python进程,因此如果您有4个CPU,则只会启动4个Python进程.最后一步是让创建的进程池使用这4个进程在数据列表上执行我们的辅助函数。要完成这一步,我们需要用新的调用executor.map():image_files=glob.glob("*.jpg")forimage_file,thumbnail_fileinzip(image_files,executor.map(make_image_thumbnail,image_files)):executor.map()函数调用时需要输入辅助函数和需要处理的数据列表。这个函数为我完成了所有艰苦的工作,包括将列表拆分为子列表、将子列表发送到每个子进程、运行子进程、合并结果等。干得好!这也向我们返回了每个函数调用的结果。Executor.map()函数以与输入数据相同的顺序返回结果。所以我用Python的zip()函数作为快捷方式一步步获取原始文件名和每一步的匹配结果。下面是这三步之后的程序代码:_thumbnail{file_extension}"#Createandsavethumbnailimage=Image.open(filename)image.thumbnail(size=(128,128))image.save(thumbnail_filename,"JPEG")returnthumbnail_filename#CreateProcessPool,默认为电脑创建一个withconcurrent。futures.ProcessPoolExecutor()asexecutorforeachCPU:#获取待处理文件列表image_files=glob.glob("*.jpg")#处理文件列表,但通过ProcessPool分工,使用所有CPU!forimage_file,thumbnail_fileinzip(image_files,executor.map(make_image_thumbnail,image_files)):print(f"Athumbnailfor{image_file}wassavedas{thumbnail_file}")Let'srunthisscriptandseeifitcompletesdataprocessingfaster:$timepython3thumbnails_2.pyAthumbnailfor1430028941_4db9dedd10.jpgwassavedas1430028941_4db9dedd10_thumbnail.jpg[...about1000morelinesofoutput...]real0m2.274suser0m8.959ssys0m0.951s脚本在2.2秒内处理了数据!比原版快4倍!数据处理速度更快的原因是因为我们使用了4个CPU而不是1个,但如果你仔细观察,“用户”时间几乎是9秒。那为什么程序处理时间是2.2秒,不知为何运行时间还是9秒呢?这似乎不太可能?这是因为“用户”时间是所有CPU时间的总和,我们最终用相同的CPU时间来完成工作,也就是9秒,但是我们用了4个CPU来完成,而实际的数据处理时间只有2.2秒!注意:启动更多Python进程并将数据分配给子进程需要时间,因此依赖此方法并不总是能保证较大的加速。如果你正在处理非常大的数据集,这里有一篇关于设置将数据集拆分成多少块的文章,你可以阅读这篇文章并且会对你有很大帮助。这种方法总是会加速我的数据处理脚本吗?如果你有一列数据,并且每个数据都可以单独处理,那么使用我们这里所说的ProcessPools是一个很好的提速方式。以下是一些适用于并行处理的示例:从一系列单独的Web服务器日志中抓取统计信息。从一堆XML、CSV和JSON文件中解析数据。预处理大量图像数据并构建机器学习数据集。但也请记住,进程池不是唯一的。使用ProcessPool需要在独立的Python处理进程之间来回传递数据。如果您正在处理的数据在处理过程中无法有效传递,则此方法将不起作用。简而言之,您正在处理的数据必须是Python知道如何处理的类型。同时,无法按预期的顺序处理数据。如果您需要上一步的结果才能继续下一步,这也不起作用。GIL问题呢?你可能知道Python有一个东西叫做全局解释器锁(GlobalInterpreterLock),或者GIL。这意味着即使你的程序是多线程的,每个线程也只能执行一条Python指令。GIL确保在任何时候只有一个Python线程执行。换句话说,多线程Python代码无法真正并行运行以充分利用多核CPU。但是ProcessPool可以解决这个问题!由于我们运行的是不同的Python实例,因此每个实例都有自己的GIL。这样我们就得到了可以真正并行处理的Python代码!不要害怕并行处理!借助concurrent.futures库,Python允许您简单地修改脚本并立即将计算机上的所有CPU投入工作。不要害怕尝试这种方法,一旦掌握了它,它就像for循环一样简单,可以让您的数据处理脚本更上一层楼。