akiyoko blog

akiyoko の IT技術系ブログです

PDB QUEST ~ pdb のショートカットはドラクエ風に覚えよう ~

f:id:akiyoko:20141217012755p:plain

この投稿は 「Python Advent Calendar 2014 - Qiita」 の 17日目の記事です。



Python のコードをデバッグするには、Python標準のデバッガである「pdb」モジュールを使いますが、使い方(ショートカット)がなかなか覚えられず、いつもネットで探してしまうことがありませんか?

そこで、pdb のショートカットをドラクエ風に覚えると絶対忘れないよね、というのを紹介してみたいと思います。




先に、まとめておきます。

ショートカット 覚え方 効能 備考
f:id:akiyoko:20141215021552p:plain (w) ールドマップ 自分の今いる場所を知りたいときに使う f:id:akiyoko:20141216223459p:plain
f:id:akiyoko:20141215021953p:plain たいまつ (l の形が似ている) 実行中のコードの周りを明るく照らす f:id:akiyoko:20141216223305p:plain
f:id:akiyoko:20141215021732p:plain (s) らべる 気になったところ(関数やメソッド)を調べる(もぐり込む) f:id:akiyoko:20141216224453p:plain
f:id:akiyoko:20141215021737p:plain (n) げる  逃げる。前に逃げる f:id:akiyoko:20141216224504p:plain
f:id:akiyoko:20141215021745p:plain (r) レミト 関数やメソッドの出口まで一瞬でワープ f:id:akiyoko:20141216224513p:plain
f:id:akiyoko:20141215021751p:plain (c) メラの翼 元の場所まで一瞬でワープ f:id:akiyoko:20141216224521p:plain



では早速、Django アプリケーションをデバッグ していきましょう。




 

1. Django アプリケーションを準備する

確認環境は、

です。


python-pip や virtualenv、git は事前にインストールしておきます。

事前準備は、


を参考に。


今回は、mkvirtualenv (virtualenvwrapper) を使わないパターンで。

$ virtualenv ~/django_env
$ source ~/django_env/bin/activate
(django_env) $ sudo mkdir -p /opt/webapps
(django_env) $ sudo chown `whoami`. /opt/webapps
(django_env) $ cd /opt/webapps/
(django_env) $ pip install django==1.4.16


ここで、「django-admin.py startproject mysite」で Djangoアプリケーションを作成する代わりに、Django 1.4 公式チュートリアルの Pollアプリケーション(とほぼ同じもの)が実装されている、
https://github.com/Chive/django-poll-app.git
(の一部不具合を修正した Fork リポジトリ)を使うことにします。

(django_env) $ git clone https://github.com/akiyoko/django-poll-app.git mysite
(django_env) $ cd mysite/



なお、データが 1件だけ入ったデータベース (mysite/db.sqlite3) がリポジトリに含まれているので、起動前に「python manage.py syncdb」を実行する必要はありません。
(ちなみに、superuser は「admin/admin」)




 

2. pdb をコードに仕込む

デバッグをしたいポイントに、以下のコードを仕込みます。

import pdb; pdb.set_trace()


例えば、vote() の中に pdb を仕込んでみます。


polls/views.py

 40 def vote(request, poll_id):
 41     import pdb; pdb.set_trace()
 42     p = get_object_or_404(Poll, pk=poll_id)
 43     try:
 44         selected_choice = p.choice_set.get(pk=request.POST['choice'])
 45     except (KeyError, Choice.DoesNotExist):
 46         # Redisplay the poll voting form.
 47         return render(request, 'polls/detail.html', {
 48             'poll': p,
 49             'error_message': "You didn't select a choice.",
 50         })
 51     else:
 52         selected_choice.votes += 1
 53         selected_choice.save()
 54         # Always return an HttpResponseRedirect after successfully dealing
 55         # with POST data. This prevents data from being posted twice if a
 56         # user hits the Back button.
 57         return HttpResponseRedirect(reverse('polls:results', args=(p.id,)))



アプリケーションを起動します。

$ python manage.py runserver 0.0.0.0:8000


 

3. デバッグポイントで停止させる

ブラウザを起動し、「http://192.168.33.10:8000/polls/1/」にアクセスして「Vote」ボタンを押下すると、
f:id:akiyoko:20141216022200p:plain

pdb を仕込んだ箇所でデバッガが停止します。

f:id:akiyoko:20141216234007p:plain

> /opt/webapps/mysite/polls/views.py(42)vote()
-> p = get_object_or_404(Poll, pk=poll_id)
(Pdb) 




さあ、ここからが本番です。
ここからは、ダンジョンにいる という体で話を進めますよー。


 

4. 現在いる場所はどこ?

f:id:akiyoko:20141215021552p:plain(ワールドマップ)

最初に、ワールドマップで現在いる場所を見てみましょう。

(Pdb) w


f:id:akiyoko:20141115083720p:plain

