モンキーPython (Python3対応): 第3回 分岐・繰り返しなどとファイル操作 後編

今回の目次:

  1. ファイル操作
    • 実行時エラーを捕まえる
    • 1行ずつ読み込む
  2. データ型 - dict (辞書)
  3. お絵かきプログラムにファイル保存を付ける
    • データを記録する
    • ファイル保存するには
    • データを読み込む
  4. 今回のまとめ

@ ファイル操作

前回のお絵かきプログラムでは、絵を描くことはできても、それを保存することができませんでした。今回は、ファイル操作機能を付けてみます。

Pythonで, ファイルからデータを読み込んだり、ファイルにデータを書き込んだりするには、open()関数を呼び出して、ファイルを開きます。

03-10.py は、ファイル「data」の内容を画面に表示します。データファイル「data」の内容は、次のとおりとします。

1ほげ
2ふが

03-10.py

Python
[RAW]
  1. try:
  2. fp = open("data", "r", encoding = "UTF-8") # (1)
  3. except FileNotFoundError: # 見つからないとき
  4. print("ファイルが見つからない")
  5. exit() # プログラムを終了
  6. lines = fp.readlines() # (2)
  7. print(lines)
  8. fp.close() # (3)
実行結果:
$ python3 03-10.py
['1ほげ\n', '2ふが\n']
このスクリプトの説明:
  1. ファイルを読んだり、書いたりするときは、まず, ファイルを開きます。

    open() でファイルを開きます。最初の引数としてファイル名を、2番目の引数としてファイルを開くモードを指定します。

    モードは、読み込む場合は"r"、書き込む場合は"w"を指定します。"w"を指定すると、元もとのファイルの内容は失われます。

    また、encoding引数で、ファイルのテキストの文字コードを指定します。文字コードが正しくないと, UnicodeErrorUnicodeDecodeError が発生します。

    open() は戻り値として, ファイルアクセスのためのオブジェクトを返します。

  2. readlines メソッドは、ファイルの内容をすべて読み込んで、行(文字列)を要素とするリストを作り、戻り値としてそのリストを返します。
  3. 開いたファイルは, かならず閉じなければなりません。closeメソッドで、ファイルを閉じます。

■実行時エラーを捕まえる

プログラムを実行しないと分からないエラーがあります。たとえば、ファイルを開こうとして、ファイルが見つからない場合など。このようなエラーは、プログラムの開始時点では分かりません。

Pythonでは、このような実行時エラーが発生すると、例外が送出されます。ソースコードを書くときには, 例外を受け止めて、エラーメッセージを表示してやり直すなどの対策を取らなければなりません。

try文を使います。文法は次のとおりです。

try:
    文...
except 例外型名 [as 変数名]:    # except節は0個以上
    文...
except ( 例外型名 [, 例外型名 ...] ) [as 変数名]   # 複数の例外を受け取りたいとき
    文...
[finally:
    文... ]

try節の文で例外が発生すると、そこで処理が中断され, except節に跳んで、except節の文が実行されます。例外型名でどの例外を受け取るか指定します。ここでエラーメッセージを表示したりします。

except節の実行が終わると、try文全体から抜けます。

try節のなかで例外が発生しなかったときは、except節はまったく実行されません。

finally節は、try節であれexcept節であれ、try文から抜け出るときに、必ず実行されます。try節内で例外発生、return, break あるいは continue文が実行されても, です。

try節で例外が発生したときは、except節があれば, except節を実行したあとに finally節に制御が移り、try文から抜けます。except節がなければ, finally節が実行された後で、例外が送出されます。

■1行ずつ読み込む

readlines()は、すべての内容を読み込むものでしたが、readline() とすると, 1行ずつ読み込めます。また、for文の式リストでファイルアクセスのオブジェクトを書いても、1行ずつ読み込むことができます。

サンプル 03-11.py は、ファイルから1行ずつ読み込んで、それを表示します。sale200412.csvの内容は、次のとおりです。よくあるCSV (カンマ区切り) 形式です。

"品名","販売金額"
"テレビ",12000000
"DVDプレイヤー",6000000

03-11.py

