第十六篇 更多关于IPython的系统

第二篇中,学习了IPython shell和Jupyter notebook的基础。本篇中,继续探索IPython更深层次的功能,可以从控制台或在jupyter使⽤。

一、使⽤命令历史
Ipython维护了⼀个位于磁盘的⼩型数据库,⽤于保存执⾏的每条指令。它的⽤途有:
             只⽤最少的输⼊,就能搜索、补全和执⾏先前运⾏过的指令;
             在不同session间保存命令历史;
             将⽇志输⼊/输出历史到⼀个⽂件
这些功能在shell中,要⽐notebook更为有⽤,因为notebook从设计上是将输⼊和输出的代码放到每个代码格⼦中。

1、搜索和重复使⽤命令历史
Ipython可以搜索和执⾏之前的代码或其他命令。这个功能⾮常有⽤,当需要重复执⾏同样的命令,例如%run命令,或其它代码。假设你必须要执⾏:
%run first/second/third/data_script.py

运⾏成功,检查结果,发现计算有错。解决完问题,修改data_script.py,就可以输⼊⼀些%run命令,然后按Ctrl+P或上箭头。这样就可以搜索历史命令,匹配输⼊字符的命令。多次按Ctrl+P或上箭头,会继续搜索命令。如果你要执⾏你想要执⾏的命令。可以按下Ctrl-N或下箭头,向前移动历史命令。这样做了⼏次后,你可以不假思索地按下这些键!

Ctrl-R可以带来如同Unix⻛格shell(⽐如bash shell)的readline的部分增量搜索功能。在Windows上,readline功能是被IPython模仿的。要使⽤这个功能,先按Ctrl-R,然后输⼊⼀些包含于输⼊⾏的想要搜索的字符:
nb_add.accumulate(x, axis=0)
I-search backward: nb_add           # 输入nb_add搜索历史命令
Ctrl-R会循环历史,找到匹配字符的每⼀⾏

1、输⼊和输出变量
忘记将函数调⽤的结果分配给变量是⾮常烦⼈的。IPython的⼀个session会在⼀个特殊变量,存储输⼊和输出Python对象的引⽤。前⾯两个输出会分别存储在 _(⼀个下划线)和 __(两个下划线)变量
import math
math.pi * 3 ** 2        # 输出:28.2743
_           # 一个下划线,输出上次命令的结果 :28.2743

输⼊变量是存储在名字类似 _iX 的变量中,X是输⼊⾏的编号。对于每个输⼊变量,都有⼀个对应的输出变量_X。因此在输⼊第185⾏之后,会有两个新变量_185 (输出)和_i185(输⼊):
In [184]: foo = 'bar'

In [185]: foo
Out[185]: 'bar'

In [186]: _i185         # 输入变量
Out[186]: 'foo'

In [187]: _185          # 输出变量
Out[187]: 'bar'

因为输⼊变量是字符串,它们可以⽤Python的exec关键字再次执⾏
In [188]: exec(_i185)
这⾥,_i185是在In [185]输⼊的代码。

有⼏个魔术函数可以让你利⽤输⼊和输出历史。%hist可以打印所有或部分的输⼊历史,加上或不加上编号。%reset可以清理交互命名空间,或输⼊和输出缓存。%xdel 魔术函数可以去除IPython中对⼀个特别对象的所有引⽤。对于关于这些魔术⽅法的更多内容,查看⽂档。

注意:当处理⾮常⼤的数据集时,要记住IPython的输⼊和输出的历史会造成被引⽤的对象不被垃圾回收(释放内存),即使你使⽤del关键字从交互命名空间删除变量。在这种情况下,小心使⽤%xdel和%reset可以帮助你避免陷⼊内存问题。

二、与操作系统交互
IPython的另⼀个功能是⽆缝连接⽂件系统和操作系统。这意味着,在同时做其它事时,⽆需退出IPython,就可以像Windows或Unix使⽤命令⾏操作,包括shell命令、更改⽬录、⽤Python对象(列表或字符串)存储结果。它还有简单的命令别名和⽬录书签功能。