(Pdb) w
  /usr/lib/python2.7/threading.py(524)__bootstrap()
-> self.__bootstrap_inner()
  /usr/lib/python2.7/threading.py(551)__bootstrap_inner()
-> self.run()
  /usr/lib/python2.7/threading.py(504)run()
-> self.__target(*self.__args, **self.__kwargs)
  /usr/lib/python2.7/SocketServer.py(582)process_request_thread()
-> self.finish_request(request, client_address)
  /usr/lib/python2.7/SocketServer.py(323)finish_request()
-> self.RequestHandlerClass(request, client_address, self)
  /opt/webapps/django_env/local/lib/python2.7/site-packages/django/core/servers/basehttp.py(139)__init__()
-> super(WSGIRequestHandler, self).__init__(*args, **kwargs)
  /usr/lib/python2.7/SocketServer.py(638)__init__()
-> self.handle()
  /usr/lib/python2.7/wsgiref/simple_server.py(124)handle()
-> handler.run(self.server.get_app())
  /usr/lib/python2.7/wsgiref/handlers.py(85)run()
-> self.result = application(self.environ, self.start_response)
  /opt/webapps/django_env/local/lib/python2.7/site-packages/django/contrib/staticfiles/handlers.py(67)__call__()
-> return self.application(environ, start_response)
  /opt/webapps/django_env/local/lib/python2.7/site-packages/django/core/handlers/wsgi.py(241)__call__()
-> response = self.get_response(request)
  /opt/webapps/django_env/local/lib/python2.7/site-packages/django/core/handlers/base.py(111)get_response()
-> response = callback(request, *callback_args, **callback_kwargs)
> /opt/webapps/mysite/polls/views.py(42)vote()
-> p = get_object_or_404(Poll, pk=poll_id)


スタックトレース??
何だそりゃ??って感じですが、よくよく見てみると、矢印(->)の付いていない「フレーム」(コードの場所)と矢印(->)の付いている「コード」がペアになっているのが分かるかと思います。


ひとつ上の「フレーム」「コード」とその下の「フレーム」「コード」は呼び出し元・呼び出し先の関係になっていて、大元から順に上から出力されています。



「>」が現在のフレームを表しているので、そのすぐ下のコード

-> p = get_object_or_404(Poll, pk=poll_id)

デバッグすることができる、というわけです。


ここで、f:id:akiyoko:20141215022411p:plain(UP)や f:id:akiyoko:20141215022419p:plain(DOWN)を叩いてみましょう。フレームが上下に移動しますよね。

(Pdb) d
*** Newest frame

と表示されたら、すでに一番下のフレームまで来ているということを表しています。





 

f:id:akiyoko:20141215021953p:plain(たいまつ)

ワールドマップだけでは周りの状況がよく分からないので、実行中のコードの周囲を照らします。

(Pdb) l


「l」が何かの形に見えませんか? そう、「たいまつ」ですよね!


f:id:akiyoko:20141115075948p:plain

f:id:akiyoko:20141216025625p:plain

たいまつを使うと、周りのコードが明るく照らされます。