Python
[RAW]
  1. # -*- coding:utf-8 -*
  2. fp = open("sale200412.csv", "r", encoding="UTF-8")
  3. print( fp.readline() ) # ヘッダを表示
  4. for line in fp:
  5. k, v = line.split(',') # (1)
  6. item = k.replace('"', '') # (2)
  7. print( item, " => ", int(v) )
  8. fp.close()
実行結果
"品名","販売金額"

テレビ  =>  12000000
DVDプレイヤー  =>  6000000
このスクリプトの説明:
  1. str#split(sep) は、文字列をsepで分割してリストにして、そのリストを返します。ここではカンマで分割して、品名をkに、金額をvに代入しています。
  2. str#replace(old, new)は、文字列の中に含まれるoldをすべてnewに置換します。これで品名に含まれる「"」を取り除きます。

@ データ型 - dict (辞書)

もう少し実用的になりそうな例を書いてみます。サンプル 03-12.py は、二つの販売実績ファイルの金額を足し合わせて表示します。

03-12.py

Python
[RAW]
  1. # -*- coding:utf-8 -*
  2. files = ["sale200412.csv", "sale200501.csv"]
  3. sales = {} # (1)
  4. for file_year_mon in files:
  5. fp = open(file_year_mon, "r")
  6. fp.readline() # ヘッダを捨てる
  7. for line in fp:
  8. k, v = line.split(',')
  9. k = k.replace('"', '')
  10. if k in sales: # (2)
  11. sales[k] += int(v)
  12. else:
  13. sales[k] = int(v)
  14. fp.close()
  15. for item in sales:
  16. print( item, " => ", sales[item] )

"sale200501.csv"の内容

"品名","販売金額"
"テレビ",120000
"DVDプレイヤー",60000
"DVDレコーダ",10000
実行結果
$ python3 03-12.py 
DVDレコーダ  =>  10000
DVDプレイヤー  =>  6060000
テレビ  =>  12120000
このスクリプトの説明:
  1. salesは、辞書 (dict) オブジェクト(後述)で、各ファイルの金額を加えていきます。
  2. このif文では、読み込んだファイルの品名がすでに存在しているか判定します。すでに存在していれば金額を加算し、そうでなければelse:以下を実行し、新たに金額を設定します。

タプルやリストでは、特定の要素を取り出すときには、その格納位置を指定します。しかし、格納するオブジェクトの名前など、何か意味のある値をキーにして、それに対応する値(要素)を取り出すようにしたほうがプログラムが分かりやすくなる場合がよくあります。

先ほどのサンプルでも、品名をキーにしました。そのようなときに使うのが辞書 (dict型)です。

辞書を生成するときは、

  {キー: 値, ...}
という形で書きます。空の辞書を生成するときは、
  {}
とします。値を取り出すときは、オブジェクト[キー]と書いてキーを指定します。値を追加するときは、
  オブジェクト[キー] = 値
とします。

次のスクリプトは、辞書の動作を確認します。

03-13.py

Python
[RAW]
  1. # -*- coding:utf-8 -*-
  2. hash = {"key1": "ほげほげ", 2: "foobar"}
  3. print( hash["key1"] ) #=> "ほげほげ"
  4. print( hash[2] ) #=> "foobar"
  5. hash["a"] = "foo"
  6. print( hash["a"] ) #=> "foo"

キーの型をいろいろ変えてもエラーにはなりませんが、プログラムの見通しが悪くなるため、普通は一つの辞書ではキーの型は固定します。

一つのキーに対して複数のオブジェクトを結びつけることはできません。また、タプル、リストと違い、先頭から順番に取り出そうにも、どのような順序かはやってみないと分かりません。

@ お絵かきプログラムにファイル保存を付ける

では最後に、少し長くなりますが、お絵かきプログラムに保存、読み込み機能を付けてみましょう。

03-14.py ダウンロード後、拡張子を.pyにしてください。

■データを記録する

マウスをボタンを押したときやドラッグしたりしたときの座標を記録していきます。

Python
[RAW]
  1. # ボタンが押された
  2. def on_pressed(self, event):
  3. # 追加
  4. self.current_line = [(self.color.get(), self.width.get()),
  5. (event.x, event.y)] # (1)
  6. self.data.append(self.current_line)
  7. # 追加ここまで
  8. self.sx = event.x; self.sy = event.y
  9. self.canvas.create_oval(self.sx, self.sy, event.x, event.y,
  10. outline = self.color.get(),
  11. width = self.width.get())
  12. # ドラッグ
  13. def on_dragged(self, event):
  14. # 追加
  15. self.current_line.append( (event.x, event.y) ) # (2)
  16. # 追加ここまで
  17. self.canvas.create_line(self.sx, self.sy, event.x, event.y,
  18. fill = self.color.get(),
  19. width = self.width.get())
  20. self.sx = event.x; self.sy = event.y

