現在地: ホーム Dive Into Python 3

難易度: ♦♦♦♢♢

クロージャとジェネレータ

ぼくのつづり字はふらふらするんだ。つづりは正しいのだけど、ふらつくから、文字が間違ったところに行っちゃうんだ。
— クマのプーさん

 

飛び込む

司書と英文学専攻者の息子として育ったせいか、私はいつも言語というものに魅きつけられてきた。ここで言っているのは自然言語のことだ。プログラミング言語は特にそうではない……ええとまあ、プログラミング言語もだ。英語を取り上げてみよう。英語は統合失調症気味の言語で、(少し例を挙げれば)ドイツ語、フランス語、スペイン語、ラテン語から言葉を借りてきている。いや、「借りる」という言葉は適切じゃないな。「ぶんどってきた」と言った方がしっくりくる。あるいは、「同化する」という単語を使った方が良いかも—ボーグみたいに。うん、それがいい。

我々はボーグだ。お前たちの言語的・語源的特性は我々に取り込まれる。抵抗は無意味だ。

この章では名詞の複数形について学ぶ。それ以外にも、別の関数を返す関数、高度な正規表現、ジェネレータといったものについても学ぶのだが、しかし、まずは「名詞の複数形はどのように作るのか」ということから話を始めよう。(もし、正規表現の章をまだ読んでいないのなら、良い機会なので読んでおいてほしい。この章は正規表現の基礎を知っていることを前提としていて、すぐにより高度な使い方の話に進んでいってしまうからだ。)

もし、あなたが英語圏の国で育ったか、正規の学校教育で英語を習ったことがあるなら、次のような基本的な規則には馴染みがあるだろう:

(もちろん例外はたくさんある。manmenになるし、womanwomenになるが、humanhumansになる。また、mousemiceに、louseliceになるのに、househousesになる。さらに、Knifeknivesに、wifewivesになるのだが、lowlifelowlifesになる。ああ、それと独自の複数形を持つsheepとかdeerとかhaikuとかの単語があることも知っているから、もうそこらへんで止めておいてくれ)

当然のことだが、他の言語では事情は全く異なる。

では、英語の名詞を自動で複数形にしてくれるPythonのライブラリを設計してみよう。ここでは、上に挙げた4つの規則だけで始めることにするが、この規則だけでは不十分だということは頭にとめておいてほしい。

そうか、正規表現を使うんだ!

さて、あなたは単語を調べていくことになるのだが、これは少なくとも英語では文字列を調べるということを意味している。そしてここにはいくつかの規則があり、それによれば異なる文字の組み合わせを見つけだした上で、それぞれに異なった処理を施す必要があるという。正規表現の出番みたいじゃないか!

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

import re

def plural(noun):
    if re.search('[sxz]$', noun):             
        return re.sub('$', 'es', noun)        
    elif re.search('[^aeioudgkprt]h$', noun):
        return re.sub('$', 'es', noun)
    elif re.search('[^aeiou]y$', noun):
        return re.sub('y$', 'ies', noun)
    else:
        return noun + 's'
  1. これは正規表現だが、前章の正規表現では触れなかった構文が使われている。この角括弧は「どれか一文字にマッチする」という意味だ。だから、[sxz]は「sxまたはz」ということを表しているが、ただしこのどれか一つだけにマッチするものでなくてはならない。この$には馴染みがあるだろう。これは文字列の末尾にマッチするものだ。まとめると、この正規表現はnounsxzのいずれかで終わるかどうかを調べているのだ。
  2. このre.sub()は正規表現に基づいて単語を置換する関数だ。

正規表現を使った置換についてもっと詳しく見てみよう。

>>> import re
>>> re.search('[abc]', 'Mark')    
<_sre.SRE_Match object at 0x001C1FA8>
>>> re.sub('[abc]', 'o', 'Mark')  
'Mork'
>>> re.sub('[abc]', 'o', 'rock')  
'rook'
>>> re.sub('[abc]', 'o', 'caps')  
'oops'
  1. Markという文字列はabcのいずれかを含んでいるだろうか? 答えはイエスだ。この文字列にはaがある。
  2. OK、なら今度はabcを探し出してoに置換してみよう。すると、MarkMorkになる。
  3. 同じ関数をrockに使うとrookになる。
  4. この関数を使うとcapsoapsになると思ったかもしれないが、そうはならない。re.subは最初の一つだけではなく、マッチしたもの全てを置換する。だから、この正規表現はcapsoopsに変えるのだ。このcaの両方がoに置き換えられるからだ。