表16-1总结了调⽤shell命令的魔术函数和语法。后面会介绍这些功能。
表16-1  IPython系统相关命令
命令                                  说明
!cmd                                 在系统shell执行cmd
output = !cmd args          运行run,在output存储stdout
%alias alias_name cmd    为一个系统命令定义别名
%bookmark                      使用IPython的目录书签系统
%cd directory                   改变系统的工作目录到传入的目录
%pwd                                返回当前的系统工作目录
%pushd directory             将当前目录放在堆栈上,并将其更改为目标目录
%popd                              切换到从堆栈顶部弹出的目录
%dirs                                 返回包含当前目录的堆栈列表
%dhist                               打印访问过的目录
%env                                 返回系统的环境变量的字典
%matplotlib                      配置matplotlib的选项

1、Shell命令和别名
⽤叹号开始⼀⾏,是告诉IPython执⾏叹号后⾯的所有内容。这意味着你可以删除⽂件(取决于操作系统,⽤rm或del)、改变⽬录或执⾏任何其他命令。

通过给变量加上叹号,你可以在⼀个变量中存储命令的控制台输出。例如,在联⽹的基于Linux的主机上,可以获得IP地址为Python变量:
ip_info = !ifconfig wlan0 | grep "inet "
ip_info[0].strip()      # 输出如下:
'inet addr:10.0.0.11 Bcast:10.0.0.255 Mask:255.255.255.0'

返回的Python对象ip_info实际上是⼀个⾃定义的列表类型,它包含着多种版本的控制台输出。

当使⽤ (!) ,IPython还可以替换定义在当前环境的Python值。要这么做,可以在变量名前⾯加上$符号
foo = 'test*'
!ls $foo    # windows下可用 !dir $foo,输出如下:
test4.py test.py test.xml

%alias魔术函数可以⾃定义shell命令的快捷⽅式。看⼀个简单的例⼦:
%alias ll ls -l         # windows可用:%alias ll dir
ll /usr     # 输出内容为/usr目录下的文件,输出省略

你可以执⾏多个命令,就像在命令⾏中⼀样,只需⽤分号隔开:
%alias test_alias (cd examples; ls; cd ..)      # windows不支持这样玩
test_alias              # 输出如下:
macrodata.csv spx.csv tips.csv

当session结束,你定义的别名就会失效。要创建恒久的别名,需要使⽤配置。

2、⽬录书签系统
IPython有⼀个简单的⽬录书签系统,可以保存常⽤⽬录的别名,这样在跳来跳去的时候会⾮常⽅便。例如,假设你想创建⼀个书签,指向特定的位置:
%bookmark py E:\\python
这么做之后,当使⽤%cd魔术命令,就可以使⽤定义的书签:
cd py       # 输出如下所示,此时工作目录已改变
(bookmark:py) -> E:\\python
E:\python

如果书签的名字,与当前⼯作⽬录的⼀个⽬录重名,你可以使⽤-b标志来覆写,使⽤书签的位置。使⽤
%bookmark的-l选项,可以列出所有的书签:
%bookmark -l             # 输出如下:
Current bookmarks:
py -> E:\\python

书签,和别名不同,在session之间是保持的。

三、软件开发⼯具
IPython是优秀的交互式计算和数据探索环境,也是有效的Python软件开发⼯具。在数据分析中,最重要的是要有正确的代码。第一,IPython紧密集成和加强了Python内置的pdb调试器。第⼆,需要快速的代码。对于这点,IPython有易于使⽤的代码计时和分析⼯具。下面介绍这些⼯具。

1、交互调试器
IPython的调试器⽤tab补全、语法增强、逐⾏异常追踪增强了pdb。调试代码的最佳时间就是刚刚发⽣错误。异常发⽣之后就输⼊%debug,就启动了调试器,进⼊抛出异常的堆栈框架:
run examples/ipython_bug.py         # 输出如下错误信息
---------------------------------------------------------------------------
AssertionError Traceback (most recent call last)
/home/wesm/code/pydata-book/examples/ipython_bug.py in <module>()
         13     throws_an_exception()
         14