(Pdb) l
 37         template_name = 'polls/results.html'
 38     
 39     
 40     def vote(request, poll_id):
 41         import pdb; pdb.set_trace()
 42  ->     p = get_object_or_404(Poll, pk=poll_id)
 43         try:
 44             selected_choice = p.choice_set.get(pk=request.POST['choice'])
 45         except (KeyError, Choice.DoesNotExist):
 46             # Redisplay the poll voting form.
 47             return render(request, 'polls/detail.html', {


「->」が付いているのが、現在実行しようとしているコードです。上の例では、

-> p = get_object_or_404(Poll, pk=poll_id)

のところにいるのが分かりますよね。





5. 気になった関数やメソッドを調べたい

気になった関数やメソッドを調べるには、

f:id:akiyoko:20141215021732p:plain(しらべる)


そう、「しらべる」ですよね。

(Pdb) s


f:id:akiyoko:20141115084606p:plain


「->」が付いているコード中の関数やメソッドの中を「しらべる」(もぐり込む)ことができます。


例えば、

-> p = get_object_or_404(Poll, pk=poll_id)

にいるときに f:id:akiyoko:20141215021732p:plain(しらべる)を叩くと、

(Pdb) s
--Call--
> /opt/webapps/django_env/local/lib/python2.7/site-packages/django/shortcuts/__init__.py(100)get_object_or_404()
-> def get_object_or_404(klass, *args, **kwargs):
(Pdb)

get_object_or_404() の内部にもぐり込むことができました。





6. コードを先へ進める

現在いる場所が目的の地点ではなかった場合は、どんどん先へ進めていきましょう。

そんなときは、

f:id:akiyoko:20141215021737p:plain(にげる)

しかない!!

(Pdb) n


f:id:akiyoko:20141217001203p:plain


(Pdb) s
--Call--
> /opt/webapps/django_env/local/lib/python2.7/site-packages/django/shortcuts/__init__.py(100)get_object_or_404()
-> def get_object_or_404(klass, *args, **kwargs):

(Pdb) n
> /opt/webapps/django_env/local/lib/python2.7/site-packages/django/shortcuts/__init__.py(111)get_object_or_404()
-> queryset = _get_queryset(klass)

(Pdb) l
106  	    arguments and keyword arguments are used in the get() query.
107
108  	    Note: Like with get(), an MultipleObjectsReturned will be raised if more than one
109  	    object is found.
110  	    """
111  ->	    queryset = _get_queryset(klass)
112  	    try:
113  	        return queryset.get(*args, **kwargs)
114  	    except queryset.model.DoesNotExist:
115  	        raise Http404('No %s matches the given query.' % queryset.model._meta.object_name)
116

(Pdb)


「->」が一行進みましたね。

f:id:akiyoko:20141215021953p:plain(たいまつ)を使うと、現在位置が逐一確認できて分かりやすいですね。



普段は、

(Pdb) n
(Pdb) l
(Pdb) n
(Pdb) l
    ・
    ・

と、一行ずつ進んでは実行中のコードを確認するのがよいでしょう。





7. 出口までワープ

慣れてきたら、一気に出口のところまでワープしてみます。


ダンジョンの出口まで一瞬で出る呪文、そう、

f:id:akiyoko:20141215021745p:plainリレミト

ですね。

(Pdb) r


f:id:akiyoko:20141115085922p:plain


f:id:akiyoko:20141215021745p:plainリレミト)を唱えると関数やメソッドの出口まで来るので、この状態で f:id:akiyoko:20141215021737p:plain(にげる)を 1回実行すると、関数やメソッドを脱出することができます。

(Pdb) l
106  	    arguments and keyword arguments are used in the get() query.
107
108  	    Note: Like with get(), an MultipleObjectsReturned will be raised if more than one
109  	    object is found.
110  	    """
111  ->	    queryset = _get_queryset(klass)
112  	    try:
113  	        return queryset.get(*args, **kwargs)
114  	    except queryset.model.DoesNotExist:
115  	        raise Http404('No %s matches the given query.' % queryset.model._meta.object_name)
116

(Pdb) r
--Return--
> /opt/webapps/django_env/local/lib/python2.7/site-packages/django/shortcuts/__init__.py(113)get_object_or_404()-><Poll: What's up?>
-> return queryset.get(*args, **kwargs)

(Pdb) n
> /opt/webapps/mysite/polls/views.py(43)vote()
-> try:

(Pdb) l
 38
 39
 40  	def vote(request, poll_id):
 41  	    import pdb; pdb.set_trace()
 42  	    p = get_object_or_404(Poll, pk=poll_id)
 43  ->	    try:
 44  	        selected_choice = p.choice_set.get(pk=request.POST['choice'])
 45  	    except (KeyError, Choice.DoesNotExist):
 46  	        # Redisplay the poll voting form.
 47  	        return render(request, 'polls/detail.html', {
 48  	            'poll': p,
(Pdb)


ハイ、脱出しましたね。






8. 変数を表示

ここでデバッグらしく、変数を表示してみましょう。

ドラクエじゃないので覚えにくい ですが、f:id:akiyoko:20141215021926p:plain(PRINT)を使います。

(Pdb) l
 38
 39
 40  	def vote(request, poll_id):
 41  	    import pdb; pdb.set_trace()
 42  	    p = get_object_or_404(Poll, pk=poll_id)
 43  ->	    try:
 44  	        selected_choice = p.choice_set.get(pk=request.POST['choice'])
 45  	    except (KeyError, Choice.DoesNotExist):
 46  	        # Redisplay the poll voting form.
 47  	        return render(request, 'polls/detail.html', {
 48  	            'poll': p,

(Pdb) p poll_id
u'1'

(Pdb) p request.__class__
<class 'django.core.handlers.wsgi.WSGIRequest'>

(Pdb) p p
<Poll: What's up?>

(Pdb) p selected_choice
*** NameError: NameError("name 'selected_choice' is not defined",)

「->」の時点で初期化されていない変数は、NameError になります。
(あと、「p」という変数があるので、あまりよくない例だったかもしれません。。)






9. 最後は キメラの翼

デバッグを終了して最後まで流したいとき、あるいは次のブレークポイントまで流したいときは、

f:id:akiyoko:20141215021751p:plain(キメラの翼)

を使いましょう。

(Pdb) c


f:id:akiyoko:20141115090401p:plain





お疲れ様でした。
PDB QUEST は以上で終了です。




まとめ

f:id:akiyoko:20141217212634p:plain



pdbドラクエ風に覚える、もう常識ですよね!!






明日は、 methane さんの 18日目の記事です。
よろしくお願いします!