M.Hiroi's Home Page

Lightweight Language

新・お気楽 Python プログラミング入門

第 6 回 継承と例外処理

[ PrevPage | Python | NextPage ]

はじめに

前回はオブジェクト指向の基本としてクラス、インスタンス、メソッドについて説明しました。今回はオブジェクト指向機能の目玉ともいえる「継承」と、エラー処理で使われる「例外処理」について説明します。

Python の継承は他のオブジェクト指向言語と比べると、基本的な考え方は同じですが、Python らしくシンプルなものになっています。最初に、一般的なオブジェクト指向言語で使われている継承について簡単に説明します。

●継承

「継承 (inheritance : インヘリタンス)」は簡単に言うとクラスに「親子関係」を持たせる機能です。子供のクラスは親クラスの性質を受け継ぐことができます。プログラミング言語の場合、引き継ぐ性質は定義されたインスタンス変数やメソッドになります。

プログラムを作る場合、今まで作ったプログラムと同じような機能が必要になることがありますが、継承を使うことでその機能を受け継ぎ、新規の機能や変更される機能だけプログラムする、いわゆる「差分プログラミング」が可能になります。

クラスを継承する場合、その元になるクラスを「スーパークラス」とか「ベースクラス」と呼びます。そして、継承したクラスを「サブクラス」と呼びます。この呼び方は言語によってまちまちで統一されていません。C++の場合は、元になるクラスを基本クラスといい、継承するクラスを派生クラスとか導出クラスといいます。

たとえば、クラス Foo1 を継承してクラス Foo2 を定義しましょう。クラス Foo1 にはメソッド bar() が定義されています。クラス Foo2 にメソッド bar() は定義されていませんが、Foo2 のオブジェクトに対して bar() を呼び出すと、スーパークラス Foo1 のメソッド bar() が実行されるのです。

メソッドの選択は次のように行われます。まず、オブジェクトが属するクラス Foo2 にメソッド bar() が定義されているか調べます。ところが、Foo2 には bar() が定義されていないので、スーパークラスである Foo1 に bar() が定義されているか調べます。ここでメソッド bar() が見つかり、それを実行するのです。このように、メソッドが見つかるまで順番にスーパークラスを調べていきますが、最上位のスーパークラスまで調べてもメソッドが見つからない場合はエラーとなります。

継承したクラスのメソッドとは違う働きをさせたい場合、同名のメソッドを定義することで、そのクラスのメソッドを設定することができます。これを「オーバーライド (over ride)」といいます。メソッドを選択する仕組みから見た場合、オーバーライドは必然の動作です。メソッドはサブクラスからスーパークラスに向かって探索されるので、スーパークラスのメソッドよリサブクラスのメソッドが先に選択されるわけです。

●単一継承と多重継承

継承には「単一継承」と「多重継承」の 2 種類があります。単一継承は、ただひとつのクラスからしか機能を継承することができません。したがって、クラスの階層は図 1 のような木構造 [*1] で表すことができます。


  図 1 : 単一継承におけるクラスの階層

継承は何段階に渡って行われてもかまいません。たとえばクラス E の場合、スーパークラスが B で、B のスーパークラスが A に設定されています。サブクラスは複数あってもかまいません。たとえば、A のサブクラスは B, C, D と 3 つ、B のサブクラスは E, F と 2 つあります。図 1 では、クラス A のスーパークラスはありませんが、ほかのクラスではただひとつのスーパークラスを持っています。プログラミング言語では、Smalltalk や Java が単一継承です。

これに対し、多重継承は複数のクラスを継承することができます。このため、クラスの階層は木構造ではなく、図 2 のようなグラフ [*2] で表すことができます。


  図 2 : 多重継承におけるクラスの階層

クラス E に注目してください。スーパークラスには B と C の 2 つがあります。多重継承では、単一継承と同じくサブクラスを複数持つことができ、なおかつ、スーパークラスも複数持つことができるのです。C++や Common Lisp Object System (CLOS) は多重継承をサポートしています。そして、Python も多重継承です。

実をいうと、M.Hiroi は多重継承に対してあまりいいイメージを持っていません。私見ですが、多重継承はメリットよりもプログラムを複雑にするデメリットの方が大きいのではないか、と思っています。特に、図 2 のクラス A, B, C, E のような菱形の関係をC++でプログラムする場合、とても複雑な問題を引き起こすことが知られています。

Python の場合でも、多重継承で問題が発生することがありますが、Python のオブジェクト指向機能がシンプルなぶんだけ、多重継承の複雑さはC++よりも軽減されているように思います。そこで、まず一般的な継承の仕組みについて説明します。

-- note --------
[*1] 木 (tree) は階層的な関係を表すためのデータ構造です。身近な例ではディレクトリ (フォルダ) の階層構造が木にあたります。
[*2] グラフは木をより一般化したデータ構造です。数学のグラフ理論では、いくつかの点とそれを結ぶ線でできた図形を「グラフ」といいます。

●継承の仕組み

一般的なオブジェクト指向言語の場合、継承によって引き継がれる性質は定義されたインスタンス変数やメソッドになります。図 3 を見てください。


  図 3 : 一般的なオブジェクト指向言語における継承

