例題5-1:繰り返し使う処理をまとめる(1)

B: よし、実行っと。…。んん? あれ?

A: (部屋に入ってきて) よっ。まだ帰ってなかったのか。夜の遅くまで頑張ってるね。

B: あー。このプログラムを完成させたら帰ろうと思ってたんですが…。今ちょうど「できた!」と思って実行したら キー入力がうまくいかなくて。

A: ふうん。今日はもう仕事しないから見てあげるよ。どれどれ。

B: ええとですね、まずここで注視点を表示してですね…

------------------------------ 省略 ------------------------------

A: ふむ。ここが間違っとるな。この76行目から80行目と…

76    waitingKeyPress = True
77    while waitingKeyPress: #スペースキーが押されるのを待つ
78        for e in event.get():
79            if e.type == KEYDOWN and e.key == K_SPACE:
80                waitingKeyPress = False

A: 119行目から128行目をよく見たまえ。

119    # ここから反応の記録
120    while waitingKeyPress: # XキーかCキーが押されるのを待つ
121        for e in event.get():
122            if e.type == KEYDOWN
123                if e.key == K_X:
124                    response = 'X'
125                    waitingKeyPress = False
126                elif e.key == K_C
127                    response = 'C'
128                    waitingKeyPress = False

A: 120行目、whileループが始まる時にwaitingKeyPressには何が格納されている?

B: へ?

A: 76行目でスペースキーを待つためにwaitingKeyPressにTrueを格納して、77行目からのwhileループでスペースキーが押された時点でFalseを格納している。 そのまま120行目までwaitingKeyPressに何も代入していないんだから、120行目のwhileループはいきなりwaitingKeyPressがFalseの状態から始まる。

B: あぁ!

A: 120行目のwhileループに入る前にwaitingKeyPressにTrueを代入していなかったのが失敗だね。

B: うー。リーディングスパンテストのプログラムからコピーしてきた時に1行コピーし忘れてました。

A: はは。よくあるミスだ。一度書いたプログラムはどんどん使えって教えたけど、コピー&ペーストをすると時々こういう失敗をするんだよな。 必要な行をコピーし忘れたり、コピー元とコピー先のプログラムで同じ名前の変数を違う用途で使っていたり。同じ名前を違う用途でってミスはaとかxみたいな1文字の変数やtempみたいな 一時的に使う変数でよくあるパターンだ。

B: うう。119行目に waitingKeyPress = True を追加したらうまく動きました…。

A: これからは気をつけなさい…というのは簡単だが、何カ月も前に書いたプログラムの詳細なんて覚えていられるわけもないし、できるだけ間違えにくいようにプログラムを工夫するのが正しい。

B: 工夫って、具体的にどうするんですか。

A: まあそこが問題なんだが。今回は関数の作り方を教えておこうか。よく使う機能を関数にまとめておくと、今回のような間違いは少なくなる。

B: 関数? 関数って自分で作れるんですか?

A: 最初に説明しなかったっけか。B君がプログラムで使っている関数はみんな誰かが作ってくれたわけで、誰でも関数を作ることが出来る。pythonで関数を作るにはdefという予約語を使う。

def 関数名( 引数 ):
    # 関数の処理内容

B: …これだけ?

A: そう。これだけ。簡単な例を出そうか。「xの4乗とyの2乗の和を計算する」関数を作ってみよう。

B: これまた取ってつけたような例ですね。発想が貧困な。

A: ほっとけ。

 1# -*- coding:shift_jis -*-
 2import time
 3
 4def foo(x,y):
 5    ans = x**4 + y**2
 6    return ans
 7
 8x = 3
 9y = 4
10print u'%dの4乗と%dの2乗の和は%fです。' % (x, y, foo(x,y))
11time.sleep(5.0)

A: 仮にサンプル1としておこうかな。短いプログラムなのでダウンロードは用意してない。自分で入力してみること。Unix系OSなどで文字コードがShift-JISじゃない人は1行目を変更してね。

B: 11行目のtime.sleep(5.0)って何ですか?

A: 引数で指定した時間だけプログラムの実行を停止しなさいという関数だ。単位は秒。

