例外処理

例外の捕捉

たとえば、存在しないファイルを読み込もうとするとエラー(例外)が発生する。

import sys

file = sys.argv[1]

content = open(file, "r").read()
size = len(content)
print file + ": " + str(size)
^o^ > python exception1.py sample.dat
Traceback (most recent call last):
  File "exception1.py", line 5, in 
    content = open(file, "r").read()
IOError: [Errno 2] No such file or directory: 'sample.dat'

上の例では sample.dat を読み込もうとしているが、存在しないファイルなので IOError が発生してプログラムが終了している。
ここでプログラムが終了しないようにするには、try ~ except 文を使って例外を適切に処理してやればいい。

import sys

file = sys.argv[1]

try:
    content = open(file, "r").read()
    size = len(content)
    print file + ": " + str(size)
except IOError:
    print "Cannot open file: " + file

例外が発生するかもしれない処理をtry節に書き、発生した例外はexcept節で捕捉する。こうすると例外が発生したときの処理を書くことができる。この例では単純にメッセージを出力して終わっているだけだけど。
実行結果:

^o^ > python exception2.py sample.dat
Cannot open file: sample.dat

except節の処理が実行されているのがわかる。

例外の発生

何らかの理由で例外を発生させたい場合には、raise 文を使う。一番単純な使い方としては 「raise メッセージ」とすればいい。

try:
    raise "Someting error."
except:
    print "Error occured."
^o^ > python exception3.py
Error occured.

この場合、発生する例外は StandardError みたい。特定の種類の例外を発生するには「raise 例外の種類(メッセージ)」という形式にする。

クラス (3)新スタイルクラス

__slots__

Python のクラスには旧スタイルクラスと新スタイルクラスがあるらしい。ややこしいな。
新スタイルクラスを作るには、ビルトイン型を継承するか、もしくは最上位のクラスであるobjectを継承する。

class Person(object):

    __slots__ = ["name", "age"]

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def greeting(self):
        print "Hello, I'm " + self.name + "!"

新スタイルクラスの機能の一つとして、__slots__によってアクセスできるアトリビュートを制限できるというのがある。上の例では、アクセス可能なアトリビュートを name と age に制限している。これで勝手なアトリビュートを追加したりはできなくなったはずだ。やってみよう。

>>> from person2 import Person
>>> andy = Person("Andy", 32)
>>> andy.name
'Andy'
>>> andy.age
32
>>> andy.job = "programmer"
Traceback (most recent call last):
  File "", line 1, in 
AttributeError: 'Person' object has no attribute 'job'

__slots__ で指定している name と age にはアクセスできるけど、job に代入しようとしたら ‘job’なんてアトリビュート無いよ、と怒られた。

プロパティ

ただし、まだ name と age には値を代入できてしまう。

>>> andy.name = "Bill"
>>> andy.name
'Bill'

名前は変わらないのだから、参照はできても代入はできてほしくない。こういうときはプロパティにすればいいらしい。

class Person(object):

    __slots__ = ["_name", "age"]

    def __init__(self, name, age):
        self._name = name
        self.age = age

    def greeting(self):
        print "Hello, I'm " + self._name + "!"

    def get_name(self):
        return self._name

name = property(get_name, None)

名前用のアトリビュートを _name に変更して、参照用の get_name メソッドを作った。そしてproperty関数を使ってnameというアクセサを作っている。property関数の引数にはゲッター、セッターのメソッド指定する。セッターとしては None を指定しているから、これで name は参照だけできるようになったはずだ。

>>> from person2a import Person
>>> andy = Person("Andy", 32)
>>> andy.name
'Andy'
>>> andy.name = "Bill"
Traceback (most recent call last):
  File "", line 1, in 
AttributeError: can't set attribute

name に代入しようとしたら、アトリビュートをセットできない、といわれた。うまくいってるようだ。

でもまてよ。_name アトリビュートにはアクセスできるはずだから、代入もできてしまうんじゃないか?

>>> andy._name = "Bill"
>>> andy.name
'Bill'

できちゃうじゃん。だめじゃん。
じゃあ、__slots__ から _name を取り去ったらどうだ。

class Person(object):

    __slots__ = ["age"]

    def __init__(self, name, age):
        self._name = name
        self.age = age

    def greeting(self):
        print "Hello, I'm " + self._name + "!"

    def get_name(self):
        return self._name