クラス Foo にはインスタンス変数 a, b とメソッド get_a(), get_b() が定義されています。次に、クラス Bar を定義します。Bar は Foo を継承し、Bar 固有のインスタンス変数 c とメソッド get_c() を定義します。Foo と Bar のインスタンスを生成すると、図 3 に示したように、Bar のインスタンスにはクラス Foo で定義された変数 a, b も含まれます。このように、Foo のインスタンス変数が Bar に継承されます。

Foo のインスタンスを生成すると、もちろん変数 a, b は含まれていますが、Bar のインスタンスとメモリを共有することはありません。クラスはオブジェクトの設計図です。設計に共通な部分があったとしても、それから生み出されるインスタンスは別々の実体で、インスタンス変数を共有することはないのです。

クラス Bar にはメソッド get_c() しか定義されていませんが、クラス Foo を継承することにより、メソッド get_a() と get_b() を利用することができます。Bar のインスタンスに対して get_a() を呼び出すと、クラス Bar には get_a() が定義されていないので、スーパークラス Foo を調べ、そこで定義されている get_a() が呼び出されます。もちろん、取り出される値は Bar のインスタンスにある変数 a の値です。このように、Foo のメソッドが Bar に継承されます。

一般のオブジェクト指向言語では、このようにインスタンス変数とメソッドの両方が継承されるのですが、Python はそうではありません。メソッドとクラス変数は継承されますが、インスタンス変数は継承されません。したがって、Python でインスタンス変数を継承するには、明示的にプログラムする必要があります。他のオブジェクト指向言語とは違うことに注意してください。

●単一継承

それでは、具体的に Python の継承を説明しましょう。スーパークラスは、class 文でクラス名の後ろのカッコで指定します。Python は多重継承をサポートしているので、カッコ内に複数のスーパークラスを指定することができます。継承に必要な設定はこれだけです。

簡単な例として、図 3 のクラスを実際にプログラムしてみましょう。まずクラス Foo を定義します (リスト 1)。

リスト 1 : クラス Foo の定義

class Foo:
    def __init__(self, x, y):
        self.a = x
        self.b = y

    def get_a(self): return self.a
    def get_b(self): return self.b

メソッド __init__() でインスタンス変数 a, b を初期化します。メソッド get_a() と get_b() の定義は簡単です。与えられたインスタンスから値を取り出すだけです。次にクラス Bar を定義します (リスト 2)。

リスト 2 : クラス Bar の定義

class Bar(Foo):
    def __init__(self, x, y, z):
        super().__init__(x, y)
        self.c = z

    def get_c(self): return self.c

クラス名 Bar の後ろのカッコにスーパークラス Foo を指定します。オーバーライドしたメソッドからスーパークラスのメソッドを呼び出すときは super() を使います。

super().method(args, ...)

このとき、method() の第 1 引数 (self) には、オーバーライドしたメソッドの self が渡されます。super().__init__(x, y) とすることで、Foo のメソッド __init__() を呼び出すことができます。

スーパークラスの __init__() を呼び出してインスタンス変数 a, b を初期化し、自分のクラスで使うインスタンス変数 c を初期化します。スーパークラスの __init__() を呼び出さない場合は、Bar の __init__() でインスタンス変数 a, b を初期化してください。そうしないと、Foo のメソッドは動作しません。

それでは実行してみましょう。

>>> obj1 = Foo(1, 2)
>>> obj1
<testcls.Foo object at 0x7fc6e7e7dd00>
>>> obj2 = Bar(10, 20, 30)
>>> obj2
<testcls.Bar object at 0x7fc6e7e363d0>
>>> obj1.get_a()
1
>>> obj2.get_a()
10
>>> obj2.get_c()
30

メソッド get_a() は Bar に定義されていませんが、スーパークラス Foo のメソッド get_a() が呼び出されて、インスタンス変数 a の値を求めることができます。また、Bar のインスタンスに対して get_c() を呼び出せば、インスタンス変数 c の値を求めることができます。

もしも、サブクラスで独自のインスタンス変数を使わない場合は、__init__() をオーバーライドする必要はありません。そうすると、スーパークラスの __init__() が自動的に呼び出されます。簡単な例を示します。

>>> class Bar1(Foo):
...     def get_c(self): return self.a + self.b
...
>>> obj3 = Bar1(10, 20)
>>> obj3.get_a()
10
>>> obj3.get_b()
20
>>> obj3.get_c()
30

クラス Bar1 は __init__() をオーバーライドしていません。Bar1 のインスタンスを生成すると、Foo の __init__() が呼び出されて、インスタンス変数 a と b が初期化されます。そして、メソッド get_c() を呼び出すと a と b を足した値を返します。

●クラス変数の継承

クラス変数はメソッドと同様に継承されます。簡単な例を示しましょう。

>>> class Foo:
...     z = 1
...
>>> class Bar(Foo):
...     pass
...
>>> Bar.z
1
>>> a = Foo()
>>> b = Bar()
>>> a.z
1
>>> b.z
1
>>> Foo.z = 10
>>> a.z
10
>>> b.z
10

クラス Foo にはクラス変数 z が定義されています。クラス Bar はクラス変数を定義していませんが、Foo を継承しているので、Foo のクラス変数 z を Bar.z で参照することができます。ただし、Bar.z に代入を行うと、Bar のクラス変数 z が生成されるので、Foo.z を参照することはできなくなります。ご注意ください。

インスタンスからでもスーパークラスのクラス変数にアクセスすることができます。Foo のインスタンスを変数 a に、Bar のインスタンスを変数 b にセットします。a.z と b.z は Foo のクラス変数 z を参照することができます。Foo.z を 10 に書き換えると、a.z と b.z の値も 10 になります。