--->  15     calling_things()
/home/wesm/code/pydata-book/examples/ipython_bug.py in calling_things()
         11     def calling_things():
         12     works_fine()
--->  13     throws_an_exception()
         14
         15     calling_things()
/home/wesm/code/pydata-book/examples/ipython_bug.py in throws_an_exception()
          7      a = 5
          8      b = 6
---->  9     assert(a + b == 10)
        10
        11     def calling_things():
AssertionError:

%debug      # 进入调式器
> /home/wesm/code/pydata-book/examples/ipython_bug.py(9)throws_an_exception()
          8    b = 6
----> 9    assert(a + b == 10)
        10

ipdb>

⼀旦进⼊调试器,就可以执⾏任意的Python代码,在每个堆栈框架中检查所有的对象和数据(解释器会保持它们活跃)。默认是从错误发⽣的最低级开始。通过u(up)和d(down),你可以在不同等级的堆栈踪迹切换:
ipdb> u
> /home/wesm/code/pydata-book/examples/ipython_bug.py(13)calling_things()
         12    works_fine()
--->  13    throws_an_exception()
         14

执⾏%pdb命令,可以在发⽣任何异常时让IPython⾃动启动调试器,这个功能⾮常好⽤。

⽤调试器帮助开发代码也很容易,特别是当你希望设置断点或在函数和脚本间移动,以检查每个阶段的状态。有多种⽅法可以实现。第⼀种是使⽤%run和-d,它会在执⾏传⼊脚本的任何代码之前调⽤调试器。你必须⻢上按s(step)以进⼊脚本
run -d examples/ipython_bug.py
Breakpoint 1 at /home/wesm/code/pydata-book/examples/ipython_bug.py:1
NOTE: Enter 'c' at the ipdb> prompt to start your script.
> <string>(1)<module>()

ipdb> s
--Call--
> /home/wesm/code/pydata-book/examples/ipython_bug.py(1)<module>()
1--->  1   def works_fine():
           2   a = 5
           3   b = 6

这时,你可以step进⼊works_fine(),或通过按n(next)执⾏works_fine(),进⼊下⼀⾏:
ipdb> n
> /home/wesm/code/pydata-book/examples/ipython_bug.py(13)calling_things()
2      12      works_fine()
---> 13      throws_an_exception()
        14

然后,我们可以进⼊throws_an_exception,到达发⽣错误的⼀⾏,查看变量。注意,调试器的命令是在变量名之前,在变量名前⾯加叹号!可以查看内容
ipdb> s
--Call--
> /home/wesm/code/pydata-book/examples/ipython_bug.py(6)throws_an_exception()
          5
---->  6     def throws_an_exception():
          7      a = 5

ipdb> n
> /home/wesm/code/pydata-book/examples/ipython_bug.py(7)throws_an_exception()
          6     def throws_an_exception():
---->  7     a = 5
          8     b = 6

ipdb> n
> /home/wesm/code/pydata-book/examples/ipython_bug.py(8)throws_an_exception()
          7      a = 5
---->  8      b = 6
          9       assert(a + b == 10)

ipdb> n
> /home/wesm/code/pydata-book/examples/ipython_bug.py(9)throws_an_exception()
          8       b = 6
---->  9       assert(a + b == 10)
        10

ipdb> !a
5
ipdb> !b
6

提⾼使⽤交互式调试器的熟练度需要练习和经验。表16-2,列出了所有调试器命令。如果习惯了IDE,你可能觉得终端的调试器在⼀开始会不顺⼿,但会觉得越来越好⽤。⼀些Python的IDEs有很好的GUI调试器,选择顺⼿的就好。

