附錄B 更多關(guān)于IPython的內(nèi)容(完)

資料來源:https://github.com/BrambleXu/pydata-notebook

第2章中,我們學習了IPython shell和Jupyter notebook的基礎(chǔ)养晋。本章中,我們會探索IPython更深層次的功能舵抹,可以從控制臺或在jupyter使用。

B.1 使用命令歷史

Ipython維護了一個位于磁盤的小型數(shù)據(jù)庫蜘澜,用于保存執(zhí)行的每條指令瓣距。它的用途有:

  • 只用最少的輸入箕戳,就能搜索潭千、補全和執(zhí)行先前運行過的指令氧猬;
  • 在不同session間保存命令歷史瓤的;
  • 將日志輸入/輸出歷史到一個文件

這些功能在shell中呐芥,要比notebook更為有用,因為notebook從設(shè)計上是將輸入和輸出的代碼放到每個代碼格子中壮虫。

搜索和重復使用命令歷史

Ipython可以讓你搜索和執(zhí)行之前的代碼或其他命令澳厢。這個功能非常有用,因為你可能需要重復執(zhí)行同樣的命令,例如%run命令剩拢,或其它代碼线得。假設(shè)你必須要執(zhí)行:

In[7]: %run first/second/third/data_script.py

運行成功,然后檢查結(jié)果裸扶,發(fā)現(xiàn)計算有錯框都。解決完問題,然后修改了data_script.py呵晨,你就可以輸入一些%run命令,然后按Ctrl+P或上箭頭熬尺。這樣就可以搜索歷史命令摸屠,匹配輸入字符的命令。多次按Ctrl+P或上箭頭粱哼,會繼續(xù)搜索命令季二。如果你要執(zhí)行你想要執(zhí)行的命令,不要害怕揭措。你可以按下Ctrl-N或下箭頭胯舷,向前移動歷史命令。這樣做了幾次后绊含,你可以不假思索地按下這些鍵桑嘶!

Ctrl-R可以帶來如同Unix風格shell(比如bash shell)的readline的部分增量搜索功能。在Windows上躬充,readline功能是被IPython模仿的逃顶。要使用這個功能,先按Ctrl-R充甚,然后輸入一些包含于輸入行的想要搜索的字符:

In [1]: a_command = foo(x, y, z)