●データ型の継承

Python の場合、クラス名はデータ型を表す識別子として関数 isinstance() で使うことができます。継承はメソッドやクラス変数だけに作用するのではなく、データ型も継承されます。次の例を見てください。

>>> class Foo: pass
...
>>> class Bar(Foo): pass
...
>>> a = Foo()
>>> b = Bar()
>>> isinstance(a, Foo)
True
>>> isinstance(a, Bar)
False
>>> isinstance(b, Foo)
True
>>> isinstance(b, Bar)
True

クラス Bar はクラス Foo を継承しています。Foo のインスタンス a は isinstance() でチェックすると、当然ですが Foo では True になり、Bar では False になります。ところが、クラス Bar のインスタンス b は、Bar で True になるのは当然ですが、Foo のサブクラスなのでデータ型が継承されて Foo でも True になります。

ここで、Bar を継承したクラス Baz の作って、そのインスタンスを isinstance() でチェックすると、Foo, Bar, Baz のどれでも True になります。

>>> class Baz(Bar): pass
...
>>> c = Baz()
>>> isinstance(c, Foo)
True
>>> isinstance(c, Bar)
True
>>> isinstance(c, Baz)
True

このように、クラスを継承してサブクラスを作ると、サブクラスはスーパークラスの部分集合として考えることができます。図 4 を見てください。


  図 4 : クラスとサブクラスの関係

単一継承の場合、クラスとサブクラスは図 4 の関係になります。サブクラス Bar は Bar や Foo に含まれているので、そのインスタンスに Bar や Foo のメソッドを適用することができるわけです。

なお、isinstance() はクラス名だけではなく、型オブジェクトでもデータ型を判定することもできます。

>>> isinstance(123, int)
True
>>> isinstance(123, float)
False

●制限付き連結リスト

それでは簡単な例題として、前回作成した連結リスト LinkedList を継承して、格納する要素数を制限する連結リスト FixedList というクラスを作ってみましょう。リスト 3 を見てください。

リスト 3 : 制限付き連結リスト

from linkedlist import *

class FixedList(LinkedList):
    def __init__(self, limit, *args):
        self.limit = limit
        self.size = 0
        super().__init__(*args[:limit])

    # データの挿入
    def insert(self, n, x):
        if self.size < self.limit:
            result = super().insert(n, x)
            if result is not None: self.size += 1
            return result
        return None

    # データの削除
    def delete(self, n):
        if self.size > 0:
            result = super().delete(n)
            if result is not None: self.size -= 1
            return result
        return None

FixedList は指定した上限値までしか要素を格納できません。LinkedList で要素を追加するメソッドは insert() で、削除するメソッドは delete() です。この 2 つのメソッドをオーバーライドすることで、FixedList の機能を実現することができます。

FixedList は LinkedList を継承するので、スーパークラスに LinkedList を指定します。__init__() では FixedList で使用するインスタンス変数 limit と size を初期化します。limit は要素数の上限値を表していて、引数 limit で指定します。size は連結リストに格納されている要素数を表します。あとはスーパークラス LinkedList の __init__() を呼び出すだけです。LinkedList の __init__() では insert() を呼び出していますが、この場合は FixedList のメソッドが呼び出されることに注意してください。

insert() では limit と size を比較して、size が limit よりも小さい場合はデータを挿入します。スーパークラスのメソッド insert() を呼び出して、データを挿入できた場合は size を +1 します。delete() の場合、size が 0 よりも大きいときにスーパークラスのメソッド delete() を呼び出します。データを削除できた場合は size を -1 します。これで、連結リストに格納される要素数を管理することができます。

簡単な実行例を示しましょう。

>>> from fixedlist import *
>>> a = FixedList(5, *range(6))
>>> len(a)
5
>>> print(a)
LList(0, 1, 2, 3, 4)
>>> while not a.isEmpty():
...     print(a.delete(0))
...
0
1
2
3
4
>>> for x in range(6):
...     print(a.insert(0, x))
...
0
1
2
3
4
None
>>> print(a)
LList(4, 3, 2, 1, 0)

このように LinkedList を継承することで、FixedList を簡単にプログラムすることができます。

●多重継承

Python は多重継承をサポートしているので、複数のクラスを継承することができます。単一継承の場合、クラスの階層は木構造になるので、適用可能なメソッドの探索はルートの方向をたどるだけですみます。ところが、多重継承の場合はクラスの階層がグラフになるため、適用可能なメソッドに到達する経路が複数存在する可能性があります。このため、複数の経路をうまくまとめてメソッドの適用順序を一意に定める処理が必要になります。

Python では、これを「メソッド解決順序 (Method Resolution Order, MRO)」といいます。MRO のアルゴリズムは Python の言語仕様に定められています。ですが、MRO のアルゴリズムは複雑で、初心者が理解するのはとても難しいと思います。そこで、Common Lisp のオブジェクト指向システム (Common Lisp Object System, CLOS) を参考にして、MRO の概要を簡単に説明することにします。

CLOS は Python と同じく動的で多重継承をサポートしています。Common Lisp の 教科書 によると、CLOS のメソッド適用順序は、次の 3 つの規則を適用した結果とほぼ同じになるとのことです。

  1. 左優先則
  2. 深さ優先則
  3. 合流則