name = property(get_name, None)
>>> from person2b import Person
>>> andy = Person("Andy", 32)
Traceback (most recent call last):
  File "", line 1, in 
  File "person2b.py", line 6, in __init__
    self._name = name
AttributeError: 'Person' object has no attribute '_name'

だめだ。今度は _name アトリビュートがないって怒られた。どうすりゃいいんだ。

クラス (2)継承

クラスの継承

Python ではクラスの継承に多重継承ができる。クラスを継承するには、class 文のクラス名のあとの括弧の中に継承したいクラス名を列挙すればいい。

class Foo:
    def foo(self):
        print "This is foo."

class Bar:
    def bar(self):
        print "This is bar."

class Baz(Foo, Bar):
    def baz(self):
        print "This is baz."

if __name__ == '__main__':
    baz = Baz()
    baz.foo()
    baz.bar()
    baz.baz()

BazクラスはFooクラスとBarクラスを継承していて、加えて独自のメソッドbarを持っている。
これを実行すると:

^o^ > python inheritance.py
This is foo.
This is bar.
This is baz.

FooクラスとBarクラスそれぞれのメソッドが使えることがわかる。

メソッドのオーバーライド

次に、Bazクラスでbarメソッドを書き換え(オーバーライド)てみる。

class Foo:
    def foo(self):
        print "This is foo."

class Bar:
    def bar(self):
        print "This is bar."

class Baz(Foo, Bar):
    def baz(self):
        print "This is baz."

    def bar(self):
        print "This is baz, instead bar."

if __name__ == '__main__':
    baz = Baz()
    baz.foo()
    baz.bar()
    baz.baz()

実行結果:

^o^ > python inheritance2.py
This is foo.
This is baz, instead bar.
This is baz.

barメソッドが書き換わっている。こんな風に、継承したクラス(サブクラス)で継承もとのクラス(スーパークラス)のメソッドをオーバーライドすることができる。

メソッド名の衝突と継承の順番

ところで、多重継承するとメソッド名が衝突することが考えられる。つまり2つのスーパークラスそれぞれに同じ名前のメソッドが定義されていた場合だ。ちょっと試してみよう。

class Foo:
    def hello(self):
        print "Hello, this is Foo."

class Bar:
    def hello(self):
        print "Hello, this is Bar."

class Baz(Foo, Bar):
    pass

if __name__ == '__main__':
    baz = Baz()
    baz.hello()

二つのスーパークラスFooとBarには同じ名前のhelloメソッドがあって、サブクラスBazから呼び出している。実行すると:

^o^ > python inheritance3.py
Hello, this is Foo.

Fooクラスのメソッドが実行された。これはクラスを継承するときにFooを先に書いたせい、なのか?ためしにFooとBarを逆にしてみよう。

class Foo:
    def hello(self):
        print "Hello, this is Foo."

class Bar:
    def hello(self):
        print "Hello, this is Bar."

class Baz(Bar, Foo):
    pass

if __name__ == '__main__':
    baz = Baz()
    baz.hello()

さっきと変わっているのは11行目だけだ。スーパークラスの指定にBarを先に書いている。これを実行すると:

^o^ > python inheritance3a.py
Hello, this is Bar.

今度はBarクラスのメソッドが実行された。スーパークラスでメソッド名が衝突している場合、サブクラスでどちらが呼び出されるのかは、継承するときに列挙した順番によるみたいだ。

クラス

クラスの定義とインスタンスの作成

クラスを定義するには、class 文を使う。もちろん定義の中身はインデントされたブロックになっている。

class Person:
    def __init__(self, name):
        self.name = name

    def greeting(self):
        print "Hello, I'm " + self.name + "!"

if __name__ == '__main__':
    andy = Person("Andy")
    andy.greeting()
    print andy.name

__init__ は特殊メソッドのひとつで、インスタンスの初期化に使われる。Ruby の initialize みたいなもんだな。ここでは name アトリビュート(インスタンス変数みたいなもの)を設定している。
メソッドはもうひとつ、greeting を定義してみた。それにしても、いちいち self を引数に含めなきゃいけないのは面倒だよなぁ。

インスタンスの作成は、クラス名を関数のように呼び出して行う。new とかはない。

実行結果:

^o^ > python person.py
Hello, I'm Andy!
Andy

