7章 クラスとイテレータ

飛び込む

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

クラスを定義する

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

Pythonではクラスを簡単に定義できる。 関数でもそうだったように、クラスを定義する場所が他と区別されているなどということはない。

Pythonのクラス定義は、class という予約語から始まり、 その後ろにクラス名を書く。 形の上で言えば、クラスは別に他のクラスを継承しなくてもよいので、定義するのに最低限必要なものはこれだけ。

このクラスの名前は「PapayaWhip」であり、 このクラスは他のクラスを継承していない。 クラス名は、EachWordLikeThisのように「各単語の先頭を大文字にするのが普通」。

In [1]:
class PapayaWhip:
    pass  

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

名前を除けば、Pythonのクラスが絶対に持っていなければならないものなど無い。

必須ではないが、Pythonのクラスはコンストラクタに似た__init__()メソッドを持つことができる。

init()メソッド

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

モジュールや関数と同様に、クラスもdocstringを持つことができる.

__init__()メソッドは、 クラスのインスタンスが生成されるとすぐに呼び出される。

このメソッドのことをクラスの「コンストラクタ」と呼びたくなるかもしれないが、それは厳密には誤り。 確かに、これはC++のコンストラクタと似たものに見えるし(慣例上、__init__()メソッドはクラスの一番初めに定義される)、 同じような処理をするし(新しく作成されたインスタンスのなかで一番最初に実行される)、名前自体もそれっぽい。 しかし、そう呼ぶのは正しくない。

__init__()メソッドが呼び出されたときにはオブジェクトはすでに作成されている。

In [2]:
class Fib:
    '''iterator that yields numbers in the Fibonacci sequence'''
    
    def __init__(self, max):
        pass
    

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

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

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

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

In [3]:
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
    

Fibクラスの新しいインスタンスを作り、それを変数fibに代入している。 100という引数を1つ渡しているが、これは結局Fibクラスの __init__()メソッドでmax引数として扱われる。

In [4]:
fib = Fib(100)

fibはFibクラスのインスタンスになった。

In [5]:
fib
Out[5]:
<__main__.Fib at 0x213d04b96a0>

どんなクラスのインスタンスも組み込み属性の__class__ を持っているが、 この属性はそのオブジェクトのクラスを指している。

Javaのプログラマなら、getName()やgetSuperclass()といったオブジェクトのメタデータ情報を得るためのメソッドが入ったClassクラスに馴染みがあるかもしれない。 Pythonでは属性を通じてこの種のメタデータに参照することになるのだが、その考え方自体は同じ。

In [6]:
fib.__class__
Out[6]:
__main__.Fib

インスタンスのdocstring には、関数やモジュールの場合と同じやり方でアクセスできる。ある1つのクラスのインスタンスはすべて同じdocstring を持つ。

In [7]:
fib.__doc__
Out[7]:
'iterator that yields numbers in the Fibonacci sequence'

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

インスタンス変数

self.max というのは、インスタンス変数。 __init__()メソッドに引数として渡されたmax とは違う。 self.max は、このインスタンスにおいて「グローバル」だ。要するに、他のメソッドからこの変数にアクセスできる。

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

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

In [8]:
fib1 = Fib(100)
fib2 = Fib(200)
fib1.max
Out[8]:
100
In [9]:
fib2.max
Out[9]:
200

フィボナッチイテレータ

イテレータとは __iter__()メソッドを実装した単なるクラスだ。

__init__, __iter__, __next__ の3つのメソッドのどれにも、先頭と末尾に2つのアンダースコア(_)が付いている。 通常はこれらが特殊メソッドであることを示している。 特殊メソッドはクラスやそのインスタンスについて何か他の構文を使ったときに、Pythonがこれらを呼び出す。

特殊メソッドの詳細についてはこちら。 http://diveintopython3-ja.rdy.jp/special-method-names.html

イテレータをゼロから作るには、fibを関数ではなくクラスにする必要がある。

__iter__()メソッドは、 誰かがiter(fib)を呼び出した時に呼び出される (もうすぐ分かるように、forループが自動でこれを呼び出すし、自分の手で呼び出すこともできる)。

イテレーションを始めるための初期化 (この例では、2つのカウンタself.aself.b をリセットをする)を終えたら、__iter__()メソッドは__next__()メソッドを実装した何らかのオブジェクトを返せばよい。 この例では(そして多くの場合では)、 クラス自身が__next__()メソッドを実装しているので、 __iter__()はただ単にself を返している。

__next__()メソッドがStopIteration例外を送出すると、 この例外は呼び出し側にイテレーションの終了を告げることになる。 ほとんどの例外とは違って、この例外はエラーではない。