(reverse-i-search)`com': a_command = foo(x, y, z)

Ctrl-R會循環(huán)歷史以政,找到匹配字符的每一行。

輸入和輸出變量

忘記將函數(shù)調(diào)用的結(jié)果分配給變量是非常煩人的伴找。IPython的一個session會在一個特殊變量盈蛮,存儲輸入和輸出Python對象的引用。前面兩個輸出會分別存儲在 _(一個下劃線)和 __(兩個下劃線)變量:

In [24]: 2 ** 27
Out[24]: 134217728

In [25]: _
Out[25]: 134217728

輸入變量是存儲在名字類似_iX的變量中技矮,X是輸入行的編號抖誉。對于每個輸入變量,都有一個對應(yīng)的輸出變量_X穆役。因此在輸入第27行之后寸五,會有兩個新變量_27 (輸出)和_i27(輸入):

In [26]: foo = 'bar'

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

In [28]: _i27
Out[28]: u'foo'

In [29]: _27
Out[29]: 'bar'

因為輸入變量是字符串,它們可以用Python的exec關(guān)鍵字再次執(zhí)行:

In [30]: exec(_i27)

這里耿币,_i27是在In [27]輸入的代碼梳杏。

有幾個魔術(shù)函數(shù)可以讓你利用輸入和輸出歷史。%hist可以打印所有或部分的輸入歷史,加上或不加上編號十性。%reset可以清理交互命名空間叛溢,或輸入和輸出緩存。%xdel魔術(shù)函數(shù)可以去除IPython中對一個特別對象的所有引用劲适。對于關(guān)于這些魔術(shù)方法的更多內(nèi)容楷掉,請查看文檔。

警告:當處理非常大的數(shù)據(jù)集時霞势,要記住IPython的輸入和輸出的歷史會造成被引用的對象不被垃圾回收(釋放內(nèi)存)烹植,即使你使用del關(guān)鍵字從交互命名空間刪除變量。在這種情況下愕贡,小心使用xdel %和%reset可以幫助你避免陷入內(nèi)存問題草雕。

B.2 與操作系統(tǒng)交互

IPython的另一個功能是無縫連接文件系統(tǒng)和操作系統(tǒng)。這意味著固以,在同時做其它事時墩虹,無需退出IPython,就可以像Windows或Unix使用命令行操作憨琳,包括shell命令诫钓、更改目錄、用Python對象(列表或字符串)存儲結(jié)果篙螟。它還有簡單的命令別名和目錄書簽功能菌湃。

表B-1總結(jié)了調(diào)用shell命令的魔術(shù)函數(shù)和語法。我會在下面幾節(jié)介紹這些功能闲擦。

表B-1 IPython系統(tǒng)相關(guān)命令

Shell命令和別名

用嘆號開始一行慢味,是告訴IPython執(zhí)行嘆號后面的所有內(nèi)容墅冷。這意味著你可以刪除文件(取決于操作系統(tǒng)纯路,用rm或del)、改變目錄或執(zhí)行任何其他命令寞忿。

通過給變量加上嘆號驰唬,你可以在一個變量中存儲命令的控制臺輸出。例如腔彰,在我聯(lián)網(wǎng)的基于Linux的主機上叫编,我可以獲得IP地址為Python變量:

In [1]: ip_info = !ifconfig wlan0 | grep "inet "

In [2]: ip_info[0].strip()
Out[2]: 'inet addr:10.0.0.11  Bcast:10.0.0.255  Mask:255.255.255.0'

返回的Python對象ip_info實際上是一個自定義的列表類型,它包含著多種版本的控制臺輸出霹抛。

當使用搓逾!,IPython還可以替換定義在當前環(huán)境的Python值杯拐。要這么做霞篡,可以在變量名前面加上$符號:

In [3]: foo = 'test*'

In [4]: !ls $foo
test4.py  test.py  test.xml

%alias魔術(shù)函數(shù)可以自定義shell命令的快捷方式世蔗。看一個簡單的例子:

In [1]: %alias ll ls -l

In [2]: ll /usr
total 332
drwxr-xr-x   2 root root  69632 2012-01-29 20:36 bin/
drwxr-xr-x   2 root root   4096 2010-08-23 12:05 games/
drwxr-xr-x 123 root root  20480 2011-12-26 18:08 include/
drwxr-xr-x 265 root root 126976 2012-01-29 20:36 lib/
drwxr-xr-x  44 root root  69632 2011-12-26 18:08 lib32/
lrwxrwxrwx   1 root root      3 2010-08-23 16:02 lib64 -> lib/
drwxr-xr-x  15 root root   4096 2011-10-13 19:03 local/
drwxr-xr-x   2 root root  12288 2012-01-12 09:32 sbin/
drwxr-xr-x 387 root root  12288 2011-11-04 22:53 share/
drwxrwsr-x  24 root src    4096 2011-07-17 18:38 src/

你可以執(zhí)行多個命令朗兵,就像在命令行中一樣污淋,只需用分號隔開:

In [558]: %alias test_alias (cd examples; ls; cd ..)

In [559]: test_alias
macrodata.csv  spx.csv  tips.csv

當session結(jié)束,你定義的別名就會失效余掖。要創(chuàng)建恒久的別名寸爆,需要使用配置。

目錄書簽系統(tǒng)

IPython有一個簡單的目錄書簽系統(tǒng)盐欺,可以讓你保存常用目錄的別名赁豆,這樣在跳來跳去的時候會非常方便。例如找田,假設(shè)你想創(chuàng)建一個書簽歌憨,指向本書的補充內(nèi)容:

In [6]: %bookmark py4da /home/wesm/code/pydata-book

這么做之后,當使用%cd魔術(shù)命令墩衙,就可以使用定義的書簽:

In [7]: cd py4da
(bookmark:py4da) -> /home/wesm/code/pydata-book
/home/wesm/code/pydata-book

如果書簽的名字,與當前工作目錄的一個目錄重名甲抖,你可以使用-b標志來覆寫漆改,使用書簽的位置。使用%bookmark的-l選項准谚,可以列出所有的書簽:

In [8]: %bookmark -l
Current bookmarks:
py4da -> /home/wesm/code/pydata-book-source

書簽挫剑,和別名不同,在session之間是保持的柱衔。

B.3 軟件開發(fā)工具

除了作為優(yōu)秀的交互式計算和數(shù)據(jù)探索環(huán)境樊破,IPython也是有效的Python軟件開發(fā)工具。在數(shù)據(jù)分析中唆铐,最重要的是要有正確的代碼哲戚。幸運的是,IPython緊密集成了和加強了Python內(nèi)置的pdb調(diào)試器艾岂。第二顺少,需要快速的代碼。對于這點王浴,IPython有易于使用的代碼計時和分析工具脆炎。我會詳細介紹這些工具。

交互調(diào)試器

IPython的調(diào)試器用tab補全氓辣、語法增強秒裕、逐行異常追蹤增強了pdb。調(diào)試代碼的最佳時間就是剛剛發(fā)生錯誤钞啸。異常發(fā)生之后就輸入%debug几蜻,就啟動了調(diào)試器喇潘,進入拋出異常的堆棧框架:

In [2]: 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:

In [3]: %debug
> /home/wesm/code/pydata-book/examples/ipython_bug.py(9)throws_an_exception()
      8     b = 6
----> 9     assert(a + b == 10)
     10

ipdb>

一旦進入調(diào)試器入蛆,你就可以執(zhí)行任意的Python代碼响蓉,在每個堆棧框架中檢查所有的對象和數(shù)據(jù)(解釋器會保持它們活躍)哨毁。默認是從錯誤發(fā)生的最低級開始枫甲。通過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

執(zhí)行%pdb命令扼褪,可以在發(fā)生任何異常時讓IPython自動啟動調(diào)試器想幻,許多用戶會發(fā)現(xiàn)這個功能非常好用。

用調(diào)試器幫助開發(fā)代碼也很容易话浇,特別是當你希望設(shè)置斷點或在函數(shù)和腳本間移動脏毯,以檢查每個階段的狀態(tài)。有多種方法可以實現(xiàn)幔崖。第一種是使用%run和-d食店,它會在執(zhí)行傳入腳本的任何代碼之前調(diào)用調(diào)試器。你必須馬上按s(step)以進入腳本:

In [5]: 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

然后赏寇,你就可以決定如何工作吉嫩。例如,在前面的異常嗅定,我們可以設(shè)置一個斷點自娩,就在調(diào)用works_fine之前,然后運行腳本渠退,在遇到斷點時按c(continue):

ipdb> b 12
ipdb> c
> /home/wesm/code/pydata-book/examples/ipython_bug.py(12)calling_things()
     11 def calling_things():
2--> 12     works_fine()
     13     throws_an_exception()

這時忙迁,你可以step進入works_fine(),或通過按n(next)執(zhí)行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,到達發(fā)生錯誤的一行荠锭,查看變量旱眯。注意,調(diào)試器的命令是在變量名之前证九,在變量名前面加嘆號删豺!可以查看內(nèi)容:

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

提高使用交互式調(diào)試器的熟練度需要練習和經(jīng)驗。表B-2愧怜,列出了所有調(diào)試器命令呀页。如果你習慣了IDE,你可能覺得終端的調(diào)試器在一開始會不順手拥坛,但會覺得越來越好用蓬蝶。一些Python的IDEs有很好的GUI調(diào)試器尘分,選擇順手的就好。

表B-2 IPython調(diào)試器命令

使用調(diào)試器的其它方式

還有一些其它工作可以用到調(diào)試器丸氛。第一個是使用特殊的set_trace函數(shù)(根據(jù)pdb.set_trace命名的)培愁,這是一個簡裝的斷點。還有兩種方法是你可能想用的(像我一樣缓窜,將其添加到IPython的配置):

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)

第一個函數(shù)set_trace非常簡單定续。如果你想暫時停下來進行仔細檢查(比如發(fā)生異常之前),可以在代碼的任何位置使用set_trace:

In [7]: 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)可以讓代碼繼續(xù)正常行進禾锤。

我們剛看的debug函數(shù)私股,可以讓你方便的在調(diào)用任何函數(shù)時使用調(diào)試器。假設(shè)我們寫了一個下面的函數(shù)恩掷,想逐步分析它的邏輯:

def f(x, y, z=1):
    tmp = x + y
    return tmp / z

普通地使用f倡鲸,就會像f(1, 2, z=3)。而要想進入f黄娘,將f作為第一個參數(shù)傳遞給debug峭状,再將位置和關(guān)鍵詞參數(shù)傳遞給f:

In [6]: debug(f, 1, 2, z=3)
> <ipython-input>(2)f()
      1 def f(x, y, z):
----> 2     tmp = x + y
      3     return tmp / z

ipdb>

這兩個簡單方法節(jié)省了我平時的大量時間。

最后逼争,調(diào)試器可以和%run一起使用宁炫。腳本通過運行%run -d,就可以直接進入調(diào)試器氮凝,隨意設(shè)置斷點并啟動腳本:

In [1]: %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和行號,可以預(yù)設(shè)一個斷點:

In [2]: %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

ipdb>

代碼計時:%time 和 %timeit

對于大型和長時間運行的數(shù)據(jù)分析應(yīng)用望忆,你可能希望測量不同組件或單獨函數(shù)調(diào)用語句的執(zhí)行時間罩阵。你可能想知道哪個函數(shù)占用的時間最長。幸運的是启摄,IPython可以讓你開發(fā)和測試代碼時稿壁,很容易地獲得這些信息。

手動用time模塊和它的函數(shù)time.clock和time.time給代碼計時歉备,既單調(diào)又重復傅是,因為必須要寫一些無趣的模板化代碼:

import time
start = time.time()
for i in range(iterations):
    # some code to run here
elapsed_per = (time.time() - start) / iterations

因為這是一個很普通的操作,IPython有兩個魔術(shù)函數(shù)蕾羊,%time和%timeit喧笔,可以自動化這個過程。

%time會運行一次語句龟再,報告總共的執(zhí)行時間书闸。假設(shè)我們有一個大的字符串列表,我們想比較不同的可以挑選出特定開頭字符串的方法利凑。這里有一個含有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']

看起來它們的性能應(yīng)該是同級別的,但事實呢牌借?用%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(wall-clock time的簡寫)是主要關(guān)注的度气。第一個方法是第二個方法的兩倍多,但是這種測量方法并不準確膨报。如果用%time多次測量磷籍,你就會發(fā)現(xiàn)結(jié)果是變化的。要想更準確丙躏,可以使用%timeit魔術(shù)函數(shù)择示。給出任意一條語句,它能多次運行這條語句以得到一個更為準確的時間:

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和其它庫的性能是很有價值的。在大型數(shù)據(jù)分析中废恋,這些毫秒的時間就會累積起來谈秫!

%timeit特別適合分析執(zhí)行時間短的語句和函數(shù),即使是微秒或納秒鱼鼓。這些時間可能看起來毫不重要拟烫,但是一個20微秒的函數(shù)執(zhí)行1百萬次就比一個5微秒的函數(shù)長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

基礎(chǔ)分析:%prun和%run -p

分析代碼與代碼計時關(guān)系很緊密,除了它關(guān)注的是“時間花在了哪里”嘉赎。Python主要的分析工具是cProfile模塊置媳,它并不局限于IPython。cProfile會執(zhí)行一個程序或任意的代碼塊公条,并會跟蹤每個函數(shù)執(zhí)行的時間拇囊。

使用cProfile的通常方式是在命令行中運行一整段程序,輸出每個函數(shù)的累積時間靶橱。假設(shè)我們有一個簡單的在循環(huán)中進行線型代數(shù)運算的腳本(計算一系列的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)

你可以用cProfile運行這個腳本寥袭,使用下面的命令行:

python -m cProfile cprof_example.py

運行之后,你會發(fā)現(xiàn)輸出是按函數(shù)名排序的关霸。這樣要看出誰耗費的時間多有點困難传黄,最好用-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列谒拴,可以容易地看出每個函數(shù)用了多少時間尝江。如果一個函數(shù)調(diào)用了其它函數(shù),計時并不會停止英上。cProfile會記錄每個函數(shù)的起始和結(jié)束時間炭序,使用它們進行計時啤覆。

除了在命令行中使用,cProfile也可以在程序中使用惭聂,分析任意代碼塊窗声,而不必運行新進程。Ipython的%prun和%run -p辜纲,有便捷的接口實現(xiàn)這個功能笨觅。%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'}

相似的见剩,調(diào)用%run -p -s cumulative cprof_example.py有和命令行相似的作用,只是你不用離開Ipython扫俺。

在Jupyter notebook中苍苞,你可以使用%%prun魔術(shù)方法(兩個%)來分析一整段代碼。這會彈出一個帶有分析輸出的獨立窗口狼纬。便于快速回答一些問題羹呵,比如“為什么這段代碼用了這么長時間”?

使用IPython或Jupyter疗琉,還有一些其它工具可以讓分析工作更便于理解冈欢。其中之一是SnakeViz(https://github.com/jiffyclub/snakeviz/),它會使用d3.js產(chǎn)生一個分析結(jié)果的交互可視化界面盈简。

逐行分析函數(shù)

有些情況下凑耻,用%prun(或其它基于cProfile的分析方法)得到的信息,不能獲得函數(shù)執(zhí)行時間的整個過程柠贤,或者結(jié)果過于復雜拳话,加上函數(shù)名,很難進行解讀种吸。對于這種情況,有一個小庫叫做line_profiler(可以通過PyPI或包管理工具獲得)呀非。它包含IPython插件坚俗,可以啟用一個新的魔術(shù)函數(shù)%lprun,可以對一個函數(shù)或多個函數(shù)進行逐行分析岸裙。你可以通過修改IPython配置(查看IPython文檔或本章后面的配置小節(jié))加入下面這行猖败,啟用這個插件:

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

你還可以運行命令:

%load_ext line_profiler

line_profiler也可以在程序中使用(查看完整文檔),但是在IPython中使用是最為強大的降允。假設(shè)你有一個帶有下面代碼的模塊prof_mod恩闻,做一些NumPy數(shù)組操作:

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函數(shù)的性能,%prun可以給出下面內(nèi)容:

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'}
        1    0.003    0.003    0.049    0.049 <string>:1(<module>)

上面的做法啟發(fā)性不大剧董。激活了IPython插件line_profiler幢尚,新的命令%lprun就能用了破停。使用中的不同點是,我們必須告訴%lprun要分析的函數(shù)是哪個尉剩。語法是:

%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: 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

這樣就容易詮釋了。我們分析了和代碼語句中一樣的函數(shù)理茎『诮纾看之前的模塊代碼,我們可以調(diào)用call_function并對它和add_and_sum進行分析皂林,得到一個完整的代碼性能概括:

In [574]: %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)

我的經(jīng)驗是用%prun (cProfile)進行宏觀分析朗鸠,%lprun (line_profiler)做微觀分析。最好對這兩個工具都了解清楚础倍。

筆記:使用%lprun必須要指明函數(shù)名的原因是追蹤每行的執(zhí)行時間的損耗過多烛占。追蹤無用的函數(shù)會顯著地改變結(jié)果。

B.4 使用IPython高效開發(fā)的技巧

方便快捷地寫代碼著隆、調(diào)試和使用是每個人的目標扰楼。除了代碼風格,流程細節(jié)(比如代碼重載)也需要一些調(diào)整美浦。

因此弦赖,這一節(jié)的內(nèi)容更像是門藝術(shù)而不是科學,還需要你不斷的試驗浦辨,以達成高效蹬竖。最終,你要能結(jié)構(gòu)優(yōu)化代碼流酬,并且能省時省力地檢查程序或函數(shù)的結(jié)果币厕。我發(fā)現(xiàn)用IPython設(shè)計的軟件比起命令行,要更適合工作芽腾。尤其是當發(fā)生錯誤時旦装,你需要檢查自己或別人寫的數(shù)月或數(shù)年前寫的代碼的錯誤。

重載模塊依賴

在Python中摊滔,當你輸入import some_lib阴绢,some_lib中的代碼就會被執(zhí)行,所有的變量艰躺、函數(shù)和定義的引入呻袭,就會被存入到新創(chuàng)建的some_lib模塊命名空間。當下一次輸入some_lib腺兴,就會得到一個已存在的模塊命名空間的引用左电。潛在的問題是當你%run一個腳本,它依賴于另一個模塊,而這個模塊做過修改篓足,就會產(chǎn)生問題段誊。假設(shè)我在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纷纫,下一次再執(zhí)行%run test_script.py枕扫,還會得到舊版本的some_lib.py,這是因為Python模塊系統(tǒng)的“一次加載”機制辱魁。這一點區(qū)分了Python和其它數(shù)據(jù)分析環(huán)境烟瞧,比如MATLAB,它會自動傳播代碼修改染簇。解決這個問題参滴,有多種方法。第一種是在標準庫importlib模塊中使用reload函數(shù):

import some_lib
import importlib

importlib.reload(some_lib)

這可以保證每次運行test_script.py時可以加載最新的some_lib.py锻弓。很明顯砾赔,如果依賴更深,在各處都使用reload是非常麻煩的青灼。對于這個問題暴心,IPython有一個特殊的dreload函數(shù)(它不是魔術(shù)函數(shù))重載深層的模塊。如果我運行過some_lib.py杂拨,然后輸入dreload(some_lib)专普,就會嘗試重載some_lib和它的依賴。不過弹沽,這個方法不適用于所有場景檀夹,但比重啟IPython強多了。

代碼設(shè)計技巧

對于這單策橘,沒有簡單的對策炸渡,但是有一些原則,是我在工作中發(fā)現(xiàn)很好用的丽已。

保持相關(guān)對象和數(shù)據(jù)活躍

為命令行寫一個下面示例中的程序是很少見的:

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中運行這個程序會發(fā)生問題蚌堵,你發(fā)現(xiàn)是什么了嗎?運行之后沛婴,任何定義在main函數(shù)中的結(jié)果和對象都不能在IPython中被訪問到辰斋。更好的方法是將main中的代碼直接在模塊的命名空間中執(zhí)行(或者在__name__ == '__main__':中,如果你想讓這個模塊可以被引用)瘸味。這樣,當你%rundiamante够挂,就可以查看所有定義在main中的變量旁仿。這等價于在Jupyter notebook的代碼格中定義一個頂級變量。

扁平優(yōu)于嵌套

深層嵌套的代碼總讓我聯(lián)想到洋蔥皮。當測試或調(diào)試一個函數(shù)時枯冈,你需要剝多少層洋蔥皮才能到達目標代碼呢毅贮?“扁平優(yōu)于嵌套”是Python之禪的一部分,它也適用于交互式代碼開發(fā)尘奏。盡量將函數(shù)和類去耦合和模塊化滩褥,有利于測試(如果你是在寫單元測試)、調(diào)試和交互式使用炫加。

克服對大文件的恐懼

如果你之前是寫JAVA(或者其它類似的語言)瑰煎,你可能被告知要讓文件簡短。在多數(shù)語言中俗孝,這都是合理的建議:太長會讓人感覺是壞代碼酒甸,意味著重構(gòu)和重組是必要的。但是赋铝,在用IPython開發(fā)時插勤,運行10個相關(guān)聯(lián)的小文件(小于100行),比起兩個或三個長文件革骨,會讓你更頭疼农尖。更少的文件意味著重載更少的模塊和更少的編輯時在文件中跳轉(zhuǎn)。我發(fā)現(xiàn)維護大模塊良哲,每個模塊都是緊密組織的盛卡,會更實用和Pythonic。經(jīng)過方案迭代臂外,有時會將大文件分解成小文件窟扑。

我不建議極端化這條建議,那樣會形成一個單獨的超大文件漏健。找到一個合理和直觀的大型代碼模塊庫和封裝結(jié)構(gòu)往往需要一點工作嚎货,但這在團隊工作中非常重要。每個模塊都應(yīng)該結(jié)構(gòu)緊密蔫浆,并且應(yīng)該能直觀地找到負責每個功能領(lǐng)域功能和類殖属。

B.5 IPython高級功能

要全面地使用IPython系統(tǒng)需要用另一種稍微不同的方式寫代碼,或深入IPython的配置瓦盛。

讓類是對IPython友好的

IPython會盡可能地在控制臺美化展示每個字符串洗显。對于許多對象,比如字典原环、列表和元組挠唆,內(nèi)置的pprint模塊可以用來美化格式。但是嘱吗,在用戶定義的類中玄组,你必自己生成字符串。假設(shè)有一個下面的簡單的類:

class Message:
    def __init__(self, msg):
        self.msg = msg

如果這么寫,就會發(fā)現(xiàn)默認的輸出不夠美觀:

In [576]: x = Message('I have a secret')

In [577]: x
Out[577]: <__main__.Message instance at 0x60ebbd8>

IPython會接收repr魔術(shù)方法返回的字符串(通過output = repr(obj))俄讹,并在控制臺打印出來哆致。因此,我們可以添加一個簡單的repr方法到前面的類中患膛,以得到一個更有用的輸出:

class Message:
    def __init__(self, msg):
        self.msg = msg

    def __repr__(self):
        return 'Message: %s' % self.msg
In [579]: x = Message('I have a secret')

In [580]: x
Out[580]: Message: I have a secret

文件和配置

通過擴展配置系統(tǒng)摊阀,大多數(shù)IPython和Jupyter notebook的外觀(顏色、提示符踪蹬、行間距等等)和動作都是可以配置的胞此。通過配置,你可以做到:

  • 改變顏色主題
  • 改變輸入和輸出提示符延曙,或刪除輸出之后豌鹤、輸入之前的空行
  • 執(zhí)行任意Python語句(例如,引入總是要使用的代碼或者每次加載IPython都要運行的內(nèi)容)
  • 啟用IPython總是要運行的插件枝缔,比如line_profiler中的%lprun魔術(shù)函數(shù)
  • 啟用Jupyter插件
  • 定義自己的魔術(shù)函數(shù)或系統(tǒng)別名

IPython的配置存儲在特殊的ipython_config.py文件中布疙,它通常是在用戶home目錄的.ipython/文件夾中。配置是通過一個特殊文件愿卸。當你啟動IPython灵临,就會默認加載這個存儲在profile_default文件夾中的默認文件。因此趴荸,在我的Linux系統(tǒng)儒溉,完整的IPython配置文件路徑是:

/home/wesm/.ipython/profile_default/ipython_config.py

要啟動這個文件,運行下面的命令:

ipython profile create

這個文件中的內(nèi)容留給讀者自己探索发钝。這個文件有注釋顿涣,解釋了每個配置選項的作用。另一點酝豪,可以有多個配置文件涛碑。假設(shè)你想要另一個IPython配置文件,專門是為另一個應(yīng)用或項目的。創(chuàng)建一個新的配置文件很簡單,如下所示:

ipython profile create secret_project

做完之后瀑志,在新創(chuàng)建的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的其它語言。要創(chuàng)建一個類似的Jupyter配置文件毙籽,運行:

jupyter notebook --generate-config

這樣會在home目錄的.jupyter/jupyter_notebook_config.py創(chuàng)建配置文件。編輯完之后毡庆,可以將它重命名:

$ mv ~/.jupyter/jupyter_notebook_config.py ~/.jupyter/my_custom_config.py

打開Jupyter之后坑赡,你可以添加--config參數(shù):

 jupyter notebook --config=~/.jupyter/my_custom_config.py

B.6 總結(jié)

學習過本書中的代碼案例巡扇,你的Python技能得到了一定的提升,我建議你持續(xù)學習IPython和Jupyter垮衷。因為這兩個項目的設(shè)計初衷就是提高生產(chǎn)率的,你可能還會發(fā)現(xiàn)一些工具乖坠,可以讓你更便捷地使用Python和計算庫搀突。

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末熊泵,一起剝皮案震驚了整個濱河市仰迁,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌顽分,老刑警劉巖徐许,帶你破解...
    沈念sama閱讀 222,183評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異卒蘸,居然都是意外死亡雌隅,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評論 3 399
  • 文/潘曉璐 我一進店門缸沃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來恰起,“玉大人,你說我怎么就攤上這事趾牧〖炫危” “怎么了?”我有些...
    開封第一講書人閱讀 168,766評論 0 361
  • 文/不壞的土叔 我叫張陵翘单,是天一觀的道長吨枉。 經(jīng)常有香客問我,道長哄芜,這世上最難降的妖魔是什么貌亭? 我笑而不...
    開封第一講書人閱讀 59,854評論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮忠烛,結(jié)果婚禮上属提,老公的妹妹穿的比我還像新娘。我一直安慰自己美尸,他們只是感情好冤议,可當我...
    茶點故事閱讀 68,871評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著师坎,像睡著了一般恕酸。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上胯陋,一...
    開封第一講書人閱讀 52,457評論 1 311
  • 那天蕊温,我揣著相機與錄音袱箱,去河邊找鬼。 笑死义矛,一個胖子當著我的面吹牛发笔,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播凉翻,決...
    沈念sama閱讀 40,999評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼了讨,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了制轰?” 一聲冷哼從身側(cè)響起前计,我...
    開封第一講書人閱讀 39,914評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎垃杖,沒想到半個月后男杈,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,465評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡调俘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,543評論 3 342
  • 正文 我和宋清朗相戀三年伶棒,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片脉漏。...
    茶點故事閱讀 40,675評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡苞冯,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出侧巨,到底是詐尸還是另有隱情舅锄,我是刑警寧澤,帶...
    沈念sama閱讀 36,354評論 5 351
  • 正文 年R本政府宣布司忱,位于F島的核電站皇忿,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏坦仍。R本人自食惡果不足惜鳍烁,卻給世界環(huán)境...
    茶點故事閱讀 42,029評論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望繁扎。 院中可真熱鬧幔荒,春花似錦、人聲如沸梳玫。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽提澎。三九已至姚垃,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間盼忌,已是汗流浹背积糯。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評論 1 274
  • 我被黑心中介騙來泰國打工掂墓, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人看成。 一個月前我還...
    沈念sama閱讀 49,091評論 3 378
  • 正文 我出身青樓君编,卻偏偏與公主長得像,于是被迫代替她去往敵國和親川慌。 傳聞我的和親對象是個殘疾皇子啦粹,可洞房花燭夜當晚...
    茶點故事閱讀 45,685評論 2 360

推薦閱讀更多精彩內(nèi)容