B: あれ、前にもそんな関数ありませんでしたっけ。

A: pygame.time.wait( )関数のことかな(注:例題02-2)。引数の単位が違うがまあ似たようなもんだ。 多分pygame.time.wait()の方が精度が高いと思うんだが、確認したことはないな。今回はわざわざpygameをimportするまでもないのでtime.sleep()を使ってみた。

B: うーん、その同じことをするのする方法が何通りもあるってのが未だになじめません。

A: まあそう言うな。とにかくこのサンプル1で重要なのは4行目から6行目。4行目の def foo(x,y): というのが「これからfooという関数を定義します」という宣言だ。

B: fooってまたテキトーな…

A: いや、まじめな話、こういうサンプルプログラムでは意味ありげな名前を使うとかえって混乱するので、 無意味な単語としてfooやらbarやらを使う事が一般的なんだ。興味があったら「RFC3092」とか「メタ構文変数」で検索してみたまえ。

B: へー。

A: 脱線してしまったな。話を戻すと、defの次に続く語が関数の名前、カッコの中が引数だ。だからここで定義される関数fooはxとyという二つの引数を持つことになる。

B: ふむふむ。

A: 5行目からが関数が呼び出された時に行う処理だ。forとかwhileといった構文と同じように、def文の直後の字下げされた行を関数の中身として解釈する。このサンプルでは6行目までだな。 5行目でxの4乗とyの2乗を計算して、ansという変数に格納している。6行目の return ansは変数ansをfooの戻り値として関数fooの処理を終了しなさいという指示だ。

B: 関数を終了するにはreturnを使えばいいんですね。

A: いや、戻り値が必要なければreturnはなくてもいい。defに続く字下げされた行の処理をすべて終えたら関数は自動的に終了する。 ついでに話しておくと、今までのサンプルでswap_buffers()など引数がない関数がたくさん出てきたが、引数がない関数を定義する場合は、次のように関数名に続く( )の中を空白にしておけばいい。

def bar():
    # 関数barの処理

A: 以上を踏まえて、サンプル1の処理の流れを図にするとこんな感じかな。

../_images/05-1-01.png

B: 今までは1行目から順に進んでいくだけだったのに、戻るんですね。ちょっとややこしそう。

A: まあ今までもwhile文などでは処理が前の行へ戻っていたわけだがね。確かに今までのプログラムより処理の流れは複雑になる。 ここでついでに言っておくと、pythonでは関数fooが呼び出す前に、pythonインタプリタがすでにfooという関数がどこに定義されているのか知っていなければいけない。 だから、このサンプルプログラムで4から6行目の関数の定義を10行目より後ろに持ってくると、「fooなんて関数は知らない」というエラーが出る。 難しいと思うかもしれないが、変数に値を代入する前に参照しようとしたら「そんな変数は知らない」と怒られるのと同じことだ。

B: なるほど。

A: さて、ここからがいよいよ本題だ。 関数の中で宣言された変数は、関数の中でのみ使用可能であり、関数の外部に影響を及ぼさない 。 変数がプログラムのどの部分から利用可能であるかという範囲を変数の スコープ(scope) という。

B: へ??? なんですか、いきなり。

A: 関数fooの中で使われているx、y、ansという変数はfooの中でだけ有効であり、fooの外でx、y、ansという変数が使われていても、その値に影響を及ぼさないということだ。 わかりにくいと思うのでサンプルプログラムをちょっといじってみよう。サンプル2ね。

 1# -*- coding:shift_jis -*-
 2import time
 3
 4def foo(x,y):
 5    ans = x**4 + y**2
 6    print u'fooの内部: x=%d, y=%d, ans=%d' % (x,y,ans)
 7    return ans
 8
 9x = 0; y=2; ans = 4
10print u'fooの呼び出し前: x=%f, y=%f, ans=%f' % (x,y,ans)
11
12foo(7, 6)
13
14print u'fooの呼び出し後: x=%f, y=%f, ans=%f' % (x,y,ans)
15time.sleep(5.0)

A: 主な変更点は6行目、10行目、14行目で変数に格納された値をprint文で出力していること。 そして12行目でfoo()を呼び出すときに、xに7、yに6を与えていることだ。実行してみなさい。

