例題5-4:ダイアログ出したいんですけど(2)

A: さて、いよいよウィジェットの動作を定義しようか。

サンプル3

 1#!/usr/bin/env python
 2# -*- coding: shift-jis -*-
 3
 4import Tkinter
 5import time
 6
 7class myFrame(Tkinter.Frame):
 8    def __init__(self,master=None):
 9        Tkinter.Frame.__init__(self,master)
10        self.master.title(u'ダイアログのテスト')
11
12        self.SbjNameEntry = Tkinter.StringVar()
13        Tkinter.Label(self,text=u'被験者名').grid(row=0,column=0)
14        Tkinter.Entry(self,textvariable=self.SbjNameEntry).grid(row=0,column=1)
15        Tkinter.Button(self,text=u'開始',command=self.quitfunc).grid(row=1,columnspan=2)
16        self.pack()
17    def quitfunc(self):
18        self.sbjname = self.SbjNameEntry.get()
19        self.winfo_toplevel().destroy()
20        self.quit()
21
22w = myFrame()
23w.mainloop()
24print w.sbjname

B: 関数が1個増えましたね。えーと、メソッド、でしたっけ。

A: そうだね。新しく増えたquitfunc()は被験者名を保存してウィンドウを閉じるためのメソッドだ。 ではくわしく見ていこうか。まず12行目。入力された被験者名は、変数に保存しないと使えない。Tkinter.Entryに入力された文字を保存するには普通の変数ではだめで、 Tkinter.StringVarというクラスのインスタンスでなければならない。で、12行目でTkinter.StringVarのインスタンスを生成しているわけだが、 むしろここで気をつけたいのはインスタンスを保存している変数の方だね。

B: self.SbjNameEntryっていうやつですか?

A: そう。例えばSbjNameEntryという普通の変数にしてしまうと、これは__init()__のローカル変数と解釈されるので、 __init()__の中からしか見えない。例題5-1で話した 変数のスコープ の話、覚えているか?

B: あ、わかります。関数の中で定義した変数はその関数の中でしか使えないんですよね。

A: 17行目以降の解説で詳しく説明するけど、ウィンドウを閉じるときにはquitfunc()というメソッドの中で 被験者名を保持しているTkinter.StringVarのインスタンスにアクセス出来なければいけない。すると、このインスタンスを保存するには__init__()の ローカル変数じゃダメだということだ。

B: むむむ。難しい。

A: そこで登場するのがselfだ。selfはすべてのメソッドの第1引数に現れるのだから、selfに保存しておいてやれば どのメソッドからも必ずアクセスできる。

B: ?? selfに保存する、ってどういうことですか?

A: ここで例題1であいまいにメンバに代入と言っていたことをきちんと話しておこう。 例題1では例えばこんなのが出てきた。

for e in event.get():
    if e.type == KEYDOWN and e.key == K_SPACE:
        waitingKeyPress = False

B: キーが押されたかチェックしてるところですね。

A: そう。ここに出てくる変数eが問題なんだが、eにはpygame.event.get()で取得されたイベントの情報が入っている。 このイベントの情報というのは実はpygame.event.Eventというクラスのインスタンスなんだ。このインスタンスはどのようなイベントが発生したのか、 どのキーが押されたのかといった情報を保持しているわけだが、例えばユーザーがスペースキーを押して、続いてXキーを押した場合、最初のインスタンスには 「スペースキーが押された」という情報が入っていて、次のインスタンスには「Xキーが押された」という情報が入っているといった具合に、 インスタンス毎に異なる値が保持されなければならない。こういう時に用いられるのがクラスの データ属性(data attribute) だ。 他のプログラミング言語ではメンバ変数やインスタンス変数などと呼ばれることもある。 「変数」という名前から想像がつくように、通常の変数と同じように扱えるが、個々のインスタンスに個別に用意される変数なんだ。

../_images/05-4-01.png