実際に試してみると、この規則は Python にも当てはまるようです。概要を理解するにはこれで十分だと思います。なお、MRO はクラスメソッド cls.mro() で求めることができます。

----- 参考文献 -----
Patrick Henry Winston, Berthold Klaus Paul Horn, 『LISP 原書第 3 版 (1) (2)』, 培風館, 1992

●多重継承の問題点

多重継承を使う場合、異なる性質や機能を持つクラスを継承することがあります。たとえば、クラス Foo にはメソッド method_a() があり、クラス Bar にはメソッド method_b() があるとしましょう。この 2 つのメソッドはまったく異なる働きをします。ここで、method_a() はインスタンス変数 x を使っていて、method_b() も x を使っていると、多重継承で問題が発生します。

クラス Foo と Bar を多重継承してクラス Baz を作成した場合、クラス Baz のインスタンスには変数 x がひとつしかありません。method_a と method_b はひとつしかない変数 x を使うことになります。この場合、どちらかのメソッドは正常に動作しないでしょう。これでは多重継承する意味がありません。これが多重継承の問題点です。

このように、多重継承はどんなクラスでもできるというわけではありません。同名のインスタンス変数を持つクラスは多重継承できないと考えた方がよいでしょう。それから、多重継承にはもうひとつ問題点があります。それはクラスの階層構造が複雑になることです。

単一継承の場合、クラスの階層は木構造になりますが、多重継承ではグラフになります。木構造の場合、クラスの優先順位は簡単にわかりますが、グラフになると優先順位を理解するのは難しくなります。多重継承は強力な機能ですが、使うときには十分な注意が必要なのです。

ちなみにC++の場合、多重継承したクラスに同名のメソッドがある場合、どちらを呼び出すのか明確に指定しないとコンパイルでエラーとなります。またC++はメンバ変数も継承されるため、変数名の衝突も発生します。この場合も、どちらの変数を使用するのか明確に指定しないとコンパイルエラーとなります。

このほかにも、多重継承ではいろいろな問題が発生するため、それを解決するためにC++ではいろいろな機能が用意されています。ところが、それらの機能がC++をいっそう複雑な言語にしていると、M.Hiroi には思えてなりません。C++はコンパイラ型の言語で、なによりも効率を重視するため、Python のようなインタプリタ型の言語よりも複雑な言語仕様になるのは避けられないのかもしれません。

●Mix-in

これらの問題を回避するため、インスタンス変数 (属性) を継承するスーパークラスはひとつだけに限定して、あとのスーパークラスはメソッド (実装) だけを継承するという方法があります。この方法を Mix-in といいます。

具体的には、インスタンス変数を定義せずにメソッドだけを記述したクラスを用意します。属性の継承は単一継承になりますが、実装のみを記述したクラスはいくつ継承してかまいません。ひとつのクラスに複数の実装を混ぜることから Mix-in と呼ばれています。

なお、Mix-in は特別な機能ではなく、多重継承を使いこなすための方法論にすぎません。多重継承を扱うことができるプログラミング言語であれば Mix-in を行うことが可能です。ちなみに、この Mix-in という方法を言語仕様に取り込んだのが Ruby です。

Python は多重継承をサポートしているので、Mix-in を利用することができます。図 6 を見てください。


        図 6 : Mix-in

クラス C はクラス B を継承していて、そこにクラス Mixin A が Mix-in されています。クラス D もクラス B を継承していますが、Mix-in されているクラスは Mixin B となります。

多重継承の問題点は Mix-in ですべて解決できるわけではありませんが、クラスの階層構造がすっきりとしてわかりやすくなることは間違いありません。Mix-in は多重継承を使いこなす優れた方法だと思います。

●Enumerable

それでは Mix-in の例題として、クラス Enumerable を作ってみましょう。Enumerable はコレクションに高階関数 (メソッド) を Mix-in します。これは Ruby のモジュール (Mix-in 用のクラス) Enumerable を参考にしました。追加するメソッドを表 1 に示します。

表 1 : Enumerable のメソッド
名前機能
obj.member(func)func が真となる要素を返す
obj.position(func)func が真となる要素の位置を返す
obj.count(func)func が真となる要素の個数を返す
obj.map(func)要素に func を適用した結果をリストに格納して返す
obj.filter(func)func が真となる要素をリストに格納して返す
obj.fold(func, init)すべての要素を func を用いて結合した結果を返す

プログラムは次のようになります。

リスト 7 : Mix-in 用のクラス Enumerable

class Enumerable:
    # 探索
    def member(self, func):
        for x in self.each():
            if func(x): return x
        return False

    # 位置を返す
    def position(self, func):
        n = 0
        for x in self.each():
            if func(x): return n
            n += 1
        return -1

    # 個数を数える
    def count(self, func):
        n = 0
        for x in self.each():
            if func(x): n += 1
        return n

    # マップ
    def map(self, func):
        a = []
        for x in self.each(): a.append(func(x))
        return a

    # フィルター
    def filter(self, func):
        a = []
        for x in self.each():
            if func(x): a.append(x)
        return a

    # 畳み込み
    def fold(self, func, init):
        a = init
        for x in self.each():
            a = func(a, x)
        return a

クラス Enumerable は Mix-in を前提としているので、インスタンス変数の定義は不要でメソッドだけを定義します。コレクションの要素はジェネレータ each() で取り出します。each() は Mix-in するクラスで定義されているものとします。つまり、ジェネレータ each() を定義さえすれば、どんなクラスでも Enumberable を Mix-in することができるわけです。このプログラムではジェネレータ each() を用いましたがイテレータでもかまいません。