さて、plural()関数に話を戻そうか……

def plural(noun):
    if re.search('[sxz]$', noun):
        return re.sub('$', 'es', noun)         
    elif re.search('[^aeioudgkprt]h$', noun):  
        return re.sub('$', 'es', noun)
    elif re.search('[^aeiou]y$', noun):        
        return re.sub('y$', 'ies', noun)
    else:
        return noun + 's'
  1. ここでは、文字列の末尾を($でマッチさせて)esで置き換えている。言い換えると、esを文字列に付け加えているということだ。これと同じことは、文字列の連結を使ってもできるのだが(例えば、noun + 'es')、ここではそれぞれの規則について正規表現を使って実装することにする。その理由はこの章を読み進めば明らかになるはずだ。
  2. よく見てほしい。ここにあるのも正規表現の新しいバリエーションだ。 この角括弧の中の一文字目にある^は特別な意味を持っている。つまり否定だ。例えば、[^abc]は「abc以外の一文字」という意味になる。だから、[^aeioudgkprt]aeioudgkprt以外の任意の一文字を表すことになるのだ。さて、ここではさらにその文字にhが続き、そこで文字列が終わりになる必要があった。今探しているのは、有音のHで終わる単語だからだ。
  3. ここも同じパターンだ。これはYで終わる単語で、しかもその直前の文字がaeiouいずれでもないものにマッチする。ここでは、Yが末尾にあって、さらにそのYをIというように発音する単語を探しているのだ。

否定を使った正規表現についてもっと詳しく見てみよう。

>>> import re
>>> re.search('[^aeiou]y$', 'vacancy')  
<_sre.SRE_Match object at 0x001C1FA8>
>>> re.search('[^aeiou]y$', 'boy')      
>>> 
>>> re.search('[^aeiou]y$', 'day')
>>> 
>>> re.search('[^aeiou]y$', 'pita')     
>>> 
  1. この正規表現はvacancyにマッチする。この単語はcyで終わっていて、caeiouのいずれでもないからだ。
  2. boyはマッチしない。oyで終わっているが、yの前の文字がoであってはならないと明示されているからだ。dayもマッチしない。この単語はayで終わっているからだ。
  3. pitaはマッチしない。 この単語はyで終わっていないからだ。
>>> re.sub('y$', 'ies', 'vacancy')               
'vacancies'
>>> re.sub('y$', 'ies', 'agency')
'agencies'
>>> re.sub('([^aeiou])y$', r'\1ies', 'vacancy')  
'vacancies'
  1. この正規表現は、あなたの望む通り、vacancyvacanciesに、agencyagenciesに変えてくれる。ただし、この正規表現はboyboiesに変えてしまうことに注意すること。この関数でこういったことが起こらないのは、re.subで処理するかどうかをre.searchを使って最初に判別しているからだ。
  2. ついでに次のことを指摘しておきたい。この二つの正規表現(一方が規則を適用すべきかを調べ、他方が実際にその規則に従って処理する)を組み合わせて一つの正規表現にすることもできるのだ。これはその例だが、この大部分は見慣れたものだろう。例えば、ケーススタディ: 電話番号をパースするで学んだ、グループを表す括弧が使われていて、ここではyの直前にある文字を記憶するのに使われている。一方で、置換文字列の中では\1という新しい構文が使われているが、これは「ねえ、最初に記憶したグループがあったよね? それをここに置いてよ」ということを表している。この例では、yの前のcを記憶しているので、置換を行うと、ccで、yiesで置き換えることになる(記憶されるグループが複数ある場合は、\2\3というように使うことができる)

正規表現による置換は非常に強力なものだが、\1構文を加えると一層強力なものになる。しかし、全ての処理を一つの正規表現にまとめてしまうと、コードがかなり読みにくくなってしまうし、最初に述べた複数形化の規則の表現とも直接は対応しなくなってしまう。もとはといえば、これらの規則は「もし、単語がS、X、Zのいずれかで終わっているなら、ESを加えよ」というように表されていた。plural()関数を見てみれば、「もし、単語がS、X、Zのいずれかで終わっているなら、ESを加えよ」という内容を表す二行のコードがあることがわかるだろう。これ以上に率直な書き方はまず無いと言っていいぐらいだ。

