イテレーターはどんなところにもいて、あらゆるものの礎をなしているが、その姿はいつも目に見えない。
内包表記はイテレータの単純形に過ぎないし、
ジェネレータもイテレータの単純形に過ぎない。
値をyield
する関数というのは、イテレータを組み立てることなしにイテレータを作るコンパクトで上手い方法。
Pythonは完全にオブジェクト指向の言語なので、 独自のクラスを定義したり、自作のクラスや組み込みのクラスを継承したり、自分で定義したクラスをインスタンス化したりできる。
Pythonではクラスを簡単に定義できる。 関数でもそうだったように、クラスを定義する場所が他と区別されているなどということはない。
Pythonのクラス定義は、class
という予約語から始まり、
その後ろにクラス名を書く。
形の上で言えば、クラスは別に他のクラスを継承しなくてもよいので、定義するのに最低限必要なものはこれだけ。
このクラスの名前は「PapayaWhip」であり、 このクラスは他のクラスを継承していない。 クラス名は、EachWordLikeThisのように「各単語の先頭を大文字にするのが普通」。
class PapayaWhip:
pass
pass文は、JavaやC言語における空っぽの波括弧({})のようなもの。
名前を除けば、Pythonのクラスが絶対に持っていなければならないものなど無い。
必須ではないが、Pythonのクラスはコンストラクタに似た__init__()
メソッドを持つことができる。
次のコードでは __init__
メソッドを使ってFibクラスを初期化している。
モジュールや関数と同様に、クラスもdocstringを持つことができる.
__init__()
メソッドは、
クラスのインスタンスが生成されるとすぐに呼び出される。
このメソッドのことをクラスの「コンストラクタ」と呼びたくなるかもしれないが、それは厳密には誤り。
確かに、これはC++のコンストラクタと似たものに見えるし(慣例上、__init__()
メソッドはクラスの一番初めに定義される)、
同じような処理をするし(新しく作成されたインスタンスのなかで一番最初に実行される)、名前自体もそれっぽい。
しかし、そう呼ぶのは正しくない。
__init__()
メソッドが呼び出されたときにはオブジェクトはすでに作成されている。
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__()
が要求する引数を渡して、
クラスを関数のように呼び出すだけで良い。
すると、戻り値として新しく生成されたオブジェクトが返される。
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
引数として扱われる。
fib = Fib(100)
fibはFibクラスのインスタンスになった。
fib
<__main__.Fib at 0x213d04b96a0>
どんなクラスのインスタンスも組み込み属性の__class__
を持っているが、
この属性はそのオブジェクトのクラスを指している。
Javaのプログラマなら、getName()やgetSuperclass()といったオブジェクトのメタデータ情報を得るためのメソッドが入ったClassクラスに馴染みがあるかもしれない。 Pythonでは属性を通じてこの種のメタデータに参照することになるのだが、その考え方自体は同じ。
fib.__class__
__main__.Fib
インスタンスのdocstring には、関数やモジュールの場合と同じやり方でアクセスできる。ある1つのクラスのインスタンスはすべて同じdocstring を持つ。
fib.__doc__
'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つはそれぞれの値を保持することになる。
fib1 = Fib(100)
fib2 = Fib(200)
fib1.max
100
fib2.max
200
イテレータとは __iter__()
メソッドを実装した単なるクラスだ。
__init__, __iter__, __next__
の3つのメソッドのどれにも、先頭と末尾に2つのアンダースコア(_
)が付いている。
通常はこれらが特殊メソッドであることを示している。
特殊メソッドはクラスやそのインスタンスについて何か他の構文を使ったときに、Pythonがこれらを呼び出す。
特殊メソッドの詳細についてはこちら。 http://diveintopython3-ja.rdy.jp/special-method-names.html
イテレータをゼロから作るには、fibを関数ではなくクラスにする必要がある。
__iter__()
メソッドは、
誰かがiter(fib)を呼び出した時に呼び出される
(もうすぐ分かるように、forループが自動でこれを呼び出すし、自分の手で呼び出すこともできる)。
イテレーションを始めるための初期化
(この例では、2つのカウンタself.a
とself.b
をリセットをする)を終えたら、__iter__()
メソッドは__next__()
メソッドを実装した何らかのオブジェクトを返せばよい。
この例では(そして多くの場合では)、
クラス自身が__next__()
メソッドを実装しているので、
__iter__()
はただ単にself
を返している。
__next__()
メソッドがStopIteration
例外を送出すると、
この例外は呼び出し側にイテレーションの終了を告げることになる。
ほとんどの例外とは違って、この例外はエラーではない。
この例外は正常な状況で送出されるもので、 単に「イテレータは生成すべき値をこれ以上持っていない」ということを意味するにすぎない。 仮に呼び出し側がforループだとすると、forループはStopIteration例外に気づいて、何事もなかったかのようにループから脱出する。
イテレータの __next__()
メソッドは値を単純にreturnすることで、新しい値を吐き出す。
ここでyieldを使ってはならない。ジェネレータにしか使うことができないものだから。 ここではイテレータをゼロから作っているので、代わりにreturnを使う。
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
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ループは停止すべき時は、 next
が StopIteration
例外を送出すると、forループはその例外を飲み込んで、何事もなかったかのようにループを抜ける(その他の例外は飲み込まれること無く、通常通り送出される)。
名詞を複数形にする規則のジェネレータをイテレータとして書き換える。
LazyRules
クラスをインスタンス化する際に、
パターンファイルが開かれるが、その中身は一切読み込まない(読み込みは後で行う)。
パターンファイルを開いたら、キャッシュの初期化を行う。このキャッシュは、あとで(__next__()
メソッドで)パターンファイルの各行を読み込むときに使う。
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
クラスのすべてのインスタンスによって共有されている。
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
r1 = LazyRules()
r2 = LazyRules()
このクラスの各々のインスタンスは、
クラスで定義された値を持つ属性 rules_filename
を受け継いでいる。
r1.rules_filename
'plural6-rules.txt'
r2.rules_filename
'plural6-rules.txt'
1つのインスタンスの属性値を変更しても、他のインスタンスの属性値には影響を与えない。
r2.rules_filename = 'r2-override.txt'
r2.rules_filename
'r2-override.txt'
r1.rules_filename
'plural6-rules.txt'
クラス属性も変更しない。
クラス自体にアクセスするための特殊属性__class__
を使うことで、
(個々のインスタンスの属性ではなく)クラス属性を参照できる。
r2.__class__.rules_filename
'plural6-rules.txt'
r1.__class__.rules_filename
'plural6-rules.txt'
クラス属性を変更すると、 まだ値を受け継いでいるインスタンス(ここではr1)はその影響を受ける。
r2.__class__.rules_filename = 'papayawhip.txt'
r2.__class__.rules_filename
'papayawhip.txt'
r1.__class__.rules_filename
'papayawhip.txt'
r1.rules_filename
'papayawhip.txt'
その属性を上書きしているインスタンス(ここではr2)は影響を受けない。
r2.rules_filename
'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
__next__()
メソッドは、誰か(例えばforループ)がnext(rules)を呼び出すたびに呼び出される。build_match_and_apply_functions()
関数は変更されていない。def __next__(self):
.
.
.
line = self.pattern_file.readline() ①
if not line: ②
self.pattern_file.close()
raise StopIteration ③
readline()
メソッド(注: 単数形のreadline()で、複数形のreadlines()ではない)は、開かれたファイルからちょうど1行だけを読み込む。具体的には次の1行を読みこむ。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 ②
self.cache
は、それぞれの規則に基づいてマッチと処理を行うために必要な関数のリストになるもの。self.cache_index
は、キャッシュされた要素のうち、次にどれを返さなければならないかを記録している。キャッシュがまだ残っている場合には
(つまりself.cacheの長さがself.cache_index
よりも大きい場合は)キャッシュが使える。
マッチと処理を行う関数を初めから作るかわりに、値をキャッシュから取り出して返すことができる。
— つまり、既にパターンファイル全体を読み込んでいて、すべてのパターンに基づくマッチと処理の関数を作成し終わり、そのキャッシュ化も済んでいる.
まとめ:
LazyRules
クラスのインスタンスが1つだけ生成され、rulesという名前が付けられる。
これはパターンファイルを開くが、その内容は読み込まない。
すると、パターンファイルから1つの行が読み込まれ、そのパターンを元にマッチと処理の関数が作成され、キャッシュされる。
仮に、一番最初の規則がマッチしたとしよう。その場合は、マッチと処理の関数はそれ以上作成されず、パターンファイルからそれ以上は読み込まれない。
さらに、今度は別の単語を複数形にするために、呼び出し元が再びplural()関数を呼び出したと仮定しよう。plural()関数のforループはiter(rules)を呼び出し、ここでキャッシュのインデックスは初期化されるが、ファイルオブジェクトはリセットされない。
最初のループで、forループは値をrulesから取得しようとする。
その結果、__next__()
メソッドが呼び出されることになるが、
今回はパターンファイルの1行目に対応するマッチと処理の関数のペアがキャッシュに1つ準備されている。この2つの関数は前の単語を複数形にする時に作成・キャッシュされたものなので、ここでキャッシュから取り出される。そして、キャッシュのインデックスはインクリメントされ、開いたファイルについては触れることもない。
すると、for文は再びループして、rulesから別の値が取り出そうとする。
ここで__next__()
メソッドが2度目の呼び出しを受けることになるが、この時点でキャッシュは使い切られている
— 2つ目のルールが欲しいのに、キャッシュには要素が1つしかない — ので__next__()
メソッドは処理を続け、開いたファイルから別の行を読み込み、パターンを元にマッチと処理の関数を構築し、この2つの関数をキャッシュする。
もし、ファイルの末尾に達する前にマッチする規則を見つけた場合は、その規則を使って変換を施し処理を停止する。 この時、ファイルは開かれたままなので、ファイルポインタは読み込みをやめたところに留まり、次のreadline()の呼び出しを待つ。 他方で、この処理の間にキャッシュには要素が溜まっていく。次に新しい単語を複数形にしようとした時には、パターンファイルの次の行を読み込む前にキャッシュの中にあるものが試されるのだ。
名詞を複数形にするパラダイスに到達した。
最小の起動コスト。import時に行われることは、1つのクラスをインスタンス化するのと、ファイルを開く(しかし内容は読み込まない)ことだけだ。
最大限のパフォーマンス。 前のバージョンでは単語を複数形にするたびに、ファイルを読み込んで関数を動的に作成していた。このバージョンでは、関数は作成されるとすぐにキャッシュされる。だから、いくつの単語を複数形にするのであれ、最悪の場合でもパターンファイルは1度だけしか読み込まれない。
コードとデータの分離。 すべてのパターンは別のファイルに格納されている。コードはコード、データはデータ、二つは決して交わらない。
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))
rules.cache
[]
plural('box')
'boxes'
rules.cache
[(<function __main__.LazyRules.__build_match_and_apply_functions.<locals>.matches_rule>, <function __main__.LazyRules.__build_match_and_apply_functions.<locals>.apply_rule>)]
plural('vancy')
'vancies'
rules.cache
[(<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>)]
class Cl:
def __init__(self, v):
# アンダーバー2つ__ でprivate変数になる。
self.__v = v
def get(self):
return self.__v
c = Cl(10)
c
<__main__.Cl at 0x213d04d5588>
c.__v
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) <ipython-input-33-6d638b89ef51> in <module>() ----> 1 c.__v AttributeError: 'Cl' object has no attribute '__v'
c.get()
10
c._Cl__v
10
c._Cl__v = 100
c._Cl__v
100
c.get()
100