B: ええと。

fooの呼び出し前: x=0, y=2, ans=4
fooの内部: x=7, y=6, ans=2437
fooの呼び出し後: x=0, y=2, ans=4

A: OK。サンプル2では9行目でx=0, y=2, ans=4と代入しているんだから、 fooを呼び出す前である10行目でこれらの変数の値をprintしたらx=0, y=2, ans=4と出力される。これは当然だね。 問題は次だが、12行目でx=7、y=6の引数で関数fooを呼び出し、4行目に処理が移る。5行目でx**4+y**2が計算されるわけだが、 このxとyはそれぞれfooの呼び出しの時に渡された7と6なので、7**4+2**2となって答えは2437。したがってansは2437。 B: 7**4+2**2を暗算できるなんて、Aさん変人ですか?

A: 暗算もなにも、出力結果にans=2437って出てるじゃないか。ともかく、7行目でreturnして12行目に戻る。 そして14行目でまたx、y、ansの値を出力するわけだが、さっき言ったようにfooの中でx、y、ansに加えた変更はfooの外に影響しない。 だからfooが呼び出される前にx=0, y=2, ans=4と代入しておいたものがそのままの残っている。だからprintによる出力はfooの呼び出し前と変化していない。

B: うーん。わかったようなわからないような。

A: ふむ。どう言えばわかりやすいだろうな。マンションが何棟かあって、1号館の305号室に荷物を運び込んでも2号館や3号館の305号室には何も影響がないだろ? 関数というのは、それぞれマンションの別の棟のようなものなんだよ。fooという関数の変数xと、barという関数の変数xは、1号館の201号室と2号館の201号室のようなもので、 同じ「x」でも全く別のものなんだ。

B: ふむふむ。

A: そして、文脈上4号館の話をしているのが明らかなら、「203号室」と言ったら4号館の203号室を指しているのと同じように、 プログラムの中では単に変数xと言えば、どこのxなのかが文脈で決まるようになっている。ある関数の内部にいる時に、その関数の内部だけで通用する変数を ローカル変数 と言い、 プログラムの全域で通用する変数を グローバル変数 と言う。この文脈がさっき言った「スコープ」に該当するかな。

../_images/05-1-02.png

B: ぶ、文脈ですか? またわからなくなってきたような…

A: 通常ならここで「スコープというのは…」と説明を続けたいところだが、正直pythonのスコープはちょっとややこしいので、ここではきちんと説明しない。 機会があればいずれきちんと説明するけど、気になるんならマトモなpythonの教科書を読んでくれたまえ。

B: あ、久々に無責任発言が出ましたね。

A: まあ 他のプログラミング言語でスコープの知識がある人向け に簡単に言っておくと、 pythonでは関数を実行するときに関数内でローカルに定義されていない変数xを参照しようとすると、その変数はグローバルな変数であると解釈される。 ただし、関数内で変数xに対して代入が行われている場合、その変数はローカルであると解釈される。だから、関数内でxに代入を行っているのに、その代入の前に xを参照しようとすると「xというローカル変数は存在しない」というエラーが発生する。どうしても関数内でグローバル変数へ代入を行いたい時はglobalという予約語を使ってその変数がグローバル変数であることを宣言しなければいけない。

B: … (ぽかーん)

A: これからプログラムを学ぶ人向けに言うと、 関数内で使う変数はすべてローカル変数にするつもりでいなさい 。 もともと今回の話は「新しくプログラムを書く時に、以前書いたプログラムから安全にコピーしてくる」というところから始まったわけだけど、 この目的に関して言えば、関数の外にある変数に何が収められているかを前提としたプログラムはコピーの失敗を招きやすい。どの変数を一緒にコピーしないといけないかを 覚えておかないといけないわけだからね。それにグローバル変数を使わないと書けないようなプログラムは初級レベルではまず出てこないだろうし。

B: (意識を失いつつある)

A: …っと。つい具体的な心理実験のプログラムの話から離れすぎたかな。おーい。眠いんなら今晩はここで終わりにして帰るかー?

B: はっ。もう少しで寝るとこでした。明日は講義もバイトもないんで時間は大丈夫ですが、小難しい話はダメかも知れません…