表16-2  IPython调试器命令
命令                                        动作
h(elp)                                     展示命令列表
help command                      展示command的文档
c(ontinue)                              继续执行程序
q(quit)                                   退出调试器,不执行更多代码
b(reak) number                     在当前文件的第number行设置断点
b path/to/file.py:number      在指定文件的第number行设置断点
s(tep)                                     进入函数调用
n(ext)                                     执行当前行,并前进到同级的下一行
u(p)/d(own)                           在函数调用栈向上或向下
a(rgs)                                      展示当前函数的参数
debug statement                   在新的(递归)调试器执行命令 statement
l(ist) statement                      在当前栈的级别,展示当前位置和上下文
w(here)                                   打印完整的栈踪迹和当前位置的上下文

2、使⽤调试器的其它⽅式
还有⼀些其它⼯作可以⽤到调试器。第⼀个是使⽤特殊的set_trace函数(根据pdb.set_trace命名的),这是⼀个简装的断点。还有两种⽅法是你可能想⽤的(将其添加到IPython的配置):
下面的set_trace()函数放在需要进行调式的代码文件中。
import sys
from IPython.core.debugger import Pdb
def set_trace():
       Pdb(color_scheme='Linux').set_trace(sys._getframe().f_back)

def debug(f, *args, **kwargs):
       pdb = Pdb(color_scheme='Linux')
       return pdb.runcall(f, *args, **kwargs)

第⼀个函数set_trace⾮常简单。如果你想暂时停下来进⾏仔细检查(⽐如发⽣异常之前),可以在代码的任何位置使⽤set_trace:
run examples/ipython_bug.py         # 输出如下:
> /home/wesm/code/pydata-book/examples/ipython_bug.py(16)calling_things()
         15 set_trace()
---> 16 throws_an_exception()
         17
c(continue)可以让代码继续正常⾏进。

刚看的debug函数,可以很⽅便的在调⽤任何函数时使⽤调试器。假设写了⼀个下⾯的函数,想逐步分析它的逻辑:
def f(x, y, z=1):
       tmp = x + y
       return tmp / z

普通地使⽤f,就会像f(1, 2, z=3)。⽽要想进⼊f,将f作为第⼀个参数传递给debug,再将位置和关键词参数传递给f:
debug(f, 1, 2, z=3)     # 输出如下:
> <ipython-input-41-359ec13d6433>(2)f()
       1 def f(x, y, z=1):
---->  2     tmp = x + y
       3     return tmp / z

ipdb>

这两个简单⽅法平时可节省⼤量时间。

最后,调试器可以和%run⼀起使⽤。脚本通过运⾏%run -d,就可以直接进⼊调试器,随意设置断点并启动脚本:
%run -d examples/ipython_bug.py     # 输出如下:
Breakpoint 1 at /home/wesm/code/pydata-book/examples/ipython_bug.py:1
NOTE: Enter 'c' at the ipdb> prompt to start your script.
> <string>(1)<module>()
ipdb>

加上-b和⾏号,可以预设⼀个断点
%run -d -b2 examples/ipython_bug.py             # 输出如下:
Breakpoint 1 at /home/wesm/code/pydata-book/examples/ipython_bug.py:2
NOTE: Enter 'c' at the ipdb> prompt to start your script.
> <string>(1)<module>()
ipdb> c
> /home/wesm/code/pydata-book/examples/ipython_bug.py(2)works_fine()
           1    def works_fine():
1--->  2    a = 5
           3    b = 6

3、代码计时:%time 和 %timeit
对于⼤型和⻓时间运⾏的数据分析应⽤,如要测试不同组件或单独函数调⽤语句的执⾏时间。想要知道哪个函数占⽤的时间最⻓。IPython可以在开发和测试代码时,很容易地获得这些信息。

IPython有两个魔术函数,%time和%timeit,可以⾃动化这个过程。

%time会运⾏⼀次语句,报告总共的执⾏时间。假设有⼀个⼤的字符串列表,我们想⽐较不同的可以挑选出特定开头字符串的⽅法。这⾥有⼀个含有600000字符串的列表,和两个⽅法,⽤以选出foo开头的字符串:
# a very large list of strings
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进⾏⼀下测量:
%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

