現在地: ホーム Dive Into Python 3

難易度: ♦♦♦♢♢

クラスとイテレータ

東は東、西は西、二つは決して交わらない。
ラディヤード・キプリング

 

飛び込む

イテレータはPython 3の「隠し味」だ。イテレーターはどんなところにもいて、あらゆるものの礎をなしているが、その姿はいつも目に見えない。内包表記イテレータの単純形に過ぎないし、ジェネレータもイテレータの単純形に過ぎない。値をyieldする関数というのは、イテレータを組み立てることなしにイテレータを作るコンパクトで上手い方法なのだ。私が何を言っているのかについて説明していこう。

フィボナッチジェネレータを覚えているだろうか? あれをゼロから作ったイテレータにすると以下のようになる:

[fibonacci2.pyをダウンロードする]

class Fib:
    '''iterator that yields numbers in the Fibonacci sequence'''

    def __init__(self, max):
        self.max = max

    def __iter__(self):
        self.a = 0
        self.b = 1
        return self

    def __next__(self):
        fib = self.a
        if fib > self.max:
            raise StopIteration
        self.a, self.b = self.b, self.a + self.b
        return fib

それでは一行ずつ見ていこう。

class Fib:

class? クラスって何だ?

クラスを定義する

Pythonは完全にオブジェクト指向の言語なので、独自のクラスを定義したり、自作のクラスや組み込みのクラスを継承したり、自分で定義したクラスをインスタンス化したりできる。

Pythonではクラスを簡単に定義できる。関数でもそうだったように、クラスを定義する場所が他と区別されているなどということはない。ただクラスを定義して、コードを書き始めよう。Pythonのクラス定義は、classという予約語から始まり、その後ろにクラス名を書く。形の上で言えば、クラスは別に他のクラスを継承しなくてもよいので、定義するのに最低限必要なものはこれだけだ。

class PapayaWhip:  
    pass           
  1. このクラスの名前はPapayaWhipであり、このクラスは他のクラスを継承していない。クラス名は、EachWordLikeThisのように各単語の先頭を大文字にするのが普通だ。ただしこれは単なる慣例であり、要求ではない。
  2. ご想像の通り、クラスの中にあるものは、関数、if文、forループ、その他すべてのコードブロックと同様に、すべてインデントされる。

このPapayaWhipクラスはメソッドや属性を一切定義していないが、構文上は定義の中に何かが存在する必要があるため、このpass文を置いている。pass文はPythonの予約語であり、「そのまま進んでくれ、ここには何もないよ」という意味を表す。この文は何も行わないので、関数やクラスのスタブを作るときに便利なプレースホルダとして使うことができる。

pass文は、JavaやC言語における空っぽの波括弧({})のようなものだ。

多くのクラスは他のクラスを継承するが、このクラスは何も継承していない。多くのクラスはメソッドを定義するが、このクラスは何も定義していない。名前を除けば、Pythonのクラスが絶対に持っていなければならないものなど無いのだ。特にC++のプログラマは、Pythonのクラスが明示的なコンストラクタやデストラクタを持っていないことを不思議に思うかもしれない。必須ではないが、Pythonのクラスはコンストラクタに似た__init__()メソッドを持つことができる

__init__()メソッド

次のコードでは__init__メソッドを使ってFibクラスを初期化している。

class Fib:
    '''iterator that yields numbers in the Fibonacci sequence'''  

    def __init__(self, max):                                      
  1. モジュールや関数と同様に、クラスもdocstringを持つことができる(できるかぎり持たせよう)。
  2. __init__()メソッドは、クラスのインスタンスが生成されるとすぐに呼び出される。このメソッドのことをクラスの「コンストラクタ」と呼びたくなるかもしれないが、それは厳密には誤りだ。確かに、これはC++のコンストラクタと似たものに見えるし(慣例上、__init__() メソッドはクラスの一番初めに定義される)、同じような処理をするし(新しく作成されたインスタンスのなかで一番最初に実行される)、名前自体もそれっぽい。しかし、そう呼ぶのは正しくない。なぜなら、__init__()メソッドが呼び出されたときにはオブジェクトはすでに作成されており、そのクラスの新しいインスタンスへの有効な参照をあなたはすでに持っているからだ。

