Python是一门很是适合处理数据和自动化完成重复性工做的编程语言,咱们在用数据训练机器学习模型以前,一般都须要对数据进行预处理,而Python就很是适合完成这项工做,好比须要从新调整几十万张图像的尺寸,用Python没问题!你几乎老是能找到一款能够轻松完成数据处理工做的Python库。python
然而,虽然Python易于学习,使用方便,但它并不是运行速度最快的语言。默认状况下,Python程序使用一个CPU以单个进程运行。不过若是你是在最近几年配置的电脑,一般都是四核处理器,也就是有4个CPU。这就意味着在你苦苦等待Python脚本完成数据处理工做时,你的电脑其实有75%甚至更多的计算资源就在那闲着没事干!web
今天我(做者Adam Geitgey——译者注)就教你们怎样经过并行运行Python函数,充分利用你的电脑的所有处理能力。得益于Python的 concurrent.futures 模块,咱们只需3行代码,就能将一个普通数据处理脚本变为能并行处理数据的脚本,提速4倍。编程
普通Python处理数据方法服务器
比方说,咱们有一个全是图像数据的文件夹,想用Python为每张图像建立缩略图。多线程
下面是一个短暂的脚本,用Python的内置glob函数获取文件夹中全部JPEG图像的列表,而后用Pillow图像处理库为每张图像保存大小为128像素的缩略图:app
<pre class="ql-align-justify" >机器学习
import glob
import os
from PIL import Image
def make_image_thumbnail(filename):编程语言
base_filename, file_extension = os.path.splitext(filename)
thumbnail_filename = f"{base_filename}_thumbnail{file_extension}"函数
image = Image.open(filename)
image.thumbnail(size=(128, 128))
image.save(thumbnail_filename, "JPEG")
return thumbnail_filename学习
for image_file in glob.glob("*.jpg"):
thumbnail_file = make_image_thumbnail(image_file)
print(f"A thumbnail for {image_file} was saved as {thumbnail_file}")
</pre>
这段脚本沿用了一个简单的模式,你会在数据处理脚本中常常见到这种方法:
我们用一个包含1000张JPEG图像的文件夹测试一下这段脚本,看看运行完要花多长时间:
<pre class="ql-align-justify" >$ time python3 thumbnails_1.py
A thumbnail for 1430028941_4db9dedd10.jpg was saved as 1430028941_4db9dedd10_thumbnail.jpg
[... about 1000 more lines of output ...]
real 0m8.956s
user 0m7.086s
sys 0m0.743s
</pre>
运行程序花了8.9秒,可是电脑的真实工做强度怎样呢?
咱们再运行一遍程序,看看程序运行时的活动监视器状况:
仅用3行Python代码便可让数据脚本提高4倍的速度!
电脑有75%的处理资源处于闲置状态!这是什么状况?
这个问题的缘由就是个人电脑有4个CPU,但Python只使用了一个。因此程序只是卯足了劲用其中一个CPU,另外3个却无所事事。所以我须要一种方法能将工做量分红4个我能并行处理的单独部分。幸运的是,Python中有个方法很容易能让咱们作到!
试试建立多进程
下面是一种可让咱们并行处理数据的方法:
1.将JPEG文件划分为4小块。 2.运行Python解释器的4个单独实例。 3.让每一个Python实例处理这4块数据中的一块。 4.将这4部分的处理结果合并,得到结果的最终列表。
4个Python拷贝程序在4个单独的CPU上运行,处理的工做量应该能比一个CPU大约高出4倍,对吧?
最妙的是,Python已经替咱们作完了最麻烦的那部分工做。咱们只需告诉它想运行哪一个函数以及使用多少实例就好了,剩下的工做它会完成。整个过程咱们只须要改动3行代码。
首先,咱们须要导入concurrent.futures库,这个库就内置在Python中:
<pre class="ql-align-justify" >import concurrent.futures
</pre>
接着,咱们须要告诉Python启动4个额外的Python实例。咱们经过让Python建立一个Process Pool来完成这一步:
<pre class="ql-align-justify" >with concurrent.futures.ProcessPoolExecutor() as executor:
</pre>
默认状况下,它会为你电脑上的每一个CPU建立一个Python进程,因此若是你有4个CPU,就会启动4个Python进程。
最后一步是让建立的Process Pool用这4个进程在数据列表上执行咱们的辅助函数。完成这一步,咱们要将已有的for循环:
<pre class="ql-align-justify" >for image_file in glob.glob("*.jpg"):
thumbnail_file = make_image_thumbnail(image_file)
</pre>
替换为新的调用executor.map():
<pre class="ql-align-justify" >image_files = glob.glob("*.jpg")
for image_file, thumbnail_file in zip(image_files, executor.map(make_image_thumbnail, image_files)):
</pre>
该executor.map()函数调用时须要输入辅助函数和待处理的数据列表。这个函数能帮我完成全部麻烦的工做,包括将列表分为多个子列表、将子列表发送到每一个子进程、运行子进程以及合并结果等。干得漂亮!
这也能为咱们返回每一个函数调用的结果。Executor.map()函数会按照和输入数据相同的顺序返回结果。因此我用了Python的zip()函数做为捷径,一步获取原始文件名和每一步中的匹配结果。
这里是通过这三步改动后的程序代码:
<pre class="ql-align-justify" >
import glob
import os
from PIL import Image
import concurrent.futures
def make_image_thumbnail(filename):
base_filename, file_extension = os.path.splitext(filename)
thumbnail_filename = f"{base_filename}_thumbnail{file_extension}"
image = Image.open(filename)
image.thumbnail(size=(128, 128))
image.save(thumbnail_filename, "JPEG")
return thumbnail_filename
with concurrent.futures.ProcessPoolExecutor() as executor:
image_files = glob.glob("*.jpg")
for image_file, thumbnail_file in zip(image_files, executor.map(make_image_thumbnail, image_files)):
print(f"A thumbnail for {image_file} was saved as {thumbnail_file}")
</pre>
咱们来运行一下这段脚本,看看它是否以更快的速度完成数据处理:
<pre class="ql-align-justify" >$ time python3 thumbnails_2.py
A thumbnail for 1430028941_4db9dedd10.jpg was saved as 1430028941_4db9dedd10_thumbnail.jpg
[... about 1000 more lines of output ...]
real 0m2.274s
user 0m8.959s
sys 0m0.951s
</pre>
脚本在2.2秒就处理完了数据!比原来的版本提速4倍!之因此能更快的处理数据,是由于咱们使用了4个CPU而不是1个。
可是若是你仔细看看,会发现“用户”时间几乎为9秒。那为什么程序处理时间为2.2秒,但不知怎么搞得运行时间仍是9秒?这彷佛不太可能啊?
这是由于“用户”时间是全部CPU时间的总和,咱们最终完成工做的CPU时间总和同样,都是9秒,但咱们使用4个CPU完成的,实际处理数据时间只有2.2秒!
注意:启用更多Python进程以及给子进程分配数据都会占用时间,所以靠这个方法并不能保证老是能大幅提升速度。若是你要处理很是大的数据集,这里有篇设置将数据集切分红多少小块的文章,能够读读,会对你帮助甚大.
这种方法总能帮个人数据处理脚本提速吗?
若是你有一列数据,而且每一个数据都能单独处理时,使用咱们这里所说的Process Pools是一个提速的好方法。下面是一些适合使用并行处理的例子:
但也要记住,Process Pools并非万能的。使用Process Pool须要在独立的Python处理进程之间来回传递数据。若是你要处理的数据不能在处理过程当中被有效地传递,这种方法就行不通了。简而言之,你处理的数据必须是Python知道怎么应对的类型。
同时,也没法按照一个预想的顺序处理数据。若是你须要前一步的处理结果来进行下一步,这种方法也行不通。
那GIL的问题呢?
你可能知道Python有个叫全局解释器锁(Global Interpreter Lock)的东西,即GIL。这意味着即便你的程序是多线程的,每一个线程也只能执行一个Python指令。GIL确保任什么时候候都只有一个Python线程执行。换句话说,多线程的Python代码并不能真正地并行运行,从而没法充分利用多核CPU。
可是Process Pool能解决这个问题!由于咱们是运行单独的Python实例,每一个实例都有本身的GIL。这样咱们得到是真正能并行处理的Python代码!
不要惧怕并行处理!
有了concurrent.futures库,Python就能让你简简单单地修改一下脚本后,马上让你电脑上全部CPU投入到工做中。不要惧怕尝试这种方法,一旦你掌握了,它就跟一个for循环同样简单,却能让你的数据处理脚本快到飞起。