B: じゃあ、2行目のe.typeとかe.keyっていうのは…

A: それぞれ変数eに格納されたpygame.event.Eventインスタンスのtypeとkeyというデータ属性を表しているんだ。 一般に、varという変数に格納されたインスタンスのattというデータ属性はvar.attと書く。

B: なるほど。

A: さて、それを踏まえてサンプル3の12行目、self.SbjNameEntry = Tkinter.StringVar()の説明だ。 これは、インスタンスの中にSbjNameEntryというデータ属性を作成して、そこへTkinter.StringVar()の戻り値を格納する事を意味している。 データ属性はインスタンス毎に用意されるから、仮にmyFrameクラスのウィンドウを複数開いたとしても、異なるウィンドウ間で値がごっちゃになってしまう事はない。 そして、さっきも言ったようにselfはすべてのクラスメソッドの第1引数だから、異なるクラスメソッド間で値を参照できる。

B: ふーーむ。なんだかよくわかんないんですけど、なんだか良く出来てるんだなあという気になってきました。

A: まあすぐに理解するのは難しいかも知れんね。よくわからなければ、とりあえず後で参照したり、他のメソッドで参照したり する必要がある変数は前に「self.」ってつけないといけないんだって覚えておけばいいよ。心理実験のプログラム程度なら、多分それで困らない…と、思う。

B: そのくらいなら覚えられます。

A: よし。じゃあ12行目はこのくらいにしておいて、次は14行目。Tkinter.Entryの引数textvariableに、12行目で生成した self.SbjNameEntryを指定している。これで、エディットボックスに入力された被験者名を保存する準備は完了だ。15行目のglid()メソッドの引数は本質的な変更じゃなくて、 columnspanという引数の例として挙げてみた。columnspanを指定すると、row、columnで指定した位置を左端として、columnspan列だけ幅があるスペースにウィジェットが配置される。 15行目ではcolumnの指定が欠けているが、この場合columnはデフォルト値の0となる。図に描くとこんな感じかな。HTMLのソースを直接書いちゃう人なんかにはthやtdのcolspan属性 と同じようなものと言えばわかりやすいかな。

../_images/05-4-02.png

B: HTML、大学入ってすぐの情報処理でやりました。懐かしー。columnspanがあるなら、rowspanもあるんですか?

A: あるよ。でもその辺を説明しだすと今回の話題からそれちゃうんで、興味があったらTkinterのドキュメントを調べてくれ。 15行目のもうひとつの変更点、Tkinter.Button()のcommandという引数は、ボタンが押された時に実行する関数を指定するものだ。15行目ではcommand=self.quitfuncと 書いてあるから、このボタンを押すとself.quitfunc()メソッドが実行されるというわけだ。ここがこのサンプルの最大のポイントかな。

B: ?? メソッドを引数にすることなんて出来るんですか?

A: 出来る。メソッドも変数も結局はPC上ではメモリのある場所を指し示すだけのものだからね。この辺りはC言語を一度 きちんと勉強していたらすぐにピンとくるんだろうが、まあ心理実験のプログラムを書くという目的からすると当面は理解できなくても「こう書けばいいんだ」って 事を覚えておけば十分じゃないかなあ。それでは物足りなくなったらきちんとした教科書で勉強してほしい。

B: ふーん。C言語ですかあ…。ぼくは、とりあえずプログラムが動けばなんでもいいかなあ。

A: よっしゃ。じゃあ肝心のquitfunc()の中身を見てみよう。まず17行目。お約束通り第1引数はself。他に引数は不要なのでselfだけ。 18行目。get()はエディットボックスに入力された文字列を取得するメソッドだ。self.SbjNameEntryに入力された文字列をsbjnameというデータ属性に代入している。 19行目はウィンドウを閉じるための手続きだ。まあこれはこう書くんだと思っておいてくれ。そして20行目のquit()でウィンドウの処理を終了する。 これでself.sbjnameに被験者名が入った状態でウィンドウが閉じ、無事に目的達成ということになる。