%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(wall-clock time的简写)是主要关注的。第⼀个⽅法是第⼆个⽅法的两倍多,但是这种测量⽅法并不准确。如果⽤%time多次测量,你就会发现结果是变化的。要想更准确,可以使⽤%timeit魔术函数。给出任意⼀条语句,它能多次运⾏这条语句以得到⼀个更为准确的时间:
%timeit [x for x in strings if x.startswith('foo')]
104 ms ± 6.43 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

%timeit [x for x in strings if x[:3] == 'foo']
58.5 ms ± 553 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

这个例⼦说明了解Python标准库、NumPy、pandas和其它库的性能是很有价值的。在⼤型数据分析中,这些毫秒的时间就会累积起来!

%timeit特别适合分析执⾏时间短的语句和函数,即使是微秒或纳秒。这些时间可能看起来毫不重要,但是⼀个20微秒的函数执⾏1百万次就⽐⼀个5微秒的函数⻓15秒。在上⼀个例⼦中,我们可以直接⽐较两个字符串操作,以了解它们的性能特点:
x = 'foobar'
  y = 'foo'
%timeit x.startswith(y)             # 输出如下:
178 ns ± 25.6 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

%timeit x[:3] == y                  # 输出如下:
119 ns ± 6.13 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

4、基础分析:%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 range(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))

可以⽤cProfile运⾏这个脚本,使⽤下⾯的命令⾏:
python -m cProfile cprof_example.py
运⾏之后,你会发现输出是按函数名排序的。这样要看出谁耗费的时间多有点困难,最好⽤-s指定排序:
python -m cProfile -s cumulative cprof_example.py | more
Largest one we saw: 11.357900631625398
          54277 function calls (52012 primitive calls) in 0.192 seconds
    Ordered by: cumulative time
    ncalls  tottime  percall  cumtime  percall filename:lineno(function)
         6    0.000    0.000    0.238    0.040 __init__.py:1(<module>)
     135/1    0.004    0.000    0.192    0.192 {built-in method builtins.exec}
         1    0.000    0.000    0.192    0.192 cprof_example.py:3(<module>)
     162/1    0.001    0.000    0.171    0.171 <frozen importlib._bootstrap>:966(_find_and_load)
     162/1    0.001    0.000    0.171    0.171 <frozen importlib._bootstrap>:936(_find_and_load_unlocked)
     150/1    0.001    0.000    0.171    0.171 <frozen importlib._bootstrap>:651(_load_unlocked)
     125/1    0.001    0.000    0.171    0.171 <frozen importlib._bootstrap_external>:672(exec_module)
     220/1    0.000    0.000    0.170    0.170 <frozen importlib._bootstrap>:211(_call_with_frames_removed)
         1    0.000    0.000    0.170    0.170 __init__.py:106(<module>)
    798/29    0.001    0.000    0.165    0.006 <frozen importlib._bootstrap>:997(_handle_fromlist)
     323/7    0.000    0.000    0.164    0.023 {built-in method builtins.__import__}
         1    0.000    0.000    0.128    0.128 add_newdocs.py:10(<module>)
         1    0.000    0.000    0.098    0.098 type_check.py:3(<module>)
       157    0.001    0.000    0.053    0.000 <frozen importlib._bootstrap>:870(_find_spec)
       146    0.000    0.000    0.051    0.000 <frozen importlib._bootstrap_external>:1149(find_spec)
       146    0.001    0.000    0.051    0.000 <frozen importlib._bootstrap_external>:1117(_get_spec)
       125    0.001    0.000    0.050    0.000 <frozen importlib._bootstrap_external>:743(get_code)
         1    0.000    0.000    0.049    0.049 __init__.py:7(<module>)
         .......

上面只显示出前18⾏。扫描cumtime列,可以容易地看出每个函数⽤了多少时间。如果⼀个函数调⽤了其它函数,计时并不会停⽌。cProfile会记录每个函数的起始和结束时间,使⽤它们进⾏计时。

