- Python科学计算(第2版)
- 张若愚
- 2779字
- 2025-03-09 06:39:22
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