1行目の出力が greeting メソッドを呼び出した結果で、2行目の出力が name アトリビュートにアクセスした結果だ。

アトリビュートの追加

アトリビュートは Ruby インスタンス変数と違って勝手に新しいものを追加できる。やってみよう。

>>> from person import Person
>>> andy = Person("Andy")
>>> andy.age
Traceback (most recent call last):
  File "", line 1, in 
AttributeError: Person instance has no attribute 'age'
>>> andy.age = 32
>>> andy.age
32

andy.age に代入する前の段階では、アクセスしてもエラー(’age’なんてアトリビュート無いよ)が出ているが、いったん代入すると、今度は代入した値が返ってくる。これは、インスタンスにアトリビュートを追加したのであって、クラスに追加したのではないことに注意。その証拠に別のインスタンスを作ってみても age アトリビュートはない。

>>> bill = Person("Bill")
>>> bill.age
Traceback (most recent call last):
  File "", line 1, in 
AttributeError: Person instance has no attribute 'age'

クラスの定義にないアトリビュートをインスタンスに追加できるというのは面白い。

インスタンスにメソッドを代入する

インスタンスに代入できるのはアトリビュートだけじゃない。メソッドだって代入できる。

>>> andy.hello()
Traceback (most recent call last):
  File "", line 1, in 
AttributeError: Person instance has no attribute 'hello'
>>> andy.hello = andy.greeting
>>> andy.hello()
Hello, I'm Andy!

これってどうなんだよ。さすがに違和感があるぞ。もしかして bill に andy のメソッドを代入もできるのか?

>>> bill.hello = andy.hello
>>> bill.hello()
Hello, I'm Andy!

できた。うわぁ、これは気持ちが悪い。
なんか妙に自由度が高すぎるように思うなぁ。

dir関数

dir 関数を使うとインスタンスの持っているアトリビュートの一覧を得ることができる。

>>> dir(andy)
['__doc__', '__init__', '__module__', 'age', 'greeting', 'hello', 'name']

これをみると、アトリビュート(name、age)とメソッド(greeting、hello)が一緒に並んでいる。要するに Python ではメソッドもアトリビュートの一種ってことらしい。だから代入ができるのも道理ってわけだ。

ジェネレータ

ジェネレータはイテレータオブジェクトを返す関数だ。def を使って通常の関数と同じように定義できるけど、ひとつ違うのは return で値を返す代わりに、yield で計算途中の値を返すことができる点。ジェネレータから返されたイテレータオブジェクトは、nextが呼び出されると yield が出てくるまで計算を進めて値を返す。そしてまたnextが呼び出されると、続きの計算をして yield が出てきた時点でまた値を返す。これを繰り返すわけだ。だから、ジェネレータでは while ループの中で yield を使うのが常套手段、というかそうしないとうまく作れないんじゃないかな。

次の例はフィボナッチ数列を出力する。

def fib():
    a = 1
    b = 1
    while True:
        yield a
        a, b = b, a+b


i = fib()

for c in range(10):
    print i.next()

実行結果:

^o^ > python fib.py
1
1
2
3
5
8
13
21
34
55

うまくいった。この例では10個までしか出力してないけど、コードをよく見ればわかるとおり、終了しない無限ループになっている。言い換えると無限に値を生成できるってこと。これはリストやファイルオブジェクトから作るイテレータとは違うところだ。

イテレータ

Pythonのイテレータは外部イテレータといわれるやつみたい。
iter関数は、イテレート可能オブジェクトをイテレータオブジェクトに変換する。イテレータオブジェクトは次のような性質を持っている。

  • nextメソッドの呼び出しに対して「次の要素」を返す
  • 返す要素がなかったらStopIteration例外を発生する

リストをイテレータオブジェクトに変換して試してみよう。

>>> i = iter([1,2,3])
>>> i.next()
1
>>> i.next()
2
>>> i.next()
3
>>> i.next()
Traceback (most recent call last):
  File "", line 1, in 
StopIteration

イテレータオブジェクトiはnextに対して1~3を順に返し、返す要素がなくなった4回目にはStopIterationが発生している。

実際のプログラミングではforと一緒に使うことが多いようだ。

>>> i = iter(['foo','bar','baz'])
>>> for item in i:
...     print item
...
foo
bar
baz