関数のリスト

では、このコードを抽象化していくとしよう。この章では「これこれなら、あの処理をなせ。それ以外の場合は、次の規則にすすめ」という規則のリストを定義することから始めた。ここでは、とりあえずプログラムの一部を複雑化させることで、他の部分を単純化してみよう。

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

import re

def match_sxz(noun):
    return re.search('[sxz]$', noun)

def apply_sxz(noun):
    return re.sub('$', 'es', noun)

def match_h(noun):
    return re.search('[^aeioudgkprt]h$', noun)

def apply_h(noun):
    return re.sub('$', 'es', noun)

def match_y(noun):                             
    return re.search('[^aeiou]y$', noun)

def apply_y(noun):                             
    return re.sub('y$', 'ies', noun)

def match_default(noun):
    return True

def apply_default(noun):
    return noun + 's'

rules = ((match_sxz, apply_sxz),               
         (match_h, apply_h),
         (match_y, apply_y),
         (match_default, apply_default)
         )

def plural(noun):
    for matches_rule, apply_rule in rules:       
        if matches_rule(noun):
            return apply_rule(noun)
  1. マッチの規則が、それぞれre.search()関数を実行した結果を返す関数で表されるようになっている。
  2. 処理の規則もそれぞれre.sub()を呼び出す関数で表されていて、適切な複数形化の規則に基づいて処理するようになっている。
  3. いくつもの規則が入った一つの関数(plural())を作る代わりに、rulesというデータ構造、つまり関数のペアからなるシーケンスを作っている。
  4. 複数形化の規則が取り出されて別のデータ構造に収められているため、新しいplural()はほんの数行になっている。ここでは、このようにforループを使うことで、マッチのための規則と処理のための規則の二つをrules構造体から一度に取り出している。このfor文の最初のループでは、mathes_ruleにはmatch_sxzが渡され、apply_ruleにはapply_sxzが渡される。一方、(一回目では終わらなかったとして)二回目のループではmatches_rulematch_hが割り当てられ、apply_ruleapply_hが割り当てられることになる。さらに言うと、この関数は必ず何らかの値を返すことが保証されている。というのも、最後のループのマッチの規則(match_default)は単純にTrueを返すので、ループの終わりまで来た場合には、常にこのマッチの規則に対応する処理規則(apply_default)が適用されることになるからだ。

このテクニックが上手くいくのは、Pythonにおいては関数をはじめとするあらゆるものがオブジェクトだからだ。このrulesというデータ構造には関数が入っている — 関数の名前ではなく、実際の関数オブジェクトが入っているのだ。そして、これらの関数がforループの中で変数に代入されると、mathes_ruleapply_ruleは実際に呼び出すことのできる関数になるのだ。だから、このfor文の最初のループは、matches_sxz(noun)を呼び出して、マッチしたらapply_sxz(noun)を呼び出すという処理に等しいことになる。

ここで加えた抽象化がよく分からなかったら、この関数を展開してみて、それで得られた等価なコードを見てみるとよい。このforループ全体は以下のコードに等しい:


def plural(noun):
    if match_sxz(noun):
        return apply_sxz(noun)
    if match_h(noun):
        return apply_h(noun)
    if match_y(noun):
        return apply_y(noun)
    if match_default(noun):
        return apply_default(noun)

このように抽象化を施す利点は、plural()関数が単純化されるということにある。この関数は、どこか別の所で定義された規則を含むシーケンスをとり、それをイテレートしていくという一般的な処理を行うのだ。

  1. マッチルールを取得しろ。
  2. マッチした? なら、処理のルールを呼び出して、その結果を返してくれ。
  3. マッチしなかった? それなら1に戻れ。

これらの規則はどこで定義してもよいし、どのように定義しても問題ない。このplural()関数はそんなことには構わないのだ。

では、この抽象化にはそれだけの価値があっただろうか? 実は、今のところはない。例えば、この関数に新しい規則を加える時のことを考えてみよう。最初のコードだと、plural()関数にif文を加えなくてはならない。二番目のコードだと、match_foo()apply_foo()の二つの関数を新たに定義した上で、rulesシーケンスを書き換えて、この新しいマッチ関数と処理関数が、他の関数との関係で何番目に呼び出されるべきなのかを定めなくてはならない。