この例外は正常な状況で送出されるもので、 単に「イテレータは生成すべき値をこれ以上持っていない」ということを意味するにすぎない。 仮に呼び出し側がforループだとすると、forループはStopIteration例外に気づいて、何事もなかったかのようにループから脱出する。

イテレータの __next__()メソッドは値を単純にreturnすることで、新しい値を吐き出す。

ここでyieldを使ってはならない。ジェネレータにしか使うことができないものだから。 ここではイテレータをゼロから作っているので、代わりにreturnを使う。

In [10]:
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
    
In [11]:
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 

forループの中では、次のようなことが起きている:

forループは Fib(1000) を呼び出している。 この呼び出しによってFibクラスのインスタンスが返される。

forループは iter を水面下で呼び出してくれる。 この呼び出しでイテレータオブジェクトが返される。 この例では、__iter__()メソッドは self を返す.

イテレータの「全体をループする」ために、forループは next(fib_iter) を呼び出す。 これによってfib_iterオブジェクトの __next__() メソッドが呼び出され、このメソッドは次のフィボナッチ数を計算してその値を返す。 forループはこの値を受け取ってnへ代入し、このnの値についてforループの本文を実行する。

forループは停止すべき時は、 nextStopIteration例外を送出すると、forループはその例外を飲み込んで、何事もなかったかのようにループを抜ける(その他の例外は飲み込まれること無く、通常通り送出される)。

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

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

LazyRulesクラスをインスタンス化する際に、 パターンファイルが開かれるが、その中身は一切読み込まない(読み込みは後で行う)。

パターンファイルを開いたら、キャッシュの初期化を行う。このキャッシュは、あとで(__next__()メソッドで)パターンファイルの各行を読み込むときに使う。

In [12]:
class LazyRules:
    rules_filename = 'plural6-rules.txt'

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

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

In [13]:
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
    
In [14]:
r1 = LazyRules()
r2 = LazyRules()

このクラスの各々のインスタンスは、 クラスで定義された値を持つ属性 rules_filename を受け継いでいる。

In [15]:
r1.rules_filename
Out[15]:
'plural6-rules.txt'
In [16]:
r2.rules_filename
Out[16]:
'plural6-rules.txt'

1つのインスタンスの属性値を変更しても、他のインスタンスの属性値には影響を与えない。

In [17]:
r2.rules_filename = 'r2-override.txt'
r2.rules_filename
Out[17]:
'r2-override.txt'
In [18]:
r1.rules_filename
Out[18]:
'plural6-rules.txt'

クラス属性も変更しない。 クラス自体にアクセスするための特殊属性__class__を使うことで、 (個々のインスタンスの属性ではなく)クラス属性を参照できる。

In [19]:
r2.__class__.rules_filename
Out[19]:
'plural6-rules.txt'
In [20]:
r1.__class__.rules_filename
Out[20]:
'plural6-rules.txt'

クラス属性を変更すると、 まだ値を受け継いでいるインスタンス(ここではr1)はその影響を受ける。

In [21]:
r2.__class__.rules_filename = 'papayawhip.txt' 
In [22]:
r2.__class__.rules_filename
Out[22]:
'papayawhip.txt'
In [23]:
r1.__class__.rules_filename
Out[23]:
'papayawhip.txt'
In [24]:
r1.rules_filename
Out[24]:
'papayawhip.txt'

その属性を上書きしているインスタンス(ここではr2)は影響を受けない。