B: なんか説明が投げやりなような。エディットボックスが何個もあったらその分だけget()しないといけないんですか?

A: ボタンが押された時に何をしたいのかによるな。処理の都合上入力された値が必要なエディットボックスは全てget()しないといけない。 最後に24行目の説明だね。これはウィンドウを閉じた後で先ほど保存したself.sbjnameの値が必要な時にどう書けばいいかを示している。 クラスメソッドの定義中はself.sbjnameという具合に「self.」と書いていたわけだが、24行目はプログラムの本体で、wという変数に格納されたmyFrameインスタンスの sbjnameを参照するのだから、w.sbjnameと書かないといけない。その事さえ押さえておけば、何も難しい事はない。

B: ややこしいなあ。でも、このw.sbjnameを使うと実験データ保存ファイルの名前を被験者の名前にする事が出来そうですね。 後はぼく一人でもなんとかプログラムを書けそうです。

A: ほう。そりゃ頼もしいな。ただ、バイト先の先輩が使うということだから、使い方を間違えたときの予防策も伝授しておくかな。

B: 使い方を間違える?

A: そう。たとえばサンプル3を実行して、「開始」ボタンではなくウィンドウの右上のボタンを押して閉じてごらん。

../_images/05-4-03.png

B: よいしょっと。あ、エラーが出てきました。

C:\Users\user\Desktop>sample3.py
Traceback (most recent call last):
  File "C:\Users\user\Desktop\sample3.py", line 24, in <module>
    print w.sbjname
AttributeError: myFrame instance has no attribute 'sbjname'

A: そう。sbjnameというデータ属性はquitfunc()を実行した時に初めて作成されるわけだが、ウィンドウ右上のボタンを押して 閉じてしまった場合はquitfunc()が実行されないから、sbjnameが作成されないまま24行目へ処理が進んでしまう。すると24行目でsbjnameというデータ属性はないよというエラーが発生して処理が停止してしまう。

B: なるほど。

A: まあ自分でプログラムを書いて自分で使う分には「ああ、間違えた」で済む話だけど、PCに詳しくない人に使ってもらう場合は こんなエラーメッセージが出ても何のことかわからないし、不安になってしまうかも知れない。こういう事態が起こった時に、わけのわからんエラーメッセージを 出すんではなく、こちらで指定した処理をさせる事が出来る。そのための構文がtryとexceptだ。サンプル3をちょっと改造してみよう。サンプル4ね。

サンプル4

 1#!/usr/bin/env python
 2# -*- coding: shift-jis -*-
 3
 4import Tkinter
 5import time
 6
 7class myFrame(Tkinter.Frame):
 8    def __init__(self,master=None):
 9        Tkinter.Frame.__init__(self,master)
10        self.master.title(u'ダイアログのテスト')
11
12        self.SbjNameEntry = Tkinter.StringVar()
13        Tkinter.Label(self, text=u'被験者名').grid(row=0, column=0)
14        Tkinter.Entry(self, textvariable=self.SbjNameEntry).grid(row=0, column=1)
15        Tkinter.Button(self, text=u'開始', command=self.quitfunc).grid(row=1, columnspan=2)
16        self.pack()
17    def quitfunc(self):
18        self.sbjname = self.SbjNameEntry.get()
19        self.winfo_toplevel().destroy()
20        self.quit()
21
22w = myFrame()
23w.mainloop()
24try:
25    FileName =  w.sbjname + '.txt'
26except:
27    print u'被験者名が指定されませんでした。被験者名を「ほげほげ」としておきます。'
28    FileName = u'ほげほげ.txt'
29
30print u'データファイル名は%sです。' % (FileName)