Mix-in するときは次のように行います。

class LinkedList(Enumerable): ...
class FixedList(LinkedList, Enumerable): ...
class MyList(FixedList, Enumeraable) ...

LinkedList に Enumerable を Mix-in する場合、LinkedList のスーパークラスに Enumerable を指定するだけです。この場合、LinkedList を継承した FixedList も Enumerable のメソッドを呼び出すことができます。

また、FixedList に Enumerable を Mix-in したい場合、LinkedList の後ろに Enumerable を指定します。FixedList と Enumerable を継承した新しいクラス MyList を作成することもできます。いずれの場合も、Enumerable のメソッドは LinkedList で定義されている each() を呼び出すことで動作します。

簡単な実行例を示します。

>>> from fixedlist import *
>>> from enumerable import *
>>> class MyList(FixedList, Enumerable):
...     def __init__(self, *args):
...         super().__init__(*args)
...
>>> a = MyList(8, 1, 2, 3, 4, 5, 6, 7, 8)
>>> a
LList(1, 2, 3, 4, 5, 6, 7, 8)
>>> a.member(lambda x: x % 2 == 0)
2
>>> a.position(lambda x: x % 2 == 0)
1
>>> a.count(lambda x: x % 2 == 0)
4
>>> list(a.map(lambda x: x * x))
[1, 4, 9, 16, 25, 36, 49, 64]
>>> list(a.filter(lambda x: x % 2 == 0))
[2, 4, 6, 8]
>>> a.fold(lambda x, y: x + y, 0)
36

正常に動作していますね。複数のクラスで共通の操作 (メソッド) を定義したい場合、Mix-in はとても役に立ちます。

●スタック

次は連結リストを使って「スタック (stack)」という基本的なデータ構造を作ってみましょう。図 7 を見てください。


                   図 7 : スタックの動作例

図 7 はバネがついた容器を表していて、上から品物を出し入れすることができます。初めは空の状態です。ここに品物を乗せると、重さによってバネを圧縮し、品物が容器に格納されます。さらにもうひとつ品物を上に乗せると、さらにバネを圧縮し、その品物も容器に格納することができます。バネが限界まで圧縮されると、もう品物は追加できなくなります。取り出す場合は、上にある品物から行います。ひとつ取り出すと、その分バネが伸びて下にある品物が上に押し出されます。

この容器の動作がスタックの動作なのです。スタックにデータを追加する操作をプッシュ (PUSH) といい、スタックからデータを取り出す操作をポップ (POP) といいます。品物をデータに見立てれば、データ A をスタックにプッシュし (2)、次にデータ B をプッシュします (3)。データを取り出す場合、あとから入れたデータ B が先にポップされ (4)、その次にデータ A がポップされてスタックが空になります (5)。

このように、スタックはあとから入れたデータが先に取り出されるので、後入れ先出し (LIFO : Last-In, First-Out) と呼ばれます。

●継承は is-a 関係を表す

今まで説明したように、オブジェクトは関数とデータをひとつにまとめたものです。オブジェクト指向プログラミングは、このオブジェクトを部品として扱います。実際には、クラス単位でプログラムを作るので、クラス間の関係がとても重要になります。ここで、クラス間の関係 is-a と has-a を簡単に説明します。

is-a 関係は X is a Y. の略で、「X は Y の一種である」という意味になります。X がサブクラスで Y をスーパークラスと考えると、is-a 関係は継承で表すことができます。たとえば、FixedList は格納する要素数に制限がありますが連結リストの一種であることは明らかです。FixedList クラスは LinkedList クラスを継承することで簡単に実装できましたが、それは連結リストとの間に is-a 関係があるからです。

has-a 関係は X has a Y. の略で、「X は Y を持っている」という意味です。たとえば、車にはエンジンやタイヤがありますが、車とエンジンやタイヤに成り立つ関係が has-a です。車はエンジンやタイヤがないと走ることができません。このように、has-a 関係は「X が成立するのに欠かせない要素が Y である」という関係を表しています。

has-a 関係のほかに、is-implemented-using という関係があります。これは X is implemented using Y. の略で、「X は Y を使って実装される」という意味です。たとえば、スタックの場合、配列(リスト)でも連結リストでも実装することが可能です。つまり、Y の種類によらず X を実現できる関係が is-implemented-using 関係なのです。

一般に、has-a 関係や is-implemented-using 関係は、クラス X のインスタンス変数にクラス Y のオブジェクト(インスタンス)を格納することで表します。これを「X は Y を包含している」といいます。そして、これらの関係を表すのに継承を使ってはいけない、ということに注意してください。

たとえば、連結リストを継承してスタックを作ることを考えてみましょう。PUSH は連結リストの先頭にデータを追加することで、POP は連結リストの先頭からデータを取り出すことで簡単に実現できます。しかし、連結リストを継承すると、ほかの操作も可能になります。スタックの途中にデータを追加したり、途中からデータを取り出すなど、スタックを破壊する危険な操作が可能になってしまいます。

また、クラスの関係を考えた場合、スタックと連結リストには is-a 関係は成り立ちません。ところが、継承を使うとデータ型も引き継がれるため、プログラムの上でもスタックは連結リストの一種になってしまいます。継承は強力な機能ですが万能ではありません。クラス間の関係を考えて、適切に使うことが大切です。