除了在命令⾏中使⽤,cProfile也可以在程序中使⽤,分析任意代码块,⽽不必运⾏新进程。Ipython的%prun和%run -p,有便捷的接⼝实现这个功能。%prun使⽤类似cProfile的命令⾏选项,但是可以分析任意Python语句,⽽不⽤整个py⽂件:
%prun -l 7 -s cumulative run_experiment()       # 输出如下:
          42 function calls in 0.022 seconds
    Ordered by: cumulative time
    List reduced from 31 to 7 due to restriction <7>
    ncalls  tottime  percall  cumtime  percall filename:lineno(function)
         1    0.000    0.000    0.022    0.022 {built-in method builtins.exec}
         1    0.000    0.000    0.022    0.022 <string>:1(<module>)
         1    0.000    0.000    0.022    0.022 <ipython-input-35-587cda3d889f>:4(run_experiment)
         1    0.021    0.021    0.021    0.021 linalg.py:834(eigvals)
         1    0.000    0.000    0.000    0.000 {method 'randn' of 'mtrand.RandomState' objects}
         1    0.000    0.000    0.000    0.000 linalg.py:213(_assertFinite)
         3    0.000    0.000    0.000    0.000 {method 'reduce' of 'numpy.ufunc' objects}

相似的,调⽤%run -p -s cumulative cprof_example.py有和命令⾏相似的作⽤,只是你不⽤离开Ipython。

在Jupyter notebook中,可以使⽤%%prun魔术⽅法(两个%)来分析⼀整段代码。这会弹出⼀个带有分析输出的独⽴窗⼝。便于快速回答⼀些问题,⽐如“为什么这段代码⽤了这么⻓时间”?

使⽤IPython或Jupyter,还有⼀些其它⼯具可以让分析⼯作更便于理解。其中之⼀是SnakeVizhttps://github.com/jiffyclub/snakeviz/),它会使⽤d3.js产⽣⼀个分析结果的交互可视化界⾯。

5、逐⾏分析函数
有些情况下,⽤%prun(或其它基于cProfile的分析⽅法)得到的信息,不能获得函数执⾏时间的整个过程,或者结果过于复杂,加上函数名,很难进⾏解读。对于这种情况,有⼀个⼩库叫做line_profiler(可以通过PyPI或包管理⼯具获得)。它包含IPython插件,可以启⽤⼀个新的魔术函数%lprun,可以对⼀个函数或多个函数进⾏逐⾏分析。你可以通过修改IPython配置(查看IPython⽂档或后⾯的配置⼩节)加⼊下⾯这⾏,启⽤这个插件:
# A list of dotted module names of IPython extensions to load.
c.TerminalIPythonApp.extensions = ['line_profiler']

还可以运⾏命令:
%load_ext 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可以给出下⾯内容:
%run prof_mod
x = randn(3000, 3000)
y = randn(3000, 3000)
 
%prun add_and_sum(x, y)             # 输出如下:
          7 function calls in 0.086 seconds
    Ordered by: internal time
    ncalls  tottime  percall  cumtime  percall filename:lineno(function)
         1    0.056    0.056    0.069    0.069 <ipython-input-38-0355268a1452>:2(add_and_sum)
         1    0.017    0.017    0.086    0.086 <string>:1(<module>)
         1    0.013    0.013    0.013    0.013 {method 'reduce' of 'numpy.ufunc' objects}
         1    0.000    0.000    0.086    0.086 {built-in method builtins.exec}
         1    0.000    0.000    0.013    0.013 {method 'sum' of 'numpy.ndarray' objects}
         1    0.000    0.000    0.013    0.013 _methods.py:31(_sum)
         1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

上⾯的做法启发性不⼤。激活了IPython插件line_profiler,新的命令%lprun就能⽤了。使⽤中的不同点是,必须告诉%lprun要分析的函数是哪个。语法是:
%lprun -f func1 -f func2 statement_to_profile

我们想分析add_and_sum,运⾏:
%lprun -f add_and_sum add_and_sum(x, y)         # 输出如下:
Timer unit: 1e-06 s
File: 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

这样就容易诠释。我们分析了和代码语句中⼀样的函数。看之前的模块代码,我们可以调⽤call_function并对它和add_and_sum进⾏分析,得到⼀个完整的代码性能概括:
%lprun -f add_and_sum -f call_function call_function()
Timer unit: 1e-06 s
File: 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: 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⾼效开发的技巧
⽅便快捷地写代码、调试和使⽤是每个⼈的⽬标。除了代码⻛格,流程细节(⽐如代码重载)也需要⼀些调整。

⽤IPython设计的软件⽐起命令⾏,要更适合⼯作。尤其是当发⽣错误时,你需要检查⾃⼰或别⼈写的数⽉或数年前写的代码的错误。

1、重载模块依赖
在Python中,当你输⼊import some_lib,some_lib中的代码就会被执⾏,所有的变量、函数和定义的引⼊,就会被存⼊到新创建的some_lib模块命名空间。当下⼀次输⼊some_lib,就会得到⼀个已存在的模块命名空间的引⽤。潜在的问题是当你%run⼀个脚本,它依赖于另⼀个模块,⽽这个模块做过修改,就会产⽣问题。假设我在test_script.py中有如下代码:
import some_lib
x = 5
y = [1, 2, 3, 4]
result = some_lib.get_answer(x, y)

如果你运⾏过了%run test_script.py,然后修改了some_lib.py,下⼀次再执⾏%run test_script.py,还会得到旧版本的some_lib.py,这是因为Python模块系统的“⼀次加载”机制。这⼀点区分了Python和其它数据分析环境,⽐如MATLAB,它会⾃动传播代码修改。解决这个问题,有多种⽅法。第⼀种是在标准库importlib模块中使⽤reload函数
import some_lib
import importlib
importlib.reload(some_lib)

这可以保证每次运⾏test_script.py时可以加载最新的some_lib.py。很明显,如果依赖更深,在各处都使⽤reload是⾮常麻烦的。对于这个问题,IPython有⼀个特殊的dreload函数(它不是魔术函数)重载深层的模块。如果我运⾏过some_lib.py,然后输⼊dreload(some_lib),就会尝试重载some_lib和它的依赖。不过,这个⽅法不适⽤于所有场景,但⽐重启IPython强多了。

2、代码设计技巧
a、保持相关对象和数据活跃
为命令⾏写⼀个下⾯示例中的程序是很少⻅的:
from my_functions import g
def f(x, y):
     return g(x + y)
def main():
     x = 6
     y = 7.5
     result = x + y
if __name__ == '__main__':
     main()

在IPython中运⾏这个程序会发⽣问题。运⾏之后,任何定义在main函数中的结果和对象都不能在IPython中被访问到。更好的方法是将main中的代码直接在模块的命名空间中执⾏(或者在__name__ == '__main__':中,如果你想让这个模块可以被引⽤)。这样,当你%rundiamante,就可以查看所有定义在main中的变量。这等价于在Jupyter notebook的代码格中定义⼀个顶级变量。

b、扁平优于嵌套
“扁平优于嵌套”是Python之禅的⼀部分,它也适⽤于交互式代码开发。尽量将函数和类去耦合和模块化,有利于测试(如果你是在写单元测试)、调试和交互式使⽤。

c、克服对⼤⽂件的恐惧
维护⼤模块,每个模块都是紧密组织的,会更实⽤和Pythonic。经过⽅案迭代,有时会将⼤⽂件分解成⼩⽂件。

找到⼀个合理和直观的⼤型代码模块库和封装结构往往需要⼀点⼯作,但这在团队⼯作中⾮常重要。每个模块都应该结构紧密,并且应该能直观地找到负责每个功能领域功能和类。

五、IPython⾼级功能
要全⾯地使⽤IPython系统需要⽤另⼀种稍微不同的⽅式写代码,或深⼊IPython的配置。