A: 23行目まではサンプル3と同じだからいいとして、24行目にtry、26行目にexceptという文が出てくる。 tryとexceptはペアで使用する構文で、tryの後に字下げされたブロックが来て、その直後にexceptが来なければいけない。 tryが出てくると、pythonインタプリタはtryに続く字下げされたブロックをとりあえず実行してみる。そして、もしその途中でインタプリタが停止してしまうような エラーが発生したら、直ちにexceptの次の行に処理が移る。もしtryに続くブロックでエラーもなく処理が完了すれば、exceptに続く字下げされたブロックの 処理は実行されない。

B: ??

A: サンプル4の行番号で具体的に言うと、まず24行目でtryを発見。26行目にexceptがあるから、その間にある 25行目を実行してみる。もしw.sbjname属性がきちんと存在していたら、エラーは起きないのでexceptに続く字下げされたブロック(27から28行目)は処理する必要がない。 そこで30行目に処理が進んで、データファイル名が表示される。 もしw.sbjname属性が存在していなかったら、exceptに続く27行目から28行目が実行される。ここで自前のエラーメッセージを表示して、FileNameに仮の名前を入れておく。 そして30行目に進んでデータファイル名を表示する。

B: なるほど。でも、ほげほげ.txtなんて名前にするんじゃなくて被験者名をもう一度きちんと入力し直すようにしたいです。

A: ま、そりゃそうだな。他にも被験者名としてファイル名に使えない文字列が入力された場合はどうするのか という問題もある。このあたりはまとめて練習問題にしておくか。

B: えーっ。そんなのどうしたらいいかわかりません。

A: サンプル4ではtryとexceptの間に1行しか文がないが、もっとたくさんの文を書く事が出来る。 だからtryとexceptの間でopen()を使ってファイルを開くところまでやってみればいい。もしopen()出来ないようなファイル名が指定されていたら、そこで exceptに処理が飛ぶ。

B: はあ…。まあ、何とか自分で頑張ってみます。

A: さて、これで自前のクラスの作り方の基本は説明したかなあ。クラスと言えばあとメソッドのオーバーライドも 説明しておきたかったところだけど、このあたりで一区切りするべきだろうな。

B: 前回の関数と今回のクラスはどちらもハードでした…。

A: 最後にひとつ、補足しておこうかな。今回のサンプル3、サンプル4ではquitfunc()の中でself.sbjnameという データ属性に被験者名を保存していた。しかし、実はquit()した後でもself.SbjNameEntryは残っているんだから、mainloop()を終了した後にget()を実行して エディットボックスに入力された被験者名を得ることもできるんだ。具体的に言うと、サンプル3の17行目以降は以下のように書いても良い。

17    def quitfunc(self):
18        self.winfo_toplevel().destroy()
19        self.quit()
20
21w = myFrame()
22w.mainloop()
23print w.SbjNameEntry.get()

B: sbjnameというデータ属性は使わなくてもよかったんですね。

A: そういうことになる。この企画で題材にしているプログラムは私が書いたものだが、私自身pythonを きちんと勉強したわけではないので、もっと効率のよい書き方があるのを知らずに恥をさらしているような部分もあるかも知れない。 それに、今回の場合がそうだが、解説のためにわざと使わなくてよい変数やクラスを使っている場合もある。 だから、ここで題材にしているプログラムの書き方が正しいとは思わずに、慣れてきたらもっと良い書き方を各自で模索してほしい。

B: 何を言い出すんですか、いきなり。

A: いやさ、いくらテキトーですよって宣言しているとはいえ、こういうのを書いていると多少なりとも ちゃんとしたものを書かないといけないかなぁという気になってくるのよ。今までちゃんとしたものを書こうとして散々挫折してきてるんで、 もっと「ええかげん」にやるべきだと思ってるんだけど。

B: なんかAさんらしくないなあ。ぼくもお腹すきましたし、さっきのリりんご食べて気分転換しませんか。 うちの田舎のりんご、うまいんですよ。ホント。

A: そうだね。じゃあひとついただくとするか。ナイフはそこに入ってるからむいておくれよ。 さて、お茶でも入れるかな。