1. マウスのボタンを押したとき (on_pressed()) に、色、太さ、開始点を記録します。

現在、現に引いている線は self.current_line に、すべてのデータは self.data に保存します。

2. list#appendメソッドで、そのときそのときの座標をリストの末尾に加えていきます。

on_dragged()内では、current_lineに加えていますが、ここでは, 以前見たようにオブジェクトが共有されることを利用しています。dataの末尾の要素は current_line と同じオブジェクトを指しているので、これで data も更新されることになります。

■ファイル保存するには

ファイルに保存する部分です。

ファイル形式は, できるだけ単純にするために、次のようなテキストファイルで保存することにします。色名、太さ、点の位置の並びです。点の位置が2つ以上あるときは線を引くことにします。改行で線を区切ります。

red;5;146 110
red;5;214 109
red;5;76 201;75 206;71 214;67 227;64 245;60 255;59 270;56 278;56 279
Python
[RAW]
  1. # ファイルに保存する
  2. def on_save(self):
  3. filename = tkinter.filedialog.asksaveasfilename(title = "保存",
  4. filetypes = [('scribbleファイル', '.data')],
  5. initialdir = self.path_name) # (1)
  6. if filename == "":
  7. return
  8. fp = open(filename, "w") # (2)
  9. for path in self.data:
  10. fp.write(path[0][0] + ';' + str(path[0][1])) # (3)
  11. for dot in path[1:]:
  12. fp.write(';' + str(dot[0]) + ' ' + str(dot[1]))
  13. fp.write('\n')
  14. fp.close()

on_save() は新しいメソッドです。ユーザーが「保存」ボタンを押すと呼び出されます。

1. tkinter.filedialog.asksaveasfilename()で、ファイル選択ダイアログを開きます。

今回のサンプルでは、毎回ファイル名を選ぶようになっています。上書き保存できるようにするのは宿題にします。

2. 描画のデータを出力するために、open()"w"で、ファイルを書き込みモードで開きます。

3. ファイルに書き込むメソッドは、write()です。線データの先頭には、on_pressed() で色名と太さを格納していたので、まずそれから出力します。それ以外は点データなので、for文で繰り返して、座標を出力します。

■データを読み込む

データを読み込むところは、ちょっと長いです。

Python
[RAW]
  1. # @return 線のデータ
  2. def parse_line(self, line):
  3. if not isinstance(line, str):
  4. raise ValueError("list must be a str")
  5. d = line.split(';')
  6. cur = [ (d[0], int(d[1])) ] # (color, width)
  7. for dot in d[2:]:
  8. x, y = dot.split(' ')
  9. cur.append( (int(x), int(y)) )
  10. return cur
  11. # 線 (または点) を引く
  12. def draw_line(self, color, width, dots):
  13. # 空のときは何も描かない
  14. if len(dots) == 0:
  15. return
  16. elif len(dots) == 1:
  17. x = dots[0][0]; y = dots[0][1]
  18. self.canvas.create_oval(x, y, x, y, outline = color, width = width)
  19. return
  20. # 2以上のとき => 線
  21. sx = None; sy = None
  22. for dot in dots:
  23. x = dot[0]; y = dot[1]
  24. if sx:
  25. self.canvas.create_line(sx, sy, x, y, fill = color,
  26. width = width )
  27. sx = x; sy = y
  28. def on_load(self):
  29. filename = tkinter.filedialog.askopenfilename(title = "開く",
  30. filetypes = [('scribbleファイル', '.data')],
  31. initialdir = self.path_name)
  32. if filename == "":
  33. return
  34. try:
  35. fp = open(filename, "r")
  36. except IOError as e:
  37. print(e)
  38. return
  39. self.data = []
  40. self.canvas.delete("all") # (1)
  41. for line in fp:
  42. cur = self.parse_line(line) # (2)
  43. self.data.append(cur)
  44. color = cur[0][0]; width = cur[0][1]
  45. self.draw_line(color, width, cur[1:]) # (3)
  46. fp.close()