しかし、ここまでのことは次の節のための足がかりに過ぎないのだ。さあ先に進もうか……

パターンのリスト

マッチと処理のための規則の一つ一つについて、個別に名前のある関数を定義する必要は、実のところあまりない。そもそも、これらの関数は直接呼び出されるわけではなく、rulesシーケンスに加えられた上で、そこから呼び出されるのだ。もっと言えば、各々の関数は次の二つのパターンのうちのどちらかに従っている。つまり、全てのマッチ関数はre.search()を呼び出し、全ての処理関数はre.sub()を呼び出すのだ。新しい規則を定義するのがもっと楽になるように、このパターンを取り出してみよう。

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

import re

def build_match_and_apply_functions(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)                           
  1. build_match_and_apply_functions()は他の関数を動的に生成する関数だ。この関数はpatternsearchreplaceを引数にとり、mathes_rule()関数を定義する。このmathes_rule()関数は、build_match_and_apply_functions()関数に渡されたpatternと、mathes_rule()自身に渡されるwordを引数としてre.search()を呼び出す。ワーオ。
  2. 処理関数も同じように生成される。この処理関数は一つの引数をとる関数で、build_match_and_apply_functions()に渡されたsearchreplaceと、このapply_rule()自身に渡されたwordを引数としてre.sub()を呼び出している。このように、動的な関数の中で外部の引数の値を使うテクニックをクロージャと呼ぶ。実質的には、これらの定数は処理関数内部で定義されていると言える。処理関数は一つの引数(word)をとるのだが、この関数が定義されるときに定められる他の二つの値(searchreplace)も用いるのだ。
  3. 最後に、このbuild_match_and_apply_functions()関数は二つの値が入ったタプル、つまりたった今生成した二つの関数からなるタプルを返す。これらの関数の中で定めた定数(matches_rule()の中のpatternapply_rule()の中のsearchreplace)は、build_match_and_apply_functions()から返されたあとも残ってくれる。すばらしい。

これが途方もなくややこしいものに感じられるなら(そうなってることだろうと思う。これはずいぶん変わったものなのだから)、これがどのように使われるのかを見れば、もっとはっきりとするだろうと思う。

patterns = \                                                        
  (
    ('[sxz]$',           '$',  'es'),
    ('[^aeioudgkprt]h$', '$',  'es'),
    ('(qu|[^aeiou])y$',  'y$', 'ies'),
    ('$',                '$',  's')                                 
  )
rules = [build_match_and_apply_functions(pattern, search, replace)  
         for (pattern, search, replace) in patterns]
  1. 複数形化の「規則」は、文字列(関数ではない)のタプルからなるタプルとして定義されている。それぞれのグループの最初の文字列はre.search()で用いる正規表現のパターンで、これを使って名詞を選別する。各グループの二つ目と三つ目の文字列は、検索と置換のための正規表現で、re.sub()の中で用いて実際に規則を適用し、名詞を複数形にする。
  2. ここのフォールバックの部分に少し変更が加えられている。前のコードだと、match_default()関数が単純にTrueを返すようになっていて、他の個別の規則のいずれにもマッチしなかった場合には、与えられた単語の終わりにsを付けるという処理を施していた。このコードもそれと機能的に等価な処理を行っている。最後の正規表現はその単語に終わりがあるかどうかを調べるものだ($は文字列の末尾にマッチする)。もちろん、空白文字を含むあらゆる文字列には終わりがあるので、この正規表現はどんな文字列にもマッチすることになる。つまり、これは常にTrueを返すmatch_default()と同じ役割を果たすことになるのだ。結局のところ、このコードのおかげで、特定の規則のどれともマッチしないような場合には、その単語の末尾にsがつけられることになる。
  3. この行は魔法みたいなコードだ。patternsに収められた、文字列からなるシーケンスを引数にとり、関数の入ったシーケンスに変えているのだ。でもどうやって? build_match_and_apply_functions()関数に文字列を「対応させる」ことでだ。つまり、三つ組の文字列をそれぞれ取り上げ、これらの文字列を引数としてbuild_match_and_apply_functions()を呼び出しているのだ。 build_match_and_apply_functions()は二つの関数からなるタプルを返すので、結局このrulesは機能的に前のコードのものに等しいもの—関数のペアのタプルからなるリスト—になる。ちなみに、この一つ目の関数はre.search()を呼び出すマッチ関数で。二つ目の関数はre.sub()を呼び出す処理関数だ。