__init__()メソッドを含むすべてのメソッドの最初の引数は、現在のインスタンスへの参照だ。慣例により、この引数にはselfという名前を付ける。この引数は、C++やJavaにおける予約語thisと同じ役割を果たすが、selfはPythonの予約語ではなく、単に慣例上そう名付けられているだけでしかない。そうは言っても、これにself以外の名前を付けないでほしい。これは非常に強い慣例なのだ。

__init__()メソッドの中では、selfは新しく作成されたオブジェクトを指している。それ以外のメソッドでは、selfはメソッドが呼び出されたインスタンスを参照している。メソッドを定義するときはself引数を明示的に書く必要があるのだが、メソッドを呼び出すときにはこの引数を与えてはならない。Pythonが自動的に付け加えてくれるのだ。

クラスをインスタンス化する

Pythonではクラスを簡単にインスタンス化できる。クラスをインスタンス化するには、__init__()が要求する引数を渡して、クラスを関数のように呼び出すだけで良い。すると、戻り値として新しく生成されたオブジェクトが返される。

>>> import fibonacci2
>>> fib = fibonacci2.Fib(100)  
>>> fib                        
<fibonacci2.Fib object at 0x00DB8810>
>>> fib.__class__              
<class 'fibonacci2.Fib'>
>>> fib.__doc__                
'iterator that yields numbers in the Fibonacci sequence'
  1. Fibクラス(fibonacci2 モジュールで定義されている)の新しいインスタンスを作り、それを変数fibに代入している。100という引数を1つ渡しているが、これは結局Fibクラスの__init__()メソッドでmax引数として扱われる。
  2. fibFibクラスのインスタンスになった。
  3. どんなクラスのインスタンスも組み込み属性の__class__を持っているが、この属性はそのオブジェクトのクラスを指している。Javaのプログラマなら、getName()getSuperclass()といったオブジェクトのメタデータ情報を得るためのメソッドが入ったClassクラスに馴染みがあるかもしれない。Pythonでは属性を通じてこの種のメタデータに参照することになるのだが、その考え方自体は同じだ。
  4. インスタンスのdocstringには、関数やモジュールの場合と同じやり方でアクセスできる。ある1つのクラスのインスタンスはすべて同じdocstringを持つ。

Pythonでは関数のようにクラスを呼び出すことでクラスの新しいインスタンスを作成できる。C++やJavaにあるような明示的なnew演算子は存在しない。

インスタンス変数

次の行へ進もう:

class Fib:
    def __init__(self, max):
        self.max = max        
  1. self.maxというのは何だろう? これはインスタンス変数だ。__init__()メソッドに引数として渡されたmaxとはまったく違う。self.max は、このインスタンスにおいて「グローバル」だ。要するに、他のメソッドからこの変数にアクセスできるのだ。
class Fib:
    def __init__(self, max):
        self.max = max        
    .
    .
    .
    def __next__(self):
        fib = self.a
        if fib > self.max:    
  1. self.maxは、__init__()メソッドで定義されて……
  2. ……__next__()メソッドで参照される。

インスタンス変数は、クラスの個々のインスタンスに固有だ。例えば、2つの Fibインスタンスを異なる最大値を与えて作った場合、この2つはそれぞれの値を保持することになる。

>>> import fibonacci2
>>> fib1 = fibonacci2.Fib(100)
>>> fib2 = fibonacci2.Fib(200)
>>> fib1.max
100
>>> fib2.max
200

フィボナッチイテレータ

ようやくイテレータの作り方を学ぶ準備ができた。イテレータとは__iter__()メソッドを実装した単なるクラスだ。

[fibonacci2.pyをダウンロードする]

