Python科学计算(第2版)
上QQ阅读APP看书,第一时间看更新

1.2.2 魔法(Magic)命令

IPython提供了许多魔法命令,使得在IPython环境中的操作更加得心应手。魔法命令都以%或%%开头,以%开头的为行命令,以%%开头的为单元命令。行命令只对命令所在的行有效,而单元命令则必须出现在单元的第一行,对整个单元的代码进行处理。

执行%magic可以查看关于各个命令的说明,而在命令之后添加?可以查看命令的详细说明。此外扩展库可以提供自己的魔法命令,这些命令可以通过%load_ext载入。例如%load_ext cython载入%%cython命令,以该命令开头的单元将调用Cython编译其中的代码。

1.显示matplotlib图表

matplotlib是Python世界中最著名的绘图扩展库,支持输出多种格式的图形图像,并且可以使用多种GUI界面库交互式地显示图表。使用%matplotlib命令可以将matplotlib的图表直接嵌入到Notebook中,或者使用指定的界面库显示图表,它有一个参数指定matplotlib图表的显示方式。

在下面的例子中,inline表示将图表嵌入到Notebook中。因此由最后一行pl.plot()创建的图表将直接显示在该单元之下:

    %matplotlib inline
    import pylab as pl
    pl.seed(1)
    data = pl.randn(100)
    pl.plot(data)

内嵌图表的输出格式默认为PNG,可以通过%config命令修改这个配置。%config命令可以配置IPython中的各可配置对象,其中InlineBackend对象为matplotlib输出内嵌图表时所使用的配置,我们配置它的figure_format="svg",这样可将内嵌图表的输出格式修改为SVG。

    %config InlineBackend.figure_format="svg"
    pl.plot(data)

内嵌图表很适合制作图文并茂的Notebook,然而它们是静态的,无法进行交互。可以将图表输出模式修改为使用GUI界面库,下面的qt4表示使用QT4界面库显示图表。请读者根据自己系统的配置,选择合适的界面库:gtk、osx、qt、qt4、tk、wx。

执行下面的语句将弹出一个窗口显示图表,可以通过鼠标和键盘与此图表交互。请注意该功能只能在运行IPython Kernel的机器上显示图表。

    %matplotlib qt4
    pl.plot(data)

2.性能分析

性能分析对编写处理大量数据的程序非常重要,特别是Python这样的动态语言,一条语句可能会执行很多内容,有的是动态的,有的调用扩展库。不做性能分析,就无法对程序进行优化。IPython提供了性能分析的许多魔法命令。

%timeit调用timeit模块对单行语句重复执行多次,计算出执行时间。下面的代码测试修改列表的单个元素所需的时间:

    a = [1,2,3]
    %timeit a[1] = 10
    10000000 loops, best of 3: 69.3 ns per loop

%%timeit则用于测试整个单元中代码的执行时间。下面的代码测试空列表中循环添加10个元素所需的时间:

    %%timeit
    a = []
    for i in xrange(10):
        a.append(i)
    1000000 loops, best of 3: 1.82 µs per loop

timeit命令会重复执行代码多次,而time则只执行一次代码,输出代码的执行情况。和timeit命令一样,time可以作为行命令和单元命令。下面的代码统计往空列表中添加10万个元素所需的时间:

    %%time
    a = []
    for i in xrange(100000):
        a.append(i)
    Wall time: 18 ms

time和timeit命令都使用print输出信息,如果希望用程序分析这些信息,可以使用%%capture命令,将单元格的输出保存为一个对象。下面的程序对不同长度的列表调用random.shuffle()以打乱顺序,用%time记录下shuffle()的运行时间:

    %%capture time_results
    import random
    for n in [1000, 5000, 10000, 50000, 100000, 500000]:
        print "n={0}".format(n)
        alist = range(n)
        %time random.shuffle(alist)

time_results.stdout属性保存标准输出管道中的输出信息:

    print time_results.stdout
    n=1000
    Wall time: 1 ms
    n=5000
    Wall time: 5 ms
    n=10000
    Wall time: 10 ms
    n=50000
    Wall time: 40 ms
    n=100000
    Wall time: 62 ms
    n=500000
    Wall time: 400 ms

如果在调用%timeit命令时添加-o参数,则返回一个表示运行时间信息的对象。下面的程序对不同长度的列表调用sorted()排序,并使用%timeit命令统计排序所需的时间:

    timeit_results = []
    for n in [5000, 10000, 20000, 40000, 80000, 160000, 320000]:
        alist = [random.random() for i in xrange(n)]
        res = %timeit -o sorted(alist)
        timeit_results.append((n, res))
    1000 loops, best of 3: 1.56 ms per loop
    100 loops, best of 3: 3.32 ms per loop
    100 loops, best of 3: 7.57 ms per loop
    100 loops, best of 3: 16.4 ms per loop
    10 loops, best of 3: 35.8 ms per loop
    10 loops, best of 3: 81 ms per loop
    10 loops, best of 3: 185 ms per loop

图1-9显示了排序的耗时结果。横坐标为对数坐标轴,表示数组的长度;纵坐标为平均每个元素所需的排序时间。可以看出每个元素所需的平均排序时间与数组长度的对数成正比,因此可以计算出排序函数sorted()的时间复杂度为:O(nlogn)。

图1-9 sorted()函数的时间复杂度

%%prun命令调用profile模块,对单元中的代码进行性能剖析。下面的性能剖析显示fib()运行了21891次,而fib_fast()则只运行了20次:

3.代码调试

%debug命令用于调试代码,它有两种用法:一种是在执行代码之前设置断点进行调试;另一种则是在代码抛出异常之后,执行%debug命令查看调用堆栈。下面先演示第二种用法:

    import math
    
    def sinc(x):
        return math.sin(x) / x
    
    [sinc(x) for x in range(5)]
    ---------------------------------------------------------------------------
    ZeroDivisionError                         Traceback (most recent call last)
    <ipython-input-28-9b69eaad97fe> in <module>()
          4     return math.sin(x) / x
          5 
    ----> 6 [sinc(x) for x in range(5)]
    
    <ipython-input-28-9b69eaad97fe> in sinc(x)
          2 
          3 def sinc(x):
    ----> 4     return math.sin(x) / x
          5 
          6 [sinc(x) for x in range(5)]
    
    ZeroDivisionError: float division by zero

上面的程序抛出了ZeroDivisionError异常,下面用%debug查看调用堆栈。在调试模式下可以使用pdb模块提供的调试命令,例如用命令p x显示变量x的值:

    %debug
    ><ipython-input-28-9b69eaad97fe>(4)sinc()
          3 def sinc(x):
    ----> 4     return math.sin(x) / x
          5 
    
    ipdb> p x
    0
    ipdb> q

还可以先设置断点,然后运行程序。但是%debug的断点需要指定文件名和行号,使用起来并不是太方便。本书提供了%%func_debug单元命令,可以通过它指定中断运行的函数。在下面的例子中,程序将在numpy.unique()的第一行中断运行,然后通过输入命令n单步运行程序,最后输入命令c继续运行:

    %%func_debug np.unique
    np.unique([1, 2, 5, 4, 2])
    Breakpoint 1 at c:\winpython-32bit-2.7.9.2\python-2.7.9\lib\site-packages\numpy\lib\arraysetops.py:96
    NOTE: Enter 'c' at the ipdb>  prompt to continue execution.
    > c:\winpython-32bit-2.7.9.2\python-2.7.9\lib\site-packages\numpy\lib\arraysetops.py(173)
unique()
        172     """
    --> 173     ar = np.asanyarray(ar).flatten()
        174 
    
    ipdb> n
    >c:\winpython-32bit-2.7.9.2\python-2.7.9\lib\site-packages\numpy\lib\arraysetops.py(175)
unique()
        174 
    --> 175     optional_indices = return_index or return_inverse
        176     optional_returns = optional_indices or return_counts
    
    ipdb> c

4.自定义的魔法命令

scpy2.utils.nbmagics:该模块中定义了本书提供的魔法命令,如果读者使用本书提供的批处理运行Notebook,则该模块已经载入。notebooks\01-intro\scpy2-magics.ipynb是这些魔法命令的使用说明。

IPython提供了很方便的自定义魔法命令的方法。最简单的方法就是使用register_line_magic和register_cell_magic装饰器将函数转换为魔法命令。下面的例子使用register_line_magic定义了一个行魔法命令%find,它在指定的对象中搜索与目标匹配的属性名:

    from IPython.core.magic import register_line_magic
    
    @register_line_magic
    def find(line):
        from IPython.core.getipython import get_ipython
        from fnmatch import fnmatch
    
        items = line.split() ❶
        patterns, target = items[:-1], items[-1]
        ipython = get_ipython() ❷
        names = dir(ipython.ev(target)) ❸
    
        results = []
        for pattern in patterns:
            for name in names:
                if fnmatch(name, pattern):
                    results.append(name)
        return results

当调用%find行魔法命令时,魔法命令后面的所有内容都传递给line参数。❶按照空格对line进行分隔,除最后一个元素之外,其余的元素都作为搜索模板,而最后一个参数则为搜索的目标。❷通过get_ipython()函数获得表示IPython运算核的对象,通过该对象可以操作运算核。❸调用运算核的ev()方法对表达式target求值以得到实际的对象,并用dir()获取该对象的所有属性名。

最后使用fnmatch模块对搜索模板和属性名进行匹配,将匹配结果保存到results并返回。下面使用%find命令在numpy模块中搜索所有以array开头或包含mul的属性名:

下面的例子使用register_cell_magic注册%%cut单元命令。在调试代码时,我们经常会添加print语句以输出中间结果。但如果输出的字符串太多,会导致浏览器的速度变慢甚至失去响应。此时可以使用%%cut限制程序输出的行数和字符数。

cut()函数有两个参数:line和cell,其中line为单元第一行中除了魔法命令之外的字符串,而cell为除了单元中第一行之外的所有字符串。line通常为魔法命令的参数,而cell则为需要执行的代码。IPython提供了基于装饰器的参数分析函数。下面的例子使用argument()声明了两个参数-l和-c,它们分别指定最大行数和最大字符数,它们的默认值分别为100和10000:

    from IPython.core.magic import register_cell_magic
    from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring
    
    @magic_arguments()
    @argument('-l', '--lines', help='max lines', type=int, default=100)
    @argument('-c', '--chars', help='max chars', type=int, default=10000)
    @register_cell_magic
    def cut(line, cell):
        from IPython.core.getipython import get_ipython
        from sys import stdout
        args = parse_argstring(cut, line) ❶
        max_lines = args.lines
        max_chars = args.chars
    
        counters = dict(chars=0, lines=0)
    
        def write(string):
            counters["lines"] += string.count("\n")
            counters["chars"] += len(string)
    
            if counters["lines"] >= max_lines:
                raise IOError("Too many lines")
            elif counters["chars"] >= max_chars:
                raise IOError("Too many characters")
            else:
                old_write(string)
    
        try:
            old_write, stdout.write = stdout.write, write❷
            ipython = get_ipython()
            ipython.run_cell(cell) ❸
        finally:
            del stdout.write❹

❶调用parse_argstring()分析行参数,它的第一个参数是使用argument装饰器修饰过的魔法命令函数,第二个参数为行命令字符串。❷在调用单元代码之前,将stdout.write()替换为限制输出行数和字符数的write()函数。❸调用运算核对象的run_cell()来运行单元代码。❹运行完毕之后将stdout.write()删除,恢复到原始状态。

下面是使用%%cut限制输出行数的例子:

    %%cut -l 5
    for i in range(10000):
        print "I am line", i
    I am line 0
    I am line 1
    I am line 2
    I am line 3
    I am line 4
    ---------------------------------------------------------------------------
    IOError                                   Traceback (most recent call last)
    <ipython-input-9-5d2e5180be18> in <module>()
          1 for i in range(10000):
    ----> 2     print "I am line", i
    
    <ipython-input-8-e0ddfb5e18b6> in write(string)
         20 
         21         if counters["lines"] >= max_lines:
    ---> 22             raise IOError("Too many lines")
         23         elif counters["chars"] >= max_chars:
         24             raise IOError("Too many characters")
    
    IOError: Too many lines