1. これでキャンバスを白紙に戻します。self.data もクリアしておきます。

2. ファイルから1行ずつ読み込んで、parse_line() で解釈して, 色、太さ、座標に分解します。

分解した内容 (1本の線の情報) をdataに追加していきます。

3. 読み込んだ線データで、draw_line() を呼び出し, 線を引きます。

■メニューバー

今回はメニューバーを付けました。create_window() の最初の方です。

Python
[RAW]
  1. # ウィンドウを作る
  2. def create_window(self):
  3. window = tkinter.Tk()
  4. # メニュー
  5. m0 = tkinter.Menu(window)
  6. window.configure(menu = m0)
  7. # under は [Alt] + ? を表す label 上の位置
  8. m0.add_command(label = '開く(O)...', under = 3, command = self.on_load)
  9. m0.add_command(label = '保存(S)...', under = 3, command = self.on_save)
  10. m0.add_command(label = '終了(X)', under = 3, command = window.quit)

これは見てのとおりです。

@ 今回のまとめ

以上で、プログラムを作るうえで基本となるところは、一通り学習できました。あとは、リファレンスマニュアルやいろいろなサンプルを見て、作れるプログラムの幅を拡げていくといいでしょう。

次回まわし

--------------------------------------------------------------------------------------- 以降は、次回以降に。   ・ネットワーク(できるか?)   ・正規表現 ●イテレータ ●ジェネレータ ●多態  パラダイムの転換  ■ラジオボタン  オプションメニューの代わりに、ラジオボタンを使うこともできます。ラジオボタンの場合は、次のようになります。for文は、一定回数繰り返すもので、後述します。 def color_chooser_by_radio_buttons(self, window): COLORS = [ ("赤", "red"), ("緑", "green"), ("青", "blue"), ("黄色", "yellow"), ("黒", "black") ] self.color = Tkinter.StringVar() self.color.set(COLORS[0][1]) for text, color in COLORS: b = Tkinter.Radiobutton(window, text = text, variable = self.color, value = color) b.pack(side = Tkinter.LEFT) ●正規表現  これまで見てきたオブジェクトは何もしなくても使えるものでしたが、Pythonインタプリタに組み込まれていないライブラリの機能を使うときは、importする必要があります。 import ライブラリ名  正規表現は、文字列のマッチングに使う表記法で、これを使ってあいまい検索などを行えます。いくつかの文字に特別な意味を持たせ、それを組み合わせることで検索するパターンを指定します。このような特別な文字のことをメタ文字といい、次のようなものがあります。 . 改行以外の文字1文字 ^ 文字列・行の先頭 $ 文字列・行の末尾 * 直前パターンの0回以上の繰り返し + 直前パターンの1回以上の繰り返し ? 直前パターンが0または1回 [] 文字の選択 | A|Bで、AまたはB () グルーピング  次のスクリプトは、文字列を検索します。search(pattern, string)は、stringの中で、patternにマッチするか調べます。search()の戻り値のオブジェクトは、ライブラリリファレンスマニュアル 4.2.5 MatchObjectオブジェクト のメソッドを持ちます。  日本語の文字列を文字単位で検索するときは、Unicodeモードにする必要があります。 [[re-test.py]] import re print re.search("[a-z]", u"あいfffうえ").start(0) #=> 2 print re.search("b|c", "aaaczzz").start(0) #=> 3 print re.search(u"漢+t", u"漢漢!tt漢漢t").start(0) #=> 5 --------------------------------------------------------------------------------  GIMPなどのプログラムを思い描いてみてください。ファイルからデータを読み込み、メモリに保持し、そこで編集を行います。意味のあるプログラムを作ろうとすると、データをメモリ上に置いておく必要があります。  プログラミング言語によって、メモリ上のデータの見方は変わります。Pythonはオブジェクト指向と呼ばれる一団に属しており、Pythonスクリプトから見えるメモリは、オブジェクトで満たされています。  以下で基本的なオブジェクトを見ていきますが、いずれも、中身が違っても同じ種類のものであれば、同じように操作できます。このオブジェクトの種類のことを(データ)型、あるいはクラスといいます。