●スタックの実装

それでは、実際に連結リストを使ってスタックを実装してみましょう。クラス名は Stack とし、表 2 に示すメソッドを定義します。プログラムはリスト 8 のようになります。

表 2 : スタックのメソッド
メソッド機能
s.push(x) スタックにデータを追加する
s.pop() スタックからデータを取り出す
s.top() スタックの先頭データを返す
s.isEmpty()スタックが空ならば True を返す
リスト 8 : スタック (mystack.py)

from linkedlist import *

class Stack:
    def __init__(self):
        self.content = LinkedList()

    def push(self, x):
        self.content.insert(0, x)

    def pop(self):
        return self.content.delete(0)

    def top(self):
        return self.content[0]

    def isEmpty(self):
        return self.content.isEmpty()

メソッド __init__() でインスタンス変数 content に LinkedList のインスタンスを生成してセットします。あとは、このインスタンスを使ってスタックを実装します。

メソッド push() はスタックにデータ x を追加します。これは連結リストの先頭に x を追加すればいいので、メソッド insert(0, x) を呼び出すだけです。メソッド pop() は連結リストの先頭の要素を削除してそれを返せばよいので、メソッド delete(0) を呼び出すだけです。メソッド top() は先頭のデータを削除せずに返し、メソッド isEmpty() はスタックが空の場合は True を返します。

それでは実行してみましょう。

>>> from mystack import *
>>> a = Stack()
>>> for x in range(5):
...     a.push(x)
...
>>> while not a.isEmpty():
...     print(a.pop())
...
4
3
2
1
0

スタックに 0 から 4 まで push() で格納し pop() でデータを取り出すと 4, 3, 2, 1, 0 になります。このように、スタックは後から入れたデータが先に取り出されます。

●キュー

次は「キュー (queue)」という基本的なデータ構造を作ってみましょう。たとえばチケットを買う場合、カウンタの前に並んで順番を待たなくてはいけません。キューはカウンタの前に並ぶ行列と考えてください。列の先頭にいる人から順番にチケットを買うことができますが、あとから来た人は列の後ろに並ばなくてはいけません。列の先頭まで進むと、チケットを購入することができます。これを表したのが図 8 です。


       図 8 : キューの動作

このように、キューはデータを取り出すときは列の先頭から行い、データを追加するときは列の後ろへ行います。このため、キューは「待ち行列」とか「先入れ先出し (FIFO : first-in, first-out)」と呼ばれます。

キューは連結リストを使って簡単に実装することができますが、大きな欠点もあります。連結リストをキューとして使う場合、データを追加するときに最後尾までセルをたどっていく操作が必要になるため、要素数が多くなるとデータの追加に時間がかかってしまうのです。

そこで、先頭のセルを参照する変数のほかに、最後尾のセルを参照する変数を用意します。こうすると、先頭からセルをたどらなくても、最後尾にデータを追加することができます。図 9 を見てください。


                     図 9 : キューの構造

この変数を front と rear としましょう。図 9 にキューの構造を示します。キューにデータがない場合は、(1) のように front と rear は None になっています。データがある場合は、(2) のように front は先頭のセルを参照し、rear は最後尾のセルを参照しています。これで、データの追加を効率的に行うことができます。

それでは、LinkedList を使わずにセル (Cell) を直接操作してキューを作ってみましょう。定義するメソッドを表 3 に、プログラムをリスト 9 に示します。

表 3 : キューのメソッド
メソッド機能
q.enqueue(x)キューにデータを追加する
q.dequeue() キューからデータを取り出す
q.isEmpty() キューが空ならば True を返す
リスト 9 : キュー (myqueue.py)

class Queue:
    class Cell:
        def __init__(self, data, link = None):
            self.data = data
            self.link = link

    def __init__(self):
        self.size = 0
        self.front = None
        self.rear  = None

    def enqueue(self, x):
        if self.size == 0:
            self.front = self.rear = Queue.Cell(x)
        else:
            new_cell = Queue.Cell(x)
            self.rear.link = new_cell
            self.rear = new_cell
        self.size += 1

    def dequeue(self):
        if self.size == 0:
            raise IndexError
        value = self.front.data
        self.front = self.front.link
        self.size -= 1
        if self.size == 0:
            self.rear = None
        return value

    def isEmpty(self):
        return self.size == 0

メソッド __init__() は変数 size を 0 に、front と rear を None に初期化します。メソッド enqueue() は、キューが空であれば Cell(x) で新しいセルを生成して fornt と rear にセットします。キューが空でない場合は、最後尾にデータを追加します。Cell(x) で新しいセル new_cell を作り、self.rear.link にセットします。これで、最後尾のセルの後ろに new_cell をつなぐことができます。それから、rear を new_cell に書き換えます。

メソッド dequeue() は front が参照するセルの要素を返します。要素を value にセットしてから、front の値を次のセルへの参照 front.link に書き換えます。キューが空になったら、front は None になりますが、rear は None にはなりません。rear の値を None に書き換えます。

簡単な実行例を示します。

>>> from myqueue import *
>>> q = Queue()
>>> for x in range(5):
...     q.enqueue(x)
...
>>> while not q.isEmpty():
...     print(q.dequeue())
...
0
1
2
3
4

