IPython使用tips_2


1.测试代码的执行时间:%time和%timeit

对于规模更大、运行时间更长的数据分析应用程序,你可能会希望测试一下各个部分或函数调用或语句的执行时间。你可能会希望了解某个复杂计算过程中到底是哪些函数占用的时间最多。幸运的是,在开发和测试代码的过程中,IPython能够让你轻松得到这些信息。使用内置的time模块及其time.clock和time.time函数手工测试代码执行时间是一件令人烦闷的事情,因为你必须编写许多一模一样的了无生趣的公式化代码:

import time
start = time.time()
for i in range(iterations):
    # 这里放一些待执行的代码
elapsed_per = (time.time() - start) / iterations

由于这是一个非常常用的功能,所以IPython专门提供了两个魔术函数(%time和%timeit)以便自动完成该过程。%time一次执行一条语句,然后报告总体执行时间。假设我们有一大堆字符串,希望对几个“能够选出具有特殊前缀的字符串”的函数进行比较。下面是一个拥有60万字符串的数组,以及两个不同的“能够选出其中以foo开头的字符串”的方法:

# 一个非常大的字符串数组
strings = ['foo', 'foobar', 'baz', 'qux', 'python', 'Guido Van Rossum'] * 100000

method1 = [x for x in strings if x.startswith('foo')]

method2 = [x for x in strings if x[:3] == 'foo']

看上去它们的性能表现应该差不多,对吧?我们通过%time来确认一下:

In [561]: %time method1 = [x for x in strings if x.startswith('foo')]
CPU times: user 0.19 s, sys: 0.00 s, total: 0.19 s
Wall time: 0.19 s

In [562]: %time method2 = [x for x in strings if x[:3] == 'foo']
CPU times: user 0.09 s, sys: 0.00 s, total: 0.09 s
Wall time: 0.09 s

墙上时间(Wall time)是我们最感兴趣的数字。所以,看上去第一个方法耗费了两倍以上的时间,但这并不是一个非常精确的结果。如果你对相同语句多次执行%time的话,就会发现其结果是会变的。为了得到更为精确的结果,需要使用魔术函数%timeit。对于任意语句,它会自动多次执行以产生一个非常精确的平均执行时间。

In [563]: %timeit [x for x in strings if x.startswith('foo')]
10 loops, best of 3: 159 ms per loop
  
In [564]: %timeit [x for x in strings if x[:3] == 'foo']
10 loops, best of 3: 59.3 ms per loop

这个貌似平淡无奇的例子正好说明了一个事实:我们非常有必要了解Python标准库、NumPy、pandas以及本书中所用到的其他库的性能特点。在大型数据分析应用程序中,这些不起眼的毫秒数是会不断累积的!

对于那些执行时间非常短(甚至是那些微秒(1e-6秒)或纳秒(1e-9秒)级的)的分析语句和函数而言,%timeit是非常有用的。虽然这些时间值小到几乎可以忽略不计,但同样执行100万次一个20微秒的函数,所用的时间要比一个5微秒的多15秒。在上面那个例子中,我们可以直接对那两个字符串运算进行比较以了解其性能特点:

In [565]: x = 'foobar'

In [566]: y = 'foo'

In [567]: %timeit x.startswith(y)
1000000 loops, best of 3: 267 ns per loop

In [568]: %timeit x[:3] == y
10000000 loops, best of 3: 147 ns per loop
2.基本性能分析:%prun和%run -p

代码的性能分析跟代码执行时间密切相关,只不过它关注的是耗费时间的位置。主要的Python性能分析工具是cProfile模块,它不是专为IPython设计的。cProfile在执行一个程序或代码块时,会记录各函数所耗费的时间。

cProfile一般是在命令行上使用的,它将执行整个程序然后输出各函数的执行时间。假设我们有一个简单的脚本:在一个循环中执行一些线性代数计算(计算一个100×100的矩阵的最大本征值绝对值)。