このバージョンのスクリプトを締めくくるのは、メインのエントリーポイントとなるplural()関数だ。

def plural(noun):
    for matches_rule, apply_rule in rules:  
        if matches_rule(noun):
            return apply_rule(noun)
  1. rulesリストは前のコードのものと同じものなので(そう、本当に同じものだ)、plural()関数に何も手が加えられていなくても少しも驚くことはない。これは規則を表す関数のリストを受け取って順番に呼び出していくだけの、完全に一般的な関数なのであって、その規則がどのように定義されるかなんてことには構わないのだ。前のコードでは、これらはそれぞれ名前のついた関数として定義されていた。一方、このコードではbuild_match_and_apply_functions()関数の戻り値と文字列のリストを対応させることで動的に生成されている。しかし、こういったことはplural()関数に何の影響も与えないのだ。この関数は以前と同じように動いてくれる。

パターンのファイル

これで重複していたコードは全て取り除いたし、抽象化も、名詞を複数形にする規則を文字列のリストとして定義できるようになるまでに進めてある。当然の流れとして、次はこの文字列を取り出して別のファイルに保存できるようにする。こうすることで、コードとは別々に管理できるようになるのだ。

まずはテキストファイルを作って、そこにお好みの規則を加えよう。手が込んだデータ構造を使うのではなく、単に文字列を空白文字で三列に区切って書けばいい。そしてこれにplural4-rules.txtという名前をつけることにしよう。

[plural4-rules.txtをダウンロードする]

[sxz]$               $    es
[^aeioudgkprt]h$     $    es
[^aeiou]y$          y$    ies
$                    $    s

では、どのようにこのファイルを使うのかを見てみよう。

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

import re