キューに 0 から 4 まで enqueue() で格納して、dequeue() でデータを取り出すと 0, 1, 2, 3, 4 になります。スタックとは逆に、キューはデータを入れた順番にデータが取り出されます。

●例外処理

次は Python の「例外 (exception) 処理」について説明します。一般に、例外はエラー処理で使われる機能です。「例外=エラー処理」と考えてもらってもかまいません。最近は例外処理を持っているプログラミング言語が多くなりました。もちろん、Python にも例外処理があります。

たとえば、1 から 100 までの 2 乗と 3 乗の値をファイルに書き込む処理を考えてみましょう。例外処理のないプログラミング言語、たとえばC言語でプログラムする場合、ファイルをオープンする処理、データを書き込む処理、ファイルをクローズする処理でエラーが発生していないかチェックする必要があります。ところが、Python でプログラムするとリスト 10 のようになります。

リスト 10 : データの出力

with open('test.dat', 'w') as out_f:
    for x in range(1, 101):
        out_f.write('{:d}, {:d}, {:d}\n'.format(x, x * x, x * x * x))

エラーをチェックする処理がありません。これは例外処理が働いて、エラーが発生したらプログラムの実行が中断されるからです。例外処理のおかげで、プログラムをすっきりと書くことができます。

なお、エラーが発生したことを「例外が発生した」とか「例外が送出された」という場合もあります。本稿でもエラーのことを例外と記述することにします。

●例外の捕捉

ところで、例外が発生するたびに実行を中断するのではなく、致命的な例外でなければプログラムの実行を継続したい場合もあるでしょう。このような場合にこそ、例外処理が役に立つのです。Python は発生した例外を捕まえるのに try 文を使います。try 文の構文を図 10 に示します。

try:
    処理A
    処理B
    処理C
except except_type:
    処理D
    処理E
else:
    処理F

図 10 : 例外処理

try 文は try: のあとに定義されているブロック (try 節) を実行します。try 節が正常に終了した場合、else 節が定義されていればそれを実行して終了します。except 節は実行しません。例外が発生した場合、try 節の実行は中断され、その例外が except で指定した例外と一致すれば、その except 節を実行します。

except 節にはタプルで複数の例外を指定することができます。また、try 文には複数の except 節を指定することができます。最後に例外を指定しない except 節だけを定義すると、どんな例外にでも一致するワイルドカードとして機能します。

Python の場合、例外はクラスとして定義されています。例外クラスは階層構造になっていて、すべての例外クラスは直接または間接的にクラス BaseException を継承しています。システム終了以外の例外はクラス Exception を継承しています。したがって、except 節に Exception を指定すれば、システム終了以外の例外を捕捉することができます。

たとえば、0 で割ったときには例外 ZeroDivisionError が送出されますが、これはスーパークラス ArithmeticError やそのスーパークラス StandardError で捕まえることができます。逆に、except 節に ZeroDivisionError を指定すれば、それ以外のエラーは捕捉されません。

●try と except の使い方

try 文の使い方は簡単です。リスト 11 を見てください。

リスト 11 : 例外処理の使用例 (sample06.py)

def foo(a, b):
    try:
        return a // b
    except ZeroDivisionError:
        print('Error {:d} // {:d}'.format(a, b))
        return 0

関数 foo(a, b) は a // b を返します。Python の場合、整数 0 で割り算すると例外 ZeroDivisionError を送出して実行を中断します。ここで、try 文の except に ZeroDivisionError を指定すると、例外を捕捉して処理を続行することができます。

実行例を示します。

>>> foo(10, 2)
5
>>> foo(10, 0)
Error 10 // 0
0
>>> foo(10, '1')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/mhiroi/python/sample06.py", line 3, in foo
    return a // b
TypeError: unsupported operand type(s) for //: 'int' and 'str'

foo(10, 2) は 5 を返しますが、foo(10, 0) は 0 で除算しているので例外 ZeroDivisionError が送出されます。この例外は except 節に指定されているので、その節が実行されてError 10 // 0 を表示して 0 を返します。

foo(10, '1') は例外 TypeError が送出されますが、この例外は except 節に指定されていないので、try 文の外側に渡されます。関数 foo() のほかに例外を捕捉する処理はないので、処理を中断してエラーメッセージを表示します。

●例外の送出

例外は raise 文で送出することができます。

raise exception

引数 exception には例外のクラス名、または例外クラスのインスタンスを指定します。クラスが指定された場合は引数なしでインスタンスを生成します。簡単な例を示しましょう。

>>> raise Exception
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
Exception
>>> raise Exception('Oops')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
Exception: Oops

例外に渡した引数は、例外クラスのインスタンス変数 args に格納されます。例外クラスのインスタンスを受け取るときは except 節の後ろに 'as 変数' を定義します。次の例を見てください。

>>> try:
...     raise Exception('oops', 123)
... except Exception as err:
...     print(err.args)
...     print(err)
...
('oops', 123)
('oops', 123)

この場合、例外のインスタンスは変数 err にセットされ、例外の引数は err.args でアクセスすることができます。Exception にはメソッド __getitem__() と __str__() が定義されていて、角カッコ [ ] で args の要素にアクセスすることができ、print() で err を表示すると args の値が表示されます。

●例外の定義

ユーザーが独自のエラークラスを定義するときはクラス Exception を継承するといいでしょう。次の例を見てください。

>>> class FooError(Exception):
...     pass
...
>>> raise FooError('oops!')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
__main__.FooError: oops!