class Fib:                                        
    def __init__(self, max):                      
        self.max = max

    def __iter__(self):                           
        self.a = 0
        self.b = 1
        return self

    def __next__(self):                           
        fib = self.a
        if fib > self.max:
            raise StopIteration                   
        self.a, self.b = self.b, self.a + self.b
        return fib                                
  1. イテレータをゼロから作るには、fibを関数ではなくクラスにする必要がある。
  2. Fib(max)を「呼び出す」と、実際にはこのクラスのインスタンスが生成され、maxを引数として__init__()メソッドが呼び出される。__init__()メソッドは、最大値maxをインスタンス変数として保存することによって、後で他のメソッドがそれを参照できるようにする。
  3. この__iter__()メソッドは、誰かがiter(fib)を呼び出した時に呼び出される(もうすぐ分かるように、forループが自動でこれを呼び出すし、自分の手で呼び出すこともできる)。イテレーションを始めるための初期化(この例では、2つのカウンタself.aself.bをリセットをする)を終えたら、__iter__()メソッドは__next__()メソッドを実装した何らかのオブジェクトを返せばよい。この例では(そして多くの場合では)、クラス自身が__next__()メソッドを実装しているので、__iter__()はただ単にselfを返している。
  4. 誰かがイテレータたるクラスのインスタンスについてnext()メソッドを呼び出すと、この__next__()メソッドが呼び出される。この意味はこの後すぐに分かる。
  5. __next__()メソッドがStopIteration例外を送出すると、この例外は呼び出し側にイテレーションの終了を告げることになる。ほとんどの例外とは違って、この例外はエラーではない。この例外は正常な状況で送出されるもので、単に「イテレータは生成すべき値をこれ以上持っていない」ということを意味するにすぎないのだ。仮に呼び出し側がforループだとすると、forループはStopIteration例外に気づいて、何事もなかったかのようにループから脱出する(言い換えれば、例外は飲み込まれるというわけだ)。この小さな魔法こそが、forループでイテレータを使う鍵だ。
  6. イテレータの__next__()メソッドは値を単純にreturnすることで、新しい値を吐き出す。ここでyieldを使ってはならない。yieldはちょっとした糖衣構文で、ジェネレータにしか使うことができないものだからだ。ここではイテレータをゼロから作っているので、代わりにreturnを使う。

まだ完全に混乱してはいないよね? 素晴らしい。このイテレータを呼び出す方法を見ていこう:

>>> from fibonacci2 import Fib
>>> for n in Fib(1000):
...     print(n, end=' ')
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987

ほら、完全に同じだ! フィボナッチジェネレータを呼び出したときと一字一句等しい(1文字だけ大文字になっていることを除けば)。しかしなぜだろう?

forループの中にちょっとした魔法が仕込まれている。ここでは次のようなことが起きている:

名詞を複数形にする規則のイテレータ

いよいよ大詰めだ。名詞を複数形にする規則のジェネレータをイテレータとして書き換えよう。

[plural6.pyをダウンロードする]

class LazyRules:
    rules_filename = 'plural6-rules.txt'

    def __init__(self):
        self.pattern_file = open(self.rules_filename, encoding='utf-8')
        self.cache = []

    def __iter__(self):
        self.cache_index = 0
        return self

    def __next__(self):
        self.cache_index += 1
        if len(self.cache) >= self.cache_index:
            return self.cache[self.cache_index - 1]

        if self.pattern_file.closed:
            raise StopIteration

        line = self.pattern_file.readline()
        if not line:
            self.pattern_file.close()
            raise StopIteration

        pattern, search, replace = line.split(None, 3)
        funcs = build_match_and_apply_functions(
            pattern, search, replace)
        self.cache.append(funcs)
        return funcs

rules = LazyRules()

さて、このクラスは__iter__()__next__()を実装しているので、イテレータとして使うことができる。このクラスはインスタンス化されてrulesに代入される。この処理はインポートするときに一度だけ行われる。

このクラスを少しずつ見ていこう。

class LazyRules:
    rules_filename = 'plural6-rules.txt'

    def __init__(self):
        self.pattern_file = open(self.rules_filename, encoding='utf-8')  
        self.cache = []                                                  
  1. LazyRulesクラスをインスタンス化する際に、パターンファイルが開かれるが、その中身は一切読み込まない(読み込みは後で行うのだ)。
  2. パターンファイルを開いたら、キャッシュの初期化を行う。このキャッシュは、あとで(__next__()メソッドで)パターンファイルの各行を読み込むときに使う。