In [25]:
r2.rules_filename
Out[25]:
'r2-override.txt'
def __iter__(self):       ①
        self.cache_index = 0
        return self           ②
  • __iter__() メソッドは、 誰かが(例えばforループが)iter(rules)を呼び出すたびに呼び出される。
  • すべての__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例外を発生させなければならない。 この行までたどり着いた理由は、「新しい規則に基づいてマッチと処理を行う関数」を必要としていたからだということを思いだそう。 新しい規則はファイルの次の行から得られるわけだが、その次の行とやらは存在しない. つまり、返す値がもう無いので、イテレーションは終わり。
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. モジュールがインポートされると、 LazyRules クラスのインスタンスが1つだけ生成され、rulesという名前が付けられる。 これはパターンファイルを開くが、その内容は読み込まない。

  2. 最初のマッチと処理の関数が要求されると、まずキャッシュが確認されるが、キャッシュが空であることが分かる。 すると、パターンファイルから1つの行が読み込まれ、そのパターンを元にマッチと処理の関数が作成され、キャッシュされる。

  3. 仮に、一番最初の規則がマッチしたとしよう。その場合は、マッチと処理の関数はそれ以上作成されず、パターンファイルからそれ以上は読み込まれない。

  4. さらに、今度は別の単語を複数形にするために、呼び出し元が再びplural()関数を呼び出したと仮定しよう。plural()関数のforループはiter(rules)を呼び出し、ここでキャッシュのインデックスは初期化されるが、ファイルオブジェクトはリセットされない。

  5. 最初のループで、forループは値をrulesから取得しようとする。 その結果、__next__()メソッドが呼び出されることになるが、 今回はパターンファイルの1行目に対応するマッチと処理の関数のペアがキャッシュに1つ準備されている。この2つの関数は前の単語を複数形にする時に作成・キャッシュされたものなので、ここでキャッシュから取り出される。そして、キャッシュのインデックスはインクリメントされ、開いたファイルについては触れることもない。

  6. 仮に、今回は最初の規則にマッチしなかったとする。 すると、for文は再びループして、rulesから別の値が取り出そうとする。 ここで__next__()メソッドが2度目の呼び出しを受けることになるが、この時点でキャッシュは使い切られている — 2つ目のルールが欲しいのに、キャッシュには要素が1つしかない — ので__next__()メソッドは処理を続け、開いたファイルから別の行を読み込み、パターンを元にマッチと処理の関数を構築し、この2つの関数をキャッシュする。

  7. この読み込み-構築-キャッシュのプロセスは、複数形にしたい単語にパターンファイルから読み込まれた規則がマッチするまで続く。 もし、ファイルの末尾に達する前にマッチする規則を見つけた場合は、その規則を使って変換を施し処理を停止する。 この時、ファイルは開かれたままなので、ファイルポインタは読み込みをやめたところに留まり、次のreadline()の呼び出しを待つ。 他方で、この処理の間にキャッシュには要素が溜まっていく。次に新しい単語を複数形にしようとした時には、パターンファイルの次の行を読み込む前にキャッシュの中にあるものが試されるのだ。

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

  • 最小の起動コスト。import時に行われることは、1つのクラスをインスタンス化するのと、ファイルを開く(しかし内容は読み込まない)ことだけだ。

  • 最大限のパフォーマンス。 前のバージョンでは単語を複数形にするたびに、ファイルを読み込んで関数を動的に作成していた。このバージョンでは、関数は作成されるとすぐにキャッシュされる。だから、いくつの単語を複数形にするのであれ、最悪の場合でもパターンファイルは1度だけしか読み込まれない。

  • コードとデータの分離。 すべてのパターンは別のファイルに格納されている。コードはコード、データはデータ、二つは決して交わらない。

In [26]:
import re

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 = self.__build_match_and_apply_functions(pattern, search, replace)
        self.cache.append(funcs)
        return funcs
    
    def __build_match_and_apply_functions(self, pattern, search, replace):
        def matches_rule(word):
            return re.search(pattern, word)
        def apply_rule(word):
            return re.sub(search, replace, word)
        
        return (matches_rule, apply_rule)
    


rules = LazyRules()

def plural(noun):
    for matches_rule, apply_rule in rules:
        if matches_rule(noun):
            return apply_rule(noun)
    raise ValueError('no matching rule for {0}'.format(noun))
In [27]:
rules.cache
Out[27]:
[]
In [28]:
plural('box')
Out[28]:
'boxes'
In [29]:
rules.cache
Out[29]:
[(<function __main__.LazyRules.__build_match_and_apply_functions.<locals>.matches_rule>,
  <function __main__.LazyRules.__build_match_and_apply_functions.<locals>.apply_rule>)]
In [30]:
plural('vancy')
Out[30]:
'vancies'
In [31]:
rules.cache
Out[31]:
[(<function __main__.LazyRules.__build_match_and_apply_functions.<locals>.matches_rule>,
  <function __main__.LazyRules.__build_match_and_apply_functions.<locals>.apply_rule>),
 (<function __main__.LazyRules.__build_match_and_apply_functions.<locals>.matches_rule>,
  <function __main__.LazyRules.__build_match_and_apply_functions.<locals>.apply_rule>),
 (<function __main__.LazyRules.__build_match_and_apply_functions.<locals>.matches_rule>,
  <function __main__.LazyRules.__build_match_and_apply_functions.<locals>.apply_rule>)]

インスタンス変数のスコープについて

In [32]:
class Cl:
    def __init__(self, v):
        # アンダーバー2つ__ でprivate変数になる。 
        self.__v = v
    
    def get(self):
        return self.__v


c = Cl(10)
c
Out[32]:
<__main__.Cl at 0x213d04d5588>
In [33]:
c.__v
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-33-6d638b89ef51> in <module>()
----> 1 c.__v

AttributeError: 'Cl' object has no attribute '__v'
In [34]:
c.get()
Out[34]:
10
In [35]:
c._Cl__v
Out[35]:
10
In [36]:
c._Cl__v = 100
c._Cl__v
Out[36]:
100
In [37]:
c.get()
Out[37]:
100

参考リンク

In [ ]: