このブログを検索

第6章 クロージャとジェネレータ

6.1. 没頭しよう


両親が司書と英文科卒だったこともあり、私は常に言語に魅了され続けてきました。プログラミング言語にではありません。そう、プログラミング言語ではなく、自然言語にです。英語の特徴をみてみると、英語は統合失調症のような言語で、例えばドイツ語、フランス語、スペイン語、ラテン語から単語を借りています。実際は"借りている"という言葉は正確ではありません。"略奪している"という表現がより相応しいでしょう。あるいはボーグのように"同化している"のです。

我々はボーグだ。言語的、語源的な独自性は吸収される。抵抗は無意味だ。

この章では、名詞の複数形を作るコードについて学びます。また、他の関数を返す関数、正規表現の応用、さらにジェネレータについても学びます。まずは、どうやって名詞の複数形を作るかについて話しましょう。(正規表現の章を読んでいないのであれば、今すぐ読んでおくとよいでしょう。この章は正規表現の基礎を理解していることを前提としていて、すぐに応用的な使い方に進みます)

英語が母国語であったり、学校で学んだのであれば、複数形名詞を作るルールはご存知でしょう。

単語がS、X、Zで終わるときはESを追加します。bassはbassesになります。faxはfaxes、waltzはwaltzesになります。単語が有声のHで終わるときはESを付けますが、無声のHで終わるときはSをつけます。有声のHというのは、他の文字と合わさって、聞こえる音を作るHのことです。つまり、coachはcoaches、rashはrashesとなりますが、CH、SHの部分は、話すときに聞こえる音になっています。一方、cheetahではHが無声なのでcheetahsになります。

単語がYで終わってI[イー]のように発音する場合は、Yの部分をIESに変えます。Yが母音とつながって別の発音をする場合はSをつけるだけです。つまり、vacancyはvacanciesになりますが、dayはdaysとなります。(知ってのとおり、多くの例外があります。manはmen、womanはwomenになりますが、humanはhumansになります。mouseはmice、louseはliceですが、houseはhousesとなります。knifeはknivesですが、lowlifeはlowlifesです。sheep、deer、haikuのような固有の複数形をもつ単語の話はここでは扱いませ。)もちろん、他の言語では全く状況が異なります。

では、英語の名詞を自動で複数形にするPythonライブラリを設計しましょう。4つだけの条件からスタートしますが、きっと増やすことになります。

6.2. そうだ!正規表現を使おう!

単語を見るということは、英語であれば文字の列を見るということです。異なる文字の組み合わせを見つけて、異なる反応をするというルールになっています。これは正規表現がやっていることによく似ています。
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]は「s、x、zのいずれか1つだけにマッチする」という意味になります。$は以前にも出てきましたが、文字列の末尾にマッチします。これらを合わせると、この正規表現は名詞がs、x、zのいずれかで終わるかどうかをテストしています。

②このre.sub()関数は、文字列に対して正規表現による置換を行います。

正規表現を使った置換をもっと詳しく見ていきましょう。

>>> import re
>>> re.search('[abc]', 'Mark')    # ①
<_sre .sre_match="" 0x001c1fa8="" at="" object="">
>>> re.sub('[abc]', 'o', 'Mark')  # ②
'Mork'
>>> re.sub('[abc]', 'o', 'rock')  # ③
'rook'
>>> re.sub('[abc]', 'o', 'caps')  # ④
'oops'

①文字列Markは[a、b、c]を含んでいますか?はい、aを含んでいます。

②a、b、cのいずれかを見つけたので、oと置換えます。MarkはMorkになります。

③同じ関数を使ってrockをrookに置き換えます。

④capsはoapsになる、と思うかもしれませんが、re.subはマッチした最初のものだけでなく、すべてを置換します。この場合は、c、aがoになるのでcapsはoopsになります。

では、先ほどの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'

①ここでは、$でマッチした文字列の最後を、文字列esで置き換えています。つまり、esを文字列に追加しています。同じ結果は「名詞 + "es"」のように文字列を結合しても得られますが、今はあえて各条件に対して正規表現を使っています。理由はこのあとで説明します。

②よく見ると、これは新しいバリエーションです。^が[ ] 中の文字列の最初に来るときは、特別な意味(否定)になります。[^abc]は「a、b、c 以外のすべての文字」という意味です。[^aeioudgkprt]であれば、「a、e、i、o、u、d、g、k、p、t、r以外のすべての文字」という意味です。さらに、hが文字列の最後に来るかを確認しており、有声のHが最後にある単語を探す構文になっています。

③ここも②と同じです。Yで終わって、直前がa、e、i、o、uでない単語とマッチしています。Iのように発音するYを末尾にもつ単語を探す構文になっています。

否定の正規表現をもっと見ていきましょう。

>>> import re
>>> re.search('[^aeiou]y$', 'vacancy')  # ①
<_sre .sre_match="" 0x001c1fa8="" at="" object="">
>>> re.search('[^aeiou]y$', 'boy')      # ②
>>>
>>> re.search('[^aeiou]y$', 'day')
>>>
>>> re.search('[^aeiou]y$', 'pita')     # ③
>>>

①vacancyはこの正規表現にマッチします。cyで終わっており、cはa、e、i、o、uではありません。

②boyはマッチしません。oyで終わっていますが、yの前はoであってはならないからです。同様にdayはayで終わっているのでマッチしません。

③pitaはマッチしません。yで終わっていないからです。
>>> re.sub('y$', 'ies', 'vacancy')               # ①
'vacancies'
>>> re.sub('y$', 'ies', 'agency')
'agencies'
>>> re.sub('([^aeiou])y$', r'\1ies', 'vacancy')  # ②
'vacancies'

①この正規表現はvacancyをvacanciesに、agencyをagenciesに変換します(想定どおり)。boyだとboiesとなってしまいますが、re.searchを先に実行してからre.subを実行するかどうかを決めるので、実際にはboyには変換は行われません。

②このように、2つの正規表現を1つに繋げることも可能です。1つは条件を当てはめるかどうかを判定し、もう1つは実際に条件を当てはめます。

これらの正規表現は、ほとんどを見たことがあるでしょう。記憶グループを使っていますが、これはCase Study: Parsing Phone Numbersで学んだことです。このグループによって文字yの前の文字を記憶することができます。置換文字列では、新しい構文「\1」を使います。”おっと、最初の記憶グループかい?そこに置いていきな”という意味になります。この場合、cがyの前にあると記憶しているので、置換のときは、cとcの場所に、iesをyの場所に置き換えています。(さらに多くの記憶グループがあるときは「\2」、「\3」、、、を使います)

正規表現による置換は極めて強力ですが、「\1」構文を使うことでさらに便利になります。しかし、すべての操作を1つの正規表現で書いてしまうと非常に読み難くなってしまって、複数形を作る条件を最初に書いたときの考え方と直接繋がらなくなってしまいます。

もともとは「単語がs、x、zで終わればesを加える」という条件を作ったのでした。この関数では、同じ条件を2行のコードで記述するので、直接的ではなくなっています。

6.3. 関数のリスト


抽象化のレベルを上げていきましょう。最初は「もしこうなら、こうします。そうでなければ次のルールへ」というように、条件のリストを定義していました。ここからは、他の部分をシンプルにするために、プログラムの一部を一時的に複雑にします。
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)

①各マッチ条件は関数で、re.search()関数の結果を呼び出します。

②各apply条件も関数で、re.sub()関数を呼び出して当てはまる複数化ルールを適用します。

③1つの関数 plural() に複数の条件を与える代わりに、関数のペアのシーケンスで条件のデータストラクチャrulesを作ります。

④rulesがデータストラクチャに分解されるので、新しいplural()関数は、数行のコードに短縮されます。forループを使って、2つの条件(matchとapply)を同時に条件ストラクチャから抜き出します。forループの最初のイタレーションではmatches_ruleはmatch_sxzを受け取り、apply_ruleはapply_sxzを受け取ります。次のイタレーションでは(かなりあとになることを想定していますが)、matches_ruleにはmatch_hを代入して、apply_ruleにはapply_hを代入します。最後のmatch条件(match_default)は単にTrueを返し、対応するapply条件(apply_default)が常に適用されるので、最終的に関数は必ず何かを返すことになります。

Pythonでは関数を含むすべてがオブジェクトですから、このテクニックが可能になります。rulesデータストラクチャは(名前ではなく)関数オブジェクトそのものを含んでいます。

forループに代入されたときはmatches_ruleとapply_ruleが実際に呼び出されます。forループの最初イタレーションはmathces_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. マッチするでしょうか?その場合はapply条件を呼び出して結果を返します
3. マッチしませんか?その場合は1に戻ります

条件はどこで定義しても、どんな方法でも構いません。plural()関数には影響しません。

では、追加した階層抽象化は役に立つでしょうか?実は、今はまだです。関数に新しい条件を追加するにはどうしたらよいか考えましょう。最初の例では、plural()関数にif文を加える必要がありました。

この2番目の例では2つの関数match_foo()、apply_foo()を加える必要があるので、条件シーケンスを変更して順番のどこで新しいmatch、apply関数が他の条件に対して呼び出されるかを特定します。

実は、ここまでは次の節のための布石のようなものです。どんどん進みましょう。

6.4. パターンのリスト

実用上は、match、applyというルールに対して名前をつけた関数をそれぞれ定義する必要はありません。直接呼び出さずに条件シーケンスに追加して呼び出すからです。

さらに、各関数は2つのパターンのうちの1つに従います。match関数はすべてre.search()を呼び出し、apply関数はre.sub()を呼び出すのです。もっと簡単に新しいルールを作れるようにパターンを分析してみましょう。
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)                           # ③
①build_match_and_apply_functions()は動的に他の関数を作る関数で、pattern、search、 replaceを受け取ります。次にmatchs_rule()関数を定義して、patternとwordを使ってre.search()を呼び出します。patternはbuild_match_and_apply_functionsに渡されたもので、wordはmatchs_rule()に渡されたものです。

②apply関数も同じように作ります。1つのパラメータwordを受け取り、build_match_and_apply_functions()に渡されたsearchとreplaceと、apply_rule()関数に渡されたwordを使ってre.sub()を呼び出します。この手法は、動的な関数の外にあるパラメータ値を使っていて、クロージャと呼ばれます。動的に作られるapply関数の中では、本質的には定数を定義しているのですが、1つのパラメータ(word)を受け取り、それに加えて2つの値(search, replace)に従うことになります。これがapply関数を定義したときに決められたことです。

③最後に、build_match_and_apply_functions()関数は2つの値のタプルを返します。
先ほど作った2つの関数です。関数の中で定義した定数(matches_rules()関数の中のpattern、apply_rule()関数の中のsearch、replace)は、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]

①複数化のルールは、文字列タプルのタプルで定義されています(関数ではありません)。各グループの最初の文字列は正規表現のパターンで、re.search()でルールがマッチするかの判定に使います。各グループの2、3番目の文字列はsearch、replaceの表現で、re.sub()が実際にルールを適用して名詞を複数形に変換するために使われます。

②ここに少し変更があります。フォールバックルールの部分です。前の例では、match_default()関数は単純にTrueを返しました。このコードは、上記の特定ルールがマッチしなければ与えられた単語にsを加えるだけ、となります。この例は機能としては同じことをしています。最後の正規表現は単語が終点まで来たかを判定します($は文字列の最終点にマッチ)。もちろん、文字列には必ず末尾があります。たとえ空の文字列であっても末尾は存在するため、この表現は常にマッチします。このようにmatch_default()関数と同じ目的で、常にTrueを返します。他のルールがマッチしないことを確認し、コードは与えられた単語の最後にsを加えます。

③これは魔法のワンライナーです。patternsにある文字列のシーケンスを受け取り、関数のシーケンスに変換します。どのようにしているかというと、文字列をbuild_match_and_apply_functions()に”マッピング”しているのです。つまり、毎回3つの文字列を受け取り、それらを引数として使ってbuild_match_and_apply_functions()関数を呼び出しています。build_match_and_apply_functions()関数は、2つの関数のタプルを返します。つまり、rulesは前の例と機能的に等価なのです。タプルの入ったリストで、各タプルには関数のペアが入っています。

最初の関数はre.search()関数を呼び出すmatch関数で、二番目はapply関数を呼び出すre.sub()関数です。スクリプトがここまで完成したので、plural()関数の入口に到達しました。
def plural(noun):
    for matches_rule, apply_rule in rules:  # ①
        if matches_rule(noun):
            return apply_rule(noun)

①rulesリストは前の例と(全く)同じなので、plural()関数が全く変わらないことに驚きはありません。これは一般的なやり方で、条件関数のリストを受け取って順番に呼び出すものです。ルールがどのように定義されているかは関係ありません。前の例では、別々の名前の関数で定義されていました。今はbuild_match_and_apply_functions()関数の文字列のリストにマッピングした結果で動的に作られています。それでもplural()関数は問題なく同じように動きます。

6.5. パターンファイル


重複したコードをすべて外に出したり、充分な抽象化をして、複数化ルールが文字列のリストで定義できました。次の論理ステップは、これらの文字列を受け取って別々のファイルに保存し、操作するコードとは別に管理することです。

まず、所望の条件を持つテキストを作ります。きっちりとしたデータストラクチャは必要ありません。スペースをデリミタにした文字列が3列あればよいでしょう。plural4-rules.txtと名前をつけましょう。
[sxz]$               $    es
[^aeioudgkprt]h$     $    es
[^aeiou]y$          y$    ies
$                    $    s

どうやってこのルールファイルを使うのか、見ていきましょう。

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))

①build_match_and_apply_functions()関数の変更はありません。クロージャを使って2つの関数を動的に作ります。その2つでは、関数の外で定義した変数を使います。

②グローバル関数open()はファイルを開いてファイルオブジェクトを返します。このとき、開くファイルには複数化する名詞のpattern文字列が含まれています。with節ではコンテクストと呼ばれるものを作ります。withブロックが終わったら、たとえブロック内に例外があがっていても、Pythonは自動的にファイルを閉じます。withブロックやファイルオブジェクトについてはFileの章<11章>で詳しく学びます。

③for line in <ファイルオブジェクト>という定型文を使って、開いたファイルの中のデータを1行ずつ読んで変数lineに代入します。ファイルからの読み込みについては、Fileの章で詳しく学びます。

④ファイルの各行は実際には3つの値を持っていますが、空白で(タブ、スペースでもよい)区切られています。これを分離するには、文字列メソッドのsplit()を使います。split()メソッドの最初の引数Noneの意味は、「(タブでもスペースでも関係なく)どういう空白でもよい」です。2番目の引数は3で、「空白で3回分割、行の残りはそのまま」という意味です。[sxz]$ $ es のような行は['[sxz]$', '$', 'es']というリストに分割され、patternは'[sxz]$'、searchは '$'、replaceは 'es'を受け取ります。たった1行のコードで沢山のことをやっています。

⑤最終的に、pattern、search、replaceをbuild_match_and_apply_functions()関数に渡し、関数のタプルが返ってきます。条件のリストにこのタプルを追加し、rulesはplural()関数に入るmatch、apply関数のリストを格納します。

ここでの改良点は、複数化ルールを分割してすべて外部ファイルにして、これらを使うコードとは別々に管理できるようにしたことです。コードはコード、データはデータ。それで人生が楽しくなります。

6.6. ジェネレータ


rulesファイルを解析するplural()関数を一般化できれば素晴らしいのではないでしょうか?rulesを受け取り、match、applyをチェックして適切に変形し、次のルールを見る。これが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))

これはどう動くでしょう?まずはインタラクティブな例を見てみましょう。
>>> def make_counter(x):
...     print('entering make_counter')
...     while True:
...         yield x                    # ①
...         print('incrementing x')
...         x = x + 1
...
>>> counter = make_counter(2)          # ②
>>> counter                            # ③

>>> next(counter)                      # ④
entering make_counter
2
>>> next(counter)                      # ⑤
incrementing x
3
>>> next(counter)                      # ⑥
incrementing x
4

①make_counterにキーワードyieldがあるということは、これが普通の関数ではないことを示しています。一度に複数の値を生成する特別な関数です。レジューム可能な関数だと考えてよいでしょう。呼び出すことでジェネレータを返し、連続したxの値を生成するのに使うことができます。

②make_counterジェネレータのインスタンスを生成するには、他の関数のように呼びだせばよいだけです。実際には関数のコードは実行されないことに注意しましょう。make_counter()関数の一行目ではprint()を呼び出しますが何も出力されていないので、実行されていないことが分かります。

③make_counter()関数はジェネレータオブジェクトを返します。

④next()関数は、ジェネレータオブジェクトを受け取り、次の値を返します。はじめてnext()をカウンタージェネレータで呼び出すと、make_counter()が最初のyield文までが実行され、得られた値を返します。この場合は2が返ります。元々生成されたジェネレータはmake_counter(2)によって呼び出されたからです。

⑤再びnext()を同じジェネレータオブジェクトで呼び出して、前のところからレジュームされ次のyield文が出てくるところまで続きます。すべての変数、ローカル状態などはyieldに保存されnext()で復元されます。次のコード行はprint()を実行するのを待っていて、"increment x"を出力します。その後、x=x+1の文が実行されます。さらにwhileループを回して、最初にたどり着く文はyield x であるため、これですべての状態を保存し、現在の値(今は3)を返します。

⑥next(count)を2回目に呼び出すと、全く同じことをすることになりますが、xは今度は4になります。make_countは無限のループになるので、理論的には永遠に実行でき、xを増やして値を出力し続けることになります。

ではここで、もっと生産的なジェネレータの使い方を見てみましょう。

6.6.1. フィボナッチジェネレータ


def fib(max):
    a, b = 0, 1          # ①
    while a < max:
        yield a          # ②
        a, b = b, a + b  # ③

①フィボナッチ数列は、前の2つを足した数の列です。0、1から始まって最初はゆっくりと大きくなりますが、増加がどんどん速くなります。数列を始めるためには2つの変数が必要ですが、ここでは0から始まるaと1から始まるbを用意します。

②aは数列の現在の数で、これをyieldします。

③bは数列の次の数で、bをaに代入し、次の数として(a+b)を計算してbに代入してあとで使います。これらが並行して起こることに注意しましょう。aが3でbが5のとき、a, b = b, a + bという代入では、aに5を代入して(bの前の値)、bに8を代入します(a,bの前の値の和)。

これで連続したフィボナッチ数を出力する関数になります。実際のところ、回帰的に記述することもできますが、こちらの方が読みやすいです。また、こちらの方が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]

①fib()のようなジェネレータをforループで直接使うことができます。forループは自動でnext()関数を呼び出して、fib()ジェネレータから値を受け取り、forループのインデックス変数(n)に代入します。

②forループでは毎回、新しいnの値がfib()のyield文から渡されるので、それを出力すればよいだけです。

③これは便利な書き方です。list()関数にジェネレータを渡し、ジェネレータの中で繰り返し(前の例のforループのように)、すべての値のリストを返します。

6.6.2. 複数形ジェネレータ


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))

①不思議なことはありません。rulesファイルの各行はスペースで分けられた3つの値を持っているので、line.split(None, 3)を使って3”列”を得て3つのローカル変数に代入すればよいでしょう。

②ここでyieldを使いますが、何を生成するのでしょうか?2つの関数です。前に出てきたbuild_match_and_apply_functions()関数を使って動的に関数を作ります。

③rules()はジェネレータなので、forループで直接使うことができます。最初のループでrules()関数を呼び出し、patternファイルを開いて最初の行を読み、patternsからmatch関数とapply関数を動的に作り、その動的に作られた関数をyeildします。ループの二回目では、rules()に残っているものをピックアップします。(rules()はpattern_fileループの中の中央辺りの行にあります)。最初にすることは、ファイルの次の行を読み(ファイルはまだ開いています)、ファイルのその行のpatternsに基づいて、動的にもう1つのmatch、apply関数を作ることです。

ステージ4より何が良くなったでしょうか?準備時間です。plural4モジュールをインポートするときに、plural()関数を呼び出すかどうかを考えたりする前に、すべてのpatternファイルを読み込んで、可能なすべての条件のリストを作っていたのです。

ジェネレータを使うと、すべてを楽にすることができます。最初の条件を読んで関数を生成して試し、それが上手くいったなら残りのファイルを読んだり他の関数を作る必要がありません。

失ったものは何でしょうか?パフォーマンスです!plural()関数を呼び出すときは、rules()ジェネレータは常に最初から始まります。つまり、patternsファイルを新たに開いて最初から毎行読むことになります。

両方のいいとこ取りができるとしたら、どうでしょうか?準備の労力は最小限で(インポートのコードを実行しない)、最大のパフォーマンス(同じ関数を何度も繰り返しビルドしない)が得られます。さらに、同じ行を2度読まなくてよいのであれば、rulesを別のファイルに保存したくなります(なぜなら、コードはコード、データはデータで別々に管理したいからです)。

そうするためには、自分でイテレータを作らないといけません。その前に、Pythonクラスについて学ぶ必要があります。


6.7. さらに読むには


0 件のコメント:

コメントを投稿