A: ふむ。必要最小限の話はすんだので、実際にB君のキー入力待ちプログラムを関数にしてみようか。

B: あ、それなら意識を保てそうです。

A: よっしゃ。じゃあキー入力待ちの部分を新しいファイルにコピーして、と。あとpygameとpygame.localsが必要になるからimportしておくか。サンプル3。

サンプル3(その1)

 1#!/usr/bin/env python
 2# -*- coding:shift_jis -*-
 3
 4from pygame import *
 5from pygame.locals import *
 6
 7waitingKeyPress = True
 8while waitingKeyPress: #スペースキーが押されるのを待つ
 9    for e in event.get():
10        if e.type == KEYDOWN and e.key == K_SPACE:
11            waitingKeyPress = False

A: さて、これを関数にするわけだが、関数には普通名前が必要だ。pythonでは名前のない関数(注:lambda関数)も使えるが、 単純な心理実験のプログラムを書く限りはまず必要ないと思うのでパス。名前は何にしようか?

B: え、ぼくが決めるんですか? えーと、じゃあwaitkeyで。

A: waitkeyね。では7行目の前に1行追加してdef waitkey():と入力する。waitkeyという関数を定義するぞとpythonに教えてやるわけだね。 そして関数の中身にしたい処理を字下げする。

サンプル3(その2)

 1#!/usr/bin/env python
 2# -*- coding:shift_jis -*-
 3
 4from pygame import *
 5from pygame.locals import *
 6
 7def waitkey():
 8    waitingKeyPress = True
 9    while waitingKeyPress: #スペースキーが押されるのを待つ
10        for e in event.get():
11            if e.type == KEYDOWN and e.key == K_SPACE:
12                waitingKeyPress = False

B: ふむふむ。

A: 以上。これでwaitkey関数の完成だ。

B: へ? これだけ?

A: これだけ。動作確認の処理も追加しておくか。13行目から16行目が追加部分だ。

サンプル3(完成)

 1#!/usr/bin/env python
 2# -*- coding:shift_jis -*-
 3
 4from pygame import *
 5from pygame.locals import *
 6
 7def waitkey():
 8    waitingKeyPress = True
 9    while waitingKeyPress: #スペースキーが押されるのを待つ
10        for e in event.get():
11            if e.type == KEYDOWN and e.key == K_SPACE:
12                waitingKeyPress = False
13
14init()
15display.set_mode((256,256))
16waitkey()

B: init()とdisplay.set_mode((256,256))ってなんですか?

A: それぞれ正式にはpygame.init()とpygame.display.set_mode()。10行目のevent.get()はpygameの関数で、pygameのウィンドウが開いていないと使えない。 VisionEggを使っているときは、VisionEggがpygameのウィンドウを開いてくれるんだが、今回のサンプルではわざわざVisionEggを使うまでもないのでpygameの関数を直接使ったのさ。

B: では、とりあえず実行してみますね。

../_images/05-1-03.png

B: なんか真っ黒な小さいウィンドウが出てきましたが。

A: これがpygameのウィンドウだね。画面に何も描かずにただスペースキーが押されるのを待ってるのさ。スペースキーを押してみなさい。

B: あ、消えちゃいました。

A: プログラム16行目のwaitkey()が終了したからプログラム全体が終了したのさ。これでおしまい。

B: なんだか素っ気ないですね。

A: 画面に「スペースキーを押してください」とか描けばいいんだろうけどね。そこまでやると関数の作り方以外の解説もしなきゃいけなくなるからな。 B君はすでにVisionEggの基本的な使い方を知っているんだから、pygameじゃなくてVisionEggを使って「スペースキーを押してください」と画面に書いてからwaitkey()するようにプログラムを書きなおせばいい。 これ練習問題ね。

B: うげー。藪蛇藪蛇。

A: さて、これだけではつまらんから、引数と戻り値の練習も兼ねてwaitkey()関数を拡張しよう。 そろそろ分量も多くなってきてブラウザで読んでくださってる方には不便だろうから、いったんここで区切って続きは例題5-2としよう。

------------------------------ つづく ------------------------------