1、让类是对IPython友好的
IPython会尽可能地在控制台美化展示每个字符串。对于许多对象,⽐如字典、列表和元组,内置的pprint模块可以⽤来美化格式。但是,在⽤户定义的类中,你必须⾃⼰⽣成字符串。假设有⼀个下⾯的简单的类:
class Message:
     def __init__(self, msg):
         self.msg = msgg

如果这么写,就会发现默认的输出不够美观:
x = Message('hello world')
x           # 输出:<__main__.Message at 0x24f07033a58>

IPython会接收repr魔术⽅法返回的字符串(通过output =repr(obj)),并在控制台打印出来。因此,我们可以添加⼀个简单的repr⽅法到前⾯的类中,以得到⼀个更有⽤的输出:
class Message:
     def __init__(self, msg):
         self.msg = msg

    def __repr__(self):
         return 'Message: %s' % self.msg

x = Message('hello python')
x           # 输出:Message: hello python

2、⽂件和配置
通过扩展配置系统,⼤多数IPython和Jupyter notebook的外观(颜⾊、提示符、⾏间距等等)和动作都是可以配
置的。通过配置,你可以做到:
             改变颜⾊主题
             改变输⼊和输出提示符,或删除输出之后、输⼊之前的空⾏
             执⾏任意Python语句(例如,引⼊总是要使⽤的代码或者每次加载IPython都要运⾏的内容)
             启⽤IPython总是要运⾏的插件,⽐如line_profiler中的%lprun魔术函数
             启⽤Jupyter插件
             定义⾃⼰的魔术函数或系统别名
            
IPython的配置存储在特殊的ipython_config.py⽂件中,它通常是在⽤户home⽬录的.ipython/⽂件夹中。配置是通过⼀个特殊⽂件。当你启动IPython,就会默认加载这个存储在profile_default⽂件夹中的默认⽂件。因此,在Linux系统中,完整的IPython配置⽂件路径是:
/home/wesm/.ipython/profile_default/ipython_config.py

要启动这个⽂件,运⾏下⾯的命令:
ipython profile create

这个⽂件中的内容不再展示,有兴趣可自行查看。这个⽂件有注释,解释了每个配置选项的作⽤。另⼀点,可以有多个配置⽂件。假设你想要另⼀个IPython配置⽂件,专⻔是为另⼀个应⽤或项⽬的。创建⼀个新的配置⽂件很简单,如下所示:
ipython profile create secret_project

做完之后,在新创建的profile_secret_project⽬录便可配置⽂件,然后如下启动IPython:
ipython --profile=secret_project
Python 3.5.1 | packaged by conda-forge | (default, May 20 2016, 05:22:56)
Type "copyright", "credits" or "license" for more information.
IPython 5.1.0 -- An enhanced Interactive Python.
?                -> Introduction and overview of IPython's features.
%quickref -> Quick reference.
help          -> Python's own help system.
object?     -> Details about 'object', use 'object??' for extra details.
IPython profile: secret_project

和之前⼀样,IPython的⽂档是⼀个极好的学习配置⽂件的资源。

配置Jupyter有些不同,因为你可以使⽤除了Python的其它语⾔。要创建⼀个类似的Jupyter配置⽂件,运⾏:
jupyter notebook --generate-config

这样会在home⽬录的.jupyter/jupyter_notebook_config.py创建配置⽂件。编辑完之后,可以将它重命名:
mv ~/.jupyter/jupyter_notebook_config.py ~/.jupyter/my_custom_config.py

打开Jupyter之后,你可以添加--config参数:
jupyter notebook --config=~/.jupyter/my_custom_config.py

六、总结
建议持续学习IPython和Jupyter。因为这两个项⽬的设计初衷就是提⾼⽣产率的,你可能还会发现⼀些⼯具,可以让你更便捷地使⽤Python和计算库。

你可以在nbviewer(https://nbviewer.jupyter.org/)上找到更多有趣的Jupyter notebooks。

posted @ 2019-01-14 15:50  远方那一抹云  阅读(420)  评论(0编辑  收藏  举报