ファイルオブジェクトもイテレート可能。nextに対して1行ずつ返すイテレータになる。

import sys

filename = sys.argv[1]
file = open(filename, "r")

i = iter(file)

for l in i:
  print l.rstrip("\n")

実行結果:

^o^ > type sample.txt
Mon
Tue
Wed
Thu
Fri
Sat
Sun

^o^ > python iterator.py sample.txt
Mon
Tue
Wed
Thu
Fri
Sat
Sun

実際には、for文では必要なら自動的にイテレータに変換してくれるので、次のようにも書ける:

file = open("sample.txt", "r")

for line in file:
    print line.rstrip("\n")

リスト内包表記

Pythonにはリスト内容表記がある。for~in~を使ってこうやって書く:

>>> [x*x for x in [1,2,3,4,5]]
[1, 4, 9, 16, 25]

表記こそ違うけどやってることはHaskellのリスト内包表記とおなじだ。

forは複数つけることもできる:

>>> [(x,y) for x in [1,2,3,4,5] for y in [1,2,3,4,5]]
[(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (2, 1), (2, 2), (2, 3), (2, 4), (2, 5),
 (3, 1), (3, 2), (3, 3), (3, 4), (3, 5), (4, 1), (4, 2), (4, 3), (4, 4), (4, 5),
 (5, 1), (5, 2), (5, 3), (5, 4), (5, 5)]

これは次の二重のforと同じ。

>>> r = []
>>> for x in [1,2,3,4,5]:
...     for y in [1,2,3,4,5]:
...         r.append((x,y))
...
>>> r
[(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (2, 1), (2, 2), (2, 3), (2, 4), (2, 5),
 (3, 1), (3, 2), (3, 3), (3, 4), (3, 5), (4, 1), (4, 2), (4, 3), (4, 4), (4, 5),
 (5, 1), (5, 2), (5, 3), (5, 4), (5, 5)]

さらに、ifを使って条件を制限できる:

>>> [(x,y) for x in [1,2,3,4,5] for y in [1,2,3,4,5] if x <= y]
[(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (2, 2), (2, 3), (2, 4), (2, 5), (3, 3),
 (3, 4), (3, 5), (4, 4), (4, 5), (5, 5)]

書き換え可能オブジェクトを関数の引数のデフォルト値にするときの注意点

Pythonの関数の引数にはデフォルト値を設定することができて、デフォルト値が設定されている引数は呼び出し時に省略することができる。それはいいんだけど、そのデフォルト値に書き換え可能オブジェクトを設定したときには注意が必要だ。関数の中でこの引数を書き換えてしまうと、奇妙なことが起こる。たとえば次のようなコードの場合だ。

def foo(a = []):
    a.append("bar")
    print a

foo()
foo()
foo()
foo([1,2,3])
foo()

関数fooは省略可能な引数aをとり、aが省略されたときには空のリストを代入するようになっている。ところが、Pyhtonでは引数のデフォルト値の代入は関数が初めて呼び出されたときだけにおこり、しかも以後その値(オブジェクト)が引き継がれる。実行してみよう:

^o^ > python default_arg.py
['bar']
['bar', 'bar']
['bar', 'bar', 'bar']
[1, 2, 3, 'bar']
['bar', 'bar', 'bar', 'bar']

関数fooを全部で5回呼び出してるけど、引数を与えているのは4回目だけでほかは省略している。1回目の呼び出しの結果は簡単に予想できるとおりだ。省略された引数の変わりに空のリストが使われて、関数の中で’bar’が追加されている。2回めの呼び出しの結果は予想と違う。予想では1回目と同じ結果になりそうなものなのに、’bar’が2つある。これは、前回呼び出されたときのオブジェクトを使いまわしているせいだ。1回目の呼び出しで’bar’を1つ追加し、2回目の呼び出しでもう1つ追加している。結果、’bar’が2つあるってわけ。3回目の呼び出しも同様。4回目の呼び出しでは引数を与えているので予想通りの結果になっているけど、5回目の呼び出しではやっぱり’bar’が増えている。

これはないわぁ。何でこんな仕様になってるのか知らないけど、これを避けるためには実質的に引数を省略しないことしかないんじゃないかな。これはうれしくないないなぁ。

変数と書き換え可能オブジェクト

Pythonの代入は、データに名前を紐付けることだ。Rubyとおんなじだな。だから、次のように代入した場合、変数l1とl2は同じリストオブジェクトに紐づいている。

>>> l1 = [1,2,3,4,5]
>>> l2 = l1

だからl1を変更すると、同時にl2も変更されたことになる。

>>> l1.append(6)
>>> l1
[1, 2, 3, 4, 5, 6]
>>> l2
[1, 2, 3, 4, 5, 6]

これは書き換え可能オブジェクトに共通する問題。書き換え可能オブジェクトとはリスト、辞書、setなど。一方で数値や文字列は書き換え可能ではないので、こういう問題は起きない(操作すると必ず新しいオブジェクトが作られる)。

この問題を回避するにはオブジェクトのコピーを作ればいい。コピーをつくるにはcopyモジュールのcopy関数を使う。

>>> import copy
>>> l1 = [1,2,3,4,5]
>>> l2 = copy.copy(l1)
>>> l1.append(6)
>>> l1
[1, 2, 3, 4, 5, 6]
>>> l2
[1, 2, 3, 4, 5]

ところで、文字列が書き換え可能ではないというのは、Rubyとは違う点だ。たとえばリストではインデックスを利用した代入ができるが、

>>> l = [1,2,3,4,5]
>>> l[0] = 0
>>> l
[0, 2, 3, 4, 5]

文字列ではできない。

>>> s = "abcde"
>>> s[0] = "A"
Traceback (most recent call last):
  File "", line 1, in 
TypeError: 'str' object does not support item assignment

ファイルの入出力

ファイルのopenとclose

ファイルをオープンするにはopen関数、クローズするにはcloseメソッドを使う。

f = open("sample.txt", "r")
(何らかの処理)
f.close()

openの戻り値はファイルオブジェクトで、ファイルへの入出力にはこのファイルオブジェクトを使う。
2番目の引数はモードで次のモードが指定できる。

r 読み込みモード
w 書き込みモード。既存のファイルには上書きする
a 追加書き込みモード

ほかにもあるけど省略。

ファイルの読み込み

ファイルの内容すべてをいっぺんに読み込むには、readメソッド。

import sys

filename = sys.argv[1]

file = open(filename, "r")
contents = file.read()
print contents
file.close()

実行結果:

^o^ > type sample.txt
Mon
Tue
Wed
Thu
Fri
Sat
Sun

^o^ > python file_read.py sample.txt
Mon
Tue
Wed
Thu
Fri
Sat
Sun


ファイルを1行ずつ読み込むには、readlineを使う。

import sys

filename = sys.argv[1]

file = open(filename, "r")
while True:
    line = file.readline()
    if not line:
        break
    print line.rstrip("\n")

file.close()

whileループの中で1行ずつ読み込んで出力している。ファイルの最後に到達すると空文字列が返ってくるんだろうか、そこでbreakしてループを抜け出している。
あと、print文は最後に改行を出力するので、lineの最後についている改行文字をrstripで取り去っている。このprintの改行を出力する動作はおせっかいに思えてならない。

readlinesはファイルすべてを行ごとに分割して、文字列のリストを返す。

import sys

filename = sys.argv[1]

file = open(filename, "r")
for line in file.readlines():
    print line.rstrip("\n")

file.close()

大きなファイルでなければこの方がすっきり書ける。

さらに、readlinesを使わなくてもファイルオブジェクト自体をforに渡すことで、1行ずつ処理することができる。このやり方だと大きなファイルでも大丈夫みたい。

import sys

filename = sys.argv[1]

file = open(filename, "r")
for line in file:
    print line.rstrip("\n")

file.close()

ファイルへの書き込み

ファイルへの書き込みは、writeメソッド。

import sys

filename = sys.argv[1]

file = open(filename, "w")
file.write("Hello, world.\n")
file.close()

実行結果:

^o^ > python file_write.py sample.out
^o^ > type sample.out
Hello, world.

writeメソッドは余計な改行文字を出力しないから素直でいいね。

writelinesは文字列のリストを受け取って、ファイルに出力する。

import sys

filename = sys.argv[1]

seq = ["foo\n", "bar\n", "baz\n"]

file = open(filename, "w")
file.writelines(seq)
file.close()

実行結果:

^o^ > python file_writelines.py sample.out
^o^ > type sample.out
foo
bar
baz