例外クラスの名前は最後に Error を付けるのが Python の習慣です。FooError は Exception を継承しているので、インスタンス変数やメソッドを定義しなくても動作します。独自の処理を行う場合は、インスタンス変数やメソッドを定義する必要がありますが、これは本稿の範囲を超えるので説明を割愛いたします。詳細は Python のマニュアルをお読みください。

●大域脱出

Python の例外は、try 節の中で呼び出した関数の中で例外が送出されても、それを捕捉することができます。この機能を使って、評価中の関数からほかの関数へ制御を移す「大域脱出 (global exti)」を実現することができます。

簡単な例を示しましょう。

>>> class ExitError(Exception): pass
...
>>> def bar1(): print('call bar1()')
...
>>> def bar2(): raise ExitError('Global Exit')
...
>>> def bar3(): print('call bar3()')
...
>>> def foo():
...     bar1()
...     bar2()
...     bar3()
...
>>> try:
...     foo()
... except ExitError as err:
...     print(err)
...
call bar1()
Global Exit

実行の様子を図 11 に示します。

通常の関数呼び出しでは、呼び出し元の関数に制御が戻ります。ところが bar2() で raise が実行されると、呼び出し元の関数 foo() を飛び越えて、制御が try 文の except 節に移るのです。このように、例外処理を使って関数を飛び越えて制御を移すことができます。

例外処理を使った大域脱出はとても強力な機能ですが、多用すると処理の流れがわからなくなる、いわゆる「スパゲッティプログラム」になってしまいます。使用には十分ご注意下さい。

●finally 節

ところで、プログラムの途中で例外が送出されると、残りのプログラムは実行されません。このため、必要な処理が行われない場合があります。たとえば、ファイルの入出力処理の場合、最初にファイルをオープンし最後でファイルをクローズしなければいけません。ファイルを関数 open() でオープンして関数 close() でクローズする場合、例外で処理が中断されるとファイルをクローズすることができません。

このような場合、try 文の excetp 節のかわりに finally 節を定義することで解決できます。finally 節は try 節で例外が発生したかどうかにかかわらず、try 文の実行が終了するときに必ず実行されます。例外が発生した場合は、finally 節を実行したあとで同じ例外を再送出します。finally 節は例外だけではなく、return などで try 文を終了する場合でも必ず実行されます。

簡単な例を示しましょう。大域脱出で作成した foo() を呼び出す関数 baz() を作ります。

>>> def baz():
...     try:
...         foo()
...     finally:
...         print('claen up')
...
>>> try:
...     baz()
... except ExitError as err:
...     print(err)
...
call bar1()
claen up
Global Exit

関数 bar2() で送出された ExitError は baz() の finally 節で捕捉されて print('clean up') が実行されます。その後、ExitError が再送出されて、対話モードで入力した except 節に捕捉されて Global Exit と表示されます。

●with 文

なお、finally 節のようなクリーンアップ処理は with 文でも実装することができます。with 文の構文を以下に示します。

with expr as var, expr1 as var1, ...:
    処理
    ...

expr as var を複数続けて記述すると、with 文を入れ子にしたことと同じになります。

with 文を使うには次に示す 2 つの特殊メソッドを実装したクラスが必要になります。

def __enter__(self): ...
def __exit__(self, exc_type, exc_value, traceback): ...

これを「コンテキストマネージャ」といいます。with 文は式 expr を実行してコンテキストマネージャのオブジェクトを生成します。次に、オブジェクトのメソッド __enter__() を実行します。この返り値が変数 var にセットされます。通常は自分自身 (self) を返します。

with 文の処理が終了するとき、メソッド __exit__() が呼び出されます。例外が送出された場合も __exit__() が呼び出されます。正常終了した場合、3 つの引数には None が渡されます。異常終了した場合、3 つの引数には例外の型、値、トレースバック情報が渡されます。返り値が偽の場合、同じ例外が再送出されます。真の場合、例外は再送出されません。

簡単な使用例を示します。

リスト : with 文の簡単な使用例 (testwith.py)

class Foo:
    def __init__(self, a):
        self.a = a
        print("init")

    def __enter__(self):
        print("enter")
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print("exit")
        print(exc_type)
        print(exc_value)
        print(traceback)
        return True

with Foo(123) as a:
    print(a.a)

with Foo(0) as a:
    raise Exception('oops!', a.a)
$ python3 testwith.py
init
enter
123
exit
None
None
None
init
enter
exit
<class 'Exception'>
('oops!', 0)
<traceback object at 0x7f0dd9eaec40>

●おわりに

6 回にわたって簡単なプログラムを作りながら Python のプログラミングについて説明しました。Python は奥の深いプログラミング言語なので、本稿ですべての機能を説明するのは不可能ですが、Python の基本からオブジェクト指向機能まで一通り説明することができたのではないかと思っています。

一昔前、スクリプト言語といえば Perl でしたが、今では JavaScript, PHP, Python, Ruby などいろいろな言語が開発されています。その中でもシンプルでわかりやすい Python は、多くの方にとって有用な開発ツールになると思います。今後 Python は海外だけではなく、日本でもいっそう普及していくことでしょう。最後に、本稿が Python に関心を持たれている読者の参考になれば幸いです。


初版 2006 年 2 月 25 日
新版 2022 年 8 月 27 日

Copyright (C) 2006-2022 Makoto Hiroi
All rights reserved.

[ PrevPage | Python | NextPage ]