def build_match_and_apply_functions(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 = []
with open('plural4-rules.txt', encoding='utf-8') as pattern_file:  
    for line in pattern_file:                                      
        pattern, search, replace = line.split(None, 3)             
        rules.append(build_match_and_apply_functions(              
                pattern, search, replace))
  1. build_match_and_apply_functions()関数は何も変更されていない。このバージョンのコードでも、クロージャを用いて、外部の関数で定義された変数を使用する二つの関数を動的に生成している。
  2. グローバル関数のopen()はファイルを開いてファイルオブジェクトを返すものだ。このコードで開いているファイルには、名詞を複数形にするためのパターンを表す文字列が入っている。また、このwith文はいわゆるコンテクストを作るもので、withブロックが終われば、たとえブロック内で例外が送出されていようが、Pythonは自動でファイルを閉じてくれる。なお、withブロックとファイルオブジェクトについてはファイルの章でより詳しく学ぶ。
  3. for line in <fileobject>というコードは、開かれたファイルから一度に一行だけデータ読み込み、その読み出したテキストをline変数に代入していくものだ。ファイルの読み込みに関してはFilesの章でもっと詳しく学ぶことになる。
  4. ファイルの各行には三つの値が入っているのだが、これらの値は空白文字(タブかスペース。どちらでも違いはない)で区切られている。ここから値を切り出すには、split()という文字列メソッドを使えばいい。split()関数の一番目の引数はNoneになっているが、これは「空白文字(タブかスペース。違いはない)があったらそこで分割する」ということを意味している。二番目の引数は3だが、こちらは「空白文字で3回だけ行を分割して、残りはそのままにしておく」ということを表している。例えば、[sxz]$ $ esのような行は分割されて['[sxz]$', '$', 'es']というリストになり、結局、patternには'[sxz]$'searchには'$'、replaceには'es'が渡されることになる。たった一行の短いコードでこれだけのことができるのだ。
  5. ようやく、build_match_and_apply_functions()関数にpatternsearchreplaceが渡されて、その戻り値として関数のタプルが得られた。このタプルをrulesに加えていけば、最後には、rulesはマッチと処理のための関数が収まったリストになる。このリストこそplural()関数が求めているものだ。

ここでの改良点は、名詞を複数形にする規則を外部のファイルに分離して、コードとは別に管理できるようにしたことにある。コードはコード、データはデータ。人生は素晴らしい。

ジェネレータ

規則が入ったファイルの解析も行う、より包括的なplural()関数があったらすばらしいと思わないだろうか? 規則を取得する、マッチするかどうかをチェックする、適切な変換を施す、次の規則に移る。plural()関数にこれだけの機能があれば十分だし、またまさしくこの全ての機能を備えているべきだとも言える。

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

def rules(rules_filename):
    with open(rules_filename, encoding='utf-8') as pattern_file:
        for line in pattern_file:
            pattern, search, replace = line.split(None, 3)
            yield build_match_and_apply_functions(pattern, search, replace)

def plural(noun, rules_filename='plural5-rules.txt'):
    for matches_rule, apply_rule in rules(rules_filename):
        if matches_rule(noun):
            return apply_rule(noun)
    raise ValueError('no matching rule for {0}'.format(noun))

こいつは一体どのように動くんだ? まずはインタラクティブな例を見てみよう。

>>> def make_counter(x):
...     print('entering make_counter')
...     while True:
...         yield x                    
...         print('incrementing x')
...         x = x + 1
... 
>>> counter = make_counter(2)          
>>> counter                            
<generator object at 0x001C9C10>
>>> next(counter)                      
entering make_counter
2
>>> next(counter)                      
incrementing x
3
>>> next(counter)                      
incrementing x
4
  1. yieldというキーワードは、make_counterが普通の関数ではないことを示している。これは一度に一つだけ値を生成するという特殊な関数なのだ。レジューム(再開)できる関数だと捉えることもできる。この関数を呼び出すとジェネレータが返されるのだが、このジェネレーターを使えばxの値を連続的に生成していくことができる。
  2. make_counterジェネレータのインスタンスを作るには、他の関数と同じように呼び出せばいい。気をつけてほしいのは、このように呼び出してもこの関数内のコードは実行されないということだ。現にmake_counter()関数は一行目でprint()を呼び出しているのに、ここには何も出力されていない。
  3. make_counter()関数はジェネレータオブジェクトを返す。
  4. next()関数はジェネレータオブジェクトを受け取って、そのジェネレータが次に生成する値を返す。最初にcounterを渡してnext()関数を呼び出した時には、make_counter()の一番初めからyield文までのコードが実行され、さらにyieldされた値が返される。この場合、make_counter(2)としてこのジェネレータを生成したので、2が返されることになる。
  5. 同じジェネレータオブジェクトを渡してnext()を再び呼び出すと、中断した所から処理を再開し、次のyield文にあたるまでコードを実行してゆく。あらゆる変数やジェネレータ内部の状態などはyield文が実行されたときに保存され、next()が呼び出されると復元されるのだ。この例では、実行を待っていた次の行のコードはprint()を呼び出してincrementing xと出力するものだった。そして、それに続いてx = x + 1という代入文が実行され、それからwhile以下を再びループすることになる。whileループの一番初めにあるのはyield xだから、そこで状態が保存されて現在のxの値(つまり3)が返されるのだ。
  6. 二度目にnext(counter)を呼び出した時にも、全く同じ処理が行われる。ただし、今度はxの値が4になっている。

make_counterのループには終わりが無いので、原理的にはこれを永久に繰り返せる。このジェネレータはいつまでもxをインクリメントして、その値を返し続けてくれることだろう。しかしここではもっと生産的なジェネレータの使い方を見ることにしよう。

フィボナッチ数列ジェネレータ

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

def fib(max):
    a, b = 0, 1          
    while a < max:
        yield a          
        a, b = b, a + b  
  1. フィボナッチ数列とは、それぞれの数がその直前の二つの数の和になっているような数列のことだ。この数列は0と1から始まり、最初は緩やかに増加していくのだが、後の方になればなるほど急激に値は大きくなってゆく。この数列を生成し始めるには、まず二つの変数を用意しなくてはならない。つまり、0から始まるaと、1から始まるbだ。
  2. aは数列の現時点での値を表している。だから、これをyieldする。
  3. bは数列の次の値を表す。だからこれをaに代入するのだが、同時に次の値(a + b)も計算しておき、後で使えるようにbに代入しておく。この処理が平行して行われていることに注意してほしい。もし、a3b5なら、a, b = b, a + bとするとaには5(直前のbの値)が代入され、bには8(直前のabの値の和)が代入される。

これで、連続的にフィボナッチ数を返していく関数ができあがった。もちろん、再帰を使っても同じことができるのだが、こうする方が読みやすくなるのだ。ちなみに、この関数はforループで使うこともできる。

>>> from fibonacci 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
>>> list(fib(1000))          
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987]
  1. fib()のようなジェネレータはforループで直接使うことができる。forループは自動でnext()を呼び出して、fib()ジェネレータから値を受け取り、forループのインデクス変数(n)に代入してくれる。
  2. forループを反復する度に、fib()yield文がnに新しい値を渡してくれるので、後はその値を出力するだけでいい。fib()の数列が終わりに達したら(amax、つまりこの例で言えば1000を越えた場合)、forループはそこで何の問題も起こすことなく終了する。
  3. これは便利な表現だ。list()関数にジェネレータを渡すと、(直前の例のforループと同じように)ジェネレータ全体をイテレートして、その全ての値からなるリストを返してくれるのだ。