先へ進む前に、rules_filenameをよく見てみよう。これは__iter__()メソッドの内部では定義されていない。それどころか、これはどのメソッドの内部でも定義されていない。これはクラスレベルで定義されているのだ。これはクラス変数というもので、これにはインスタンス変数とまったく同じやり方(self.rules_filename)でアクセスできるのだが、この変数はLazyRulesクラスのすべてのインスタンスによって共有されている。

>>> import plural6
>>> r1 = plural6.LazyRules()
>>> r2 = plural6.LazyRules()
>>> r1.rules_filename                               
'plural6-rules.txt'
>>> r2.rules_filename
'plural6-rules.txt'
>>> r2.rules_filename = 'r2-override.txt'           
>>> r2.rules_filename
'r2-override.txt'
>>> r1.rules_filename
'plural6-rules.txt'
>>> r2.__class__.rules_filename                     
'plural6-rules.txt'
>>> r2.__class__.rules_filename = 'papayawhip.txt'  
>>> r1.rules_filename
'papayawhip.txt'
>>> r2.rules_filename                               
'r2-overridetxt'
  1. このクラスの各々のインスタンスは、クラスで定義された値を持つ属性rules_filenameを受け継いでいる。
  2. 1つのインスタンスの属性値を変更しても、他のインスタンスの属性値には影響を与えず……
  3. ……クラス属性も変更しない。クラス自体にアクセスするための特殊属性__class__を使うことで、(個々のインスタンスの属性ではなく)クラス属性を参照できる。
  4. クラス属性を変更すると、まだ値を受け継いでいるインスタンス(ここではr1)はその影響を受ける。
  5. その属性を上書きしているインスタンス(ここではr2)は影響を受けない。

では本題に戻ろう。

    def __iter__(self):       
        self.cache_index = 0
        return self           
  1. __iter__()メソッドは、誰かが(例えばforループが)iter(rules)を呼び出すたびに呼び出される。
  2. すべての__iter__()メソッドが絶対にやらなればならないのは、イテレータを返すことだけだ。この例の__iter__()メソッドはselfを返している。これは、イテレーション時に値を返す仕事をする__next__()メソッドをこのクラスが定義していることを示している。
    def __next__(self):                                 
        .
        .
        .
        pattern, search, replace = line.split(None, 3)
        funcs = build_match_and_apply_functions(        
            pattern, search, replace)
        self.cache.append(funcs)                        
        return funcs
  1. __next__() メソッドは、誰か(例えばforループ)がnext(rules)を呼び出すたびに呼び出される。このメソッドの意味は、後ろから読んでいかないと分からないので、そうやって見ていこう。
  2. この関数の最後の部分は、少なくとも見覚えがあるはずだ。build_match_and_apply_functions()関数は変更されていない。以前のものと同じだ。
  3. 唯一の違いは、マッチと処理を行う関数 (タプル funcs に格納されている) を返す前に、それをself.cacheに保存していることだけだ。

前の方に戻ろう……

    def __next__(self):
        .
        .
        .
        line = self.pattern_file.readline()  
        if not line:                         
            self.pattern_file.close()
            raise StopIteration              
        .
        .
        .
  1. ファイルの少し高度なトリックを使っている。readline()メソッド(注: 単数形のreadline()で、複数形のreadlines()ではない)は、開かれたファイルからちょうど1行だけを読み込む。具体的には次の1行を読みこむのだ(ファイルオブジェクトもイテレータとして振る舞う! 端から端までイテレータだ……)。
  2. readline()が読み込む行がまだある場合には、lineは空の文字列にはならない。たとえファイルが空行を含んでいたとしても、line は1文字の'\n'(改行文字)という文字列になるだろう。もしlineが本当に空の文字列だったとしたら、それはファイルにもう読み込める行が無いということだ。
  3. ファイルの末尾に到達したら、そのファイルを閉じて魔法のStopIteration例外を発生させなければならない。この行までたどり着いた理由は、「新しい規則に基づいてマッチと処理を行う関数」を必要としていたからだということを思いだそう。新しい規則はファイルの次の行から得られるわけだが、その次の行とやらは存在しない! つまり、返す値がもう無いので、イテレーションは終わりなのだ。

