用好这3行代码,可让你的Python脚本速度提高5倍!

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):编程语言

缩略图会被命名为"<original_filename>_thumbnail.jpg"

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学习

循环文件夹中全部JPEG图像,为每张图像建立缩略图

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>

这段脚本沿用了一个简单的模式,你会在数据处理脚本中常常见到这种方法:

  • 首先得到你想处理的文件(或其它数据)的列表
  • 写一个辅助函数,可以处理上述文件的单个数据
  • 使用for循环调用辅助函数,处理每个单个数据,一次一个。

我们用一个包含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):

缩略图会被命名为 "<original_filename>_thumbnail.jpg"

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

建立Process Pool,默认为电脑的每一个CPU建立一个

with concurrent.futures.ProcessPoolExecutor() as executor:

获取须要处理的文件列表

image_files = glob.glob("*.jpg")

处理文件列表,但经过Process Pool划分工做,使用所有CPU!

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是一个提速的好方法。下面是一些适合使用并行处理的例子:

  • 从一系列单独的网页服务器日志里抓取统计数据。
  • 从一堆XML,CSV和JSON文件中解析数据。
  • 对大量图片数据作预处理,创建机器学习数据集。

但也要记住,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循环同样简单,却能让你的数据处理脚本快到飞起。

相关文章
相关标签/搜索