名詞を複数形にする規則のジェネレータ

plural5.pyに戻って、このバージョンのplural()関数がどのように動くのかを見てみよう。

def rules(rules_filename):
    with open(rules_filename, encoding='utf-8') as pattern_file:
        for line in pattern_file:
            pattern, search, replace = line.split(None, 3)                   
            yield build_match_and_apply_functions(pattern, search, replace)  

def plural(noun, rules_filename='plural5-rules.txt'):
    for matches_rule, apply_rule in rules(rules_filename):                   
        if matches_rule(noun):
            return apply_rule(noun)
    raise ValueError('no matching rule for {0}'.format(noun))
  1. ここには何も不可思議なことはない。規則を収めたファイルの各行には空白文字で区切られた3つの値が入っていたことを思い出そう。だから、ここではline.split(None, 3)を使って三つの「列」を取得し、それぞれをローカル変数に代入しているのだ。
  2. そしてここでyieldする。 何をyieldするかって? 二つの関数だよ。これらの関数はお馴染みのbuild_match_and_apply_functions()関数で生成されたものだが、この関数は前のコードから全く変わっていない。要するに、rules()はマッチと処理のための関数をオンデマンドで返してくれるジェネレータなのだ。
  3. rules()はジェネレータなので、forループでそのまま使える。forループを最初に実行した時には、まずrules()関数が呼び出される。この関数は、パターンファイルを開き、最初の行を読み出した上で、その行に入っているパターンからマッチ関数と処理関数を動的に生成し、その二つの関数をyieldしてくれる。forループを二度目に反復したときには、rules()関数が停止した地点(for line in pattern_fileループの途中)から処理を始めることになる。つまり、最初にファイル(これはまだ開かれたままだ)の次の行の読み出した上で、その行にあるパターンに基づいて別のマッチ関数と処理関数を動的に生成し、この二つの関数をyieldするのだ。

これは四番目のコードより何が優れているんだろう? 起動時間だ。四番目のコードでは、plural4モジュールをインポートすると、plural()を呼び出すことを考える間もなく、パターンファイル全体が読み込まれ、適用される可能性のある規則全てを集めたリストが生成された。他方、ジェネレータを使えば、のらくらと処理していける。最初に一つ規則を読みこんで関数を作り、試してみる。それで上手くいけば、残りのファイルを読み込んだり別の関数を作ったりすることもない。

代わりに何を失うのだろう? パフォーマンスさ! plural()関数を呼び出すたびに、rules()ジェネレータは最初から処理をやりなおす — パターンファイルを再び開いて、また一行目から読み出していくのだ。それも一度に一行ずつ。

この二つの長所を兼ね備えることができるとしたらどうだろう。つまり、起動時の負担を最小にして(要するにimportしたときにコードを一つも実行することなく)、かつパフォーマンスを最大にするのだ(同じ関数を何度も何度も作り直さない)。ああ、それと同じ行を繰り返し読み込まなくてよいなら、規則を別のファイルに保存しておきたいね(なぜならコードはコードでデータはデータだから)。

このためには自前のイテレータを作る必要があるが、それをやる前に、まずはPythonのクラスについて学ぶ必要がある。

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

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