__next__()メソッドの一番上まで戻ろう……

    def __next__(self):
        self.cache_index += 1
        if len(self.cache) >= self.cache_index:
            return self.cache[self.cache_index - 1]     

        if self.pattern_file.closed:
            raise StopIteration                         
        .
        .
        .
  1. self.cacheは、それぞれの規則に基づいてマッチと処理を行うために必要な関数のリストになるものだ(少なくともこれには聞き覚えがあるはずだよ!)。self.cache_indexは、キャッシュされた要素のうち、次にどれを返さなければならないかを記録している。キャッシュがまだ残っている場合には(つまりself.cacheの長さがself.cache_indexよりも大きい場合は)キャッシュがヒットする! やった! マッチと処理を行う関数を初めから作るかわりに、値をキャッシュから取り出して返すことができるのだ。
  2. その一方で、キャッシュにヒットせず、なおかつファイルオブジェクトが既に閉じられている場合は(これが起こる場合については、先ほどこのメソッドの下の方のコードを説明した時に見た)、もうこれ以上やるべきことはない。ファイルが閉じてしまっているということは、このファイルを全部使い切ってしまったということ — つまり、既にパターンファイル全体を読み込んでいて、すべてのパターンに基づくマッチと処理の関数を作成し終わり、そのキャッシュ化も済んでいるということだ。ファイルは読み尽くされ、キャッシュも使い果たされ、私は疲れてしまった。待って、何だって? もう少し頑張ろう、もうゴールは目前だ。

すべてをまとめると、次のようなことが起きている:

名詞を複数形にするパラダイスに到達した。

  1. 最小の起動コスト。import時に行われることは、1つのクラスをインスタンス化するのと、ファイルを開く(しかし内容は読み込まない)ことだけだ。
  2. 最大限のパフォーマンス。 前のバージョンでは単語を複数形にするたびに、ファイルを読み込んで関数を動的に作成していた。このバージョンでは、関数は作成されるとすぐにキャッシュされる。だから、いくつの単語を複数形にするのであれ、最悪の場合でもパターンファイルは1度だけしか読み込まれない。
  3. コードとデータの分離。 すべてのパターンは別のファイルに格納されている。コードはコード、データはデータ、二つは決して交わらない。

これは本当にパラダイスなのだろうか? イエスでもありノーでもある。LazyRulesのコードについて考慮に入れるべきこととしては、パターンファイルが(__init__()において)開かれたきり、最後の規則にたどり着くまで開かれっぱなしだということがある。最終的には、Pythonが終了する時か、最後のLazyRulesクラスのインスタンスが破棄される時にファイルは閉じられるのだが、そうだとしても、その時までには長い時間がかかりうる。もしこのクラスが長時間実行されるPythonプロセスの一部である場合、Pythonインタプリタは決して終了しないかもしれないし、LazyRulesオブジェクトも破棄されることはないかもしれない。

これに対処する方法はいくつかある。__init__()でファイルを開き、1行ずつ読み込んでいくあいだファイルを開きっぱなしにするのではなく、ファイルを開いて、すべてのルールを読み込み、即座にファイルを閉じることができる。もしくは、ファイルを開いて、ルールを1つ読み込み、ファイルの位置をtell()メソッドを使って保持し、ファイルを閉じ、あとでファイルを再び開いてseek()メソッドで、先ほど読み込みを停止した位置から再開することもできる。もしくは、このことを気にせずに、この例がやっているように、ファイルを開いたままにしておくこともできる。プログラミングとはデザインであり、デザインはトレードオフと制約がすべてだ。ファイルを長時間開いたままにしておくことは問題になるかもしれないし、コードを複雑にするのも問題になるかもしれない。どちらがより大きい問題であるかは、その開発チームやアプリケーション、あるいは実行環境によって異なる。

もっと知りたい人のために

© 2001– Mark Pilgrim
© Fukada, Fujimoto(日本語版)