import numpy as np
from numpy.linalg import eigvals

def run_experiment(niter=100):
    K = 100
    results = []
    for _ in xrange(niter):
        mat = np.random.randn(K, K)
        max_eigenvalue = np.abs(eigvals(mat)).max()
        results.append(max_eigenvalue)
    return results
some_results = run_experiment()
print 'Largest one we saw: %s' % np.max(some_results)

如果你还不懂NumPy,暂时先别管,后面会讲的。在命令行中输入下列命令即可通过cProfile启动该脚本:
python -m cProfile cprof_example.py

执行之后,你会发现输出结果是按函数名排序的。这让我们很难发现哪里才是最花时间的地方,因此通常都会再用-s标记指定一个排序规则:

$ python -m cProfile -s cumulative cprof_example.py
Largest one we saw: 11.923204422
     15116 function calls (14927 primitive calls) in 0.720 seconds

Ordered by: cumulative time

ncalls tottime percall cumtime percall filename:lineno(function)
1 0.001 0.001 0.721 0.721 cprof_example.py:1(<module>)
100 0.003 0.000 0.586 0.006 linalg.py:702(eigvals)
200 0.572 0.003 0.572 0.003 {numpy.linalg.lapack_lite.dgeev}
1 0.002 0.002 0.075 0.075 __init__.py:106(<module>)
100 0.059 0.001 0.059 0.001 {method 'randn')
1 0.000 0.000 0.044 0.044 add_newdocs.py:9(<module>)
2 0.001 0.001 0.037 0.019 __init__.py:1(<module>)
2 0.003 0.002 0.030 0.015 __init__.py:2(<module>)
1 0.000 0.000 0.030 0.030 type_check.py:3(<module>)
1 0.001 0.001 0.021 0.021 __init__.py:15(<module>)
1 0.013 0.013 0.013 0.013 numeric.py:1(<module>)
1 0.000 0.000 0.009 0.009 __init__.py:6(<module>)
1 0.001 0.001 0.008 0.008 __init__.py:45(<module>)
262 0.005 0.000 0.007 0.000 function_base.py:3178(add_newdoc)
100 0.003 0.000 0.005 0.000 linalg.py:162(_assertFinite)
...

这里只给出了输出结果中的前15行。只需查看cumtime列即可发现各函数所耗费的总时间。注意,如果一个函数调用了别的函数,计时器是不会停下来重新计时的。cProfile记录的是各函数调用的起始和结束时间,并依此计算总时间。

除命令行用法之外,cProfile还可以编程的方式分析任意代码块的性能。IPython为此提供了一个方便的接口,即%prun命令和带-p选项的%run。%prun的格式跟cProfile差不多,但它分析的是Python语句而不是整个.py文件:

In [4]: %prun -l 7 -s cumulative run_experiment()
         4203 function calls in 0.643 seconds

Ordered by: cumulative time
List reduced from 32 to 7 due to restriction <7>

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     1   0.000    0.000    0.643    0.643 <string>:1(<module>)
     1   0.001    0.001    0.643    0.643 cprof_example.py:4(run_experiment)
   100   0.003    0.000    0.583    0.006 linalg.py:702(eigvals)
   200   0.569    0.003    0.569    0.003 {numpy.linalg.lapack_lite.dgeev}
   100    0.058    0.001    0.058    0.001 {method 'randn'}
   100   0.003    0.000    0.005    0.000 linalg.py:162(_assertFinite)
   200    0.002    0.000    0.002    0.000 {method 'all' of 'numpy.ndarray' objects}

执行%run -p -s cumulative cprof_example.py也能达到上面那条系统命令行命令一样的效果,但是却无需退出IPython。

3.逐行分析函数性能

有些时候,从%prun(或其他基于cProfile的性能分析手段)得到的信息要么不足以说明函数的执行时间,要么就复杂到难以理解(按函数名聚合)。对于这种情况,我们可以使用一个叫做line_profiler的小型库(可以通过PyPI或随便一种包管理工具获取)。其中有一个新的魔术函数%lprun,它可以对一个或多个函数进行逐行性能分析。你可以修改IPython配置(参考IPython文件或本章稍后关于配置的内容)以启用这个扩展,代码如下所示:

# A list of dotted module names of IPython extensions to load.
c.TerminalIPythonApp.extensions = ['line_profiler']

line_profiler可以通过编程的方式使用(请参阅完整文档),但其最强大的一面却是在IPython中的交互式使用。假设你有一个prof_mod模块,其中有一些用于NumPy数组计算的代码,如下所示:

from numpy.random import randn

def add_and_sum(x, y):
    added = x + y
    summed = added.sum(axis=1)
    return summed

def call_function():
    x = randn(1000, 1000)
    y = randn(1000, 1000)
    return add_and_sum(x, y)

如果我们想了解add_and_sum函数的性能,%prun会给出如下所示的结果:

In [569]: %run prof_mod

In [570]: x = randn(3000, 3000)
In [571]: y = randn(3000, 3000)
In [572]: %prun add_and_sum(x, y)
        4 function calls in 0.049 seconds
Ordered by: internal time
ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     1  0.036   0.036  0.046 0.046 prof_mod.py:3(add_and_sum)
     1  0.009   0.009  0.009 0.009 {method 'sum' of 'numpy.ndarray' objects}
     1  0.003   0.003  0.049 0.049 <string>:1(<module>)
     1  0.000   0.000  0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}

这个结果并不能说明什么问题。启用line_profiler这个IPython扩展之后,就会出现一个新的魔术命令%lprun。用法上唯一的区别就是:必须为%lprun指明想要测试哪个或哪些函数。%lprun的通用语法为:
%lprun -f func1 -f func2 statement_to_profile

在本例中,我们想要测试的是add_and_sum,于是执行:

In [573]: %lprun -f add_and_sum add_and_sum(x, y)
Timer unit: 1e-06 s
File: book_scripts/prof_mod.py
Function: add_and_sum at line 3
Total time: 0.045936 s
Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
     3                                           def add_and_sum(x, y):
     4      1           36510  36510.0     79.5       added = x + y
     5      1            9425   9425.0     20.5       summed = added.sum(axis=1)
     6      1               1      1.0      0.0       return summed

这个结果就容易理解多了。这里我们测试的只是add_and_sum这一个函数。上面那个模块中还有一个call_function函数,我们可以结合add_and_sum一起测试,于是最终的测试命令就成了下面这个样子:

In [574]: %lprun -f add_and_sum -f call_function call_function()
Timer unit: 1e-06 s
File: book_scripts/prof_mod.py
Function: add_and_sum at line 3
Total time: 0.005526 s
Line # Hits Time  Per Hit % Time  Line Contents
==============================================================
    3                               def add_and_sum(x, y):
    4   1 4375 4375.0 79.2    added = x + y
    5   1 1149 1149.0 20.8    summed = added.sum(axis=1)
    6   1    2    2.0  0.0    return summed
File: book_scripts/prof_mod.py
Function: call_function at line 8
Total time: 0.121016 s
Line # Hits Time    Per Hit % Time  Line Contents
==============================================================
    8     def call_function():
    9  1 57169 57169.0  47.2     x = randn(1000, 1000)
   10  1 58304 58304.0  48.2     y = randn(1000, 1000)
   11  1  5543  5543.0   4.6     return add_and_sum(x, y)

通常,我会用%prun(cProfile)做“宏观的”性能分析,而用%lprun(line_profiler)做“微观的” 性能分析。这两个工具都很有必要了解一下。

注意: 在使用%lprun时,之所以必须显式指明待测试的函数名,是因为“跟踪”每一行代码的执行时间所需的开销很大。对不感兴趣的函数进行跟踪将会对性能分析结果造成显著的影响。

 

参考链接:
,

《 “IPython使用tips_2” 》 有 2 条评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注