関数型プログラミングHOWTO
- 著者
Kuchling
- リリース
0.31
このドキュメントでは、プログラムを機能的なスタイルで実装するのに適したPythonの機能について説明します。 関数型プログラミングの概念を紹介した後、イテレーターやジェネレーターなどの言語機能と、イテレーターや functools 。
序章
このセクションでは、関数型プログラミングの基本的な概念について説明します。 Python言語の機能について知りたいだけの場合は、次のセクションに進んでください。
プログラミング言語は、いくつかの異なる方法で問題の分解をサポートします。
- ほとんどのプログラミング言語は手続き型です。プログラムは、プログラムの入力をどう処理するかをコンピューターに指示する命令のリストです。 C、Pascal、さらにはUnixシェルも手続き型言語です。
- 宣言型言語では、解決すべき問題を説明する仕様を記述し、言語の実装は計算を効率的に実行する方法を理解します。 SQLは、最もよく知っている宣言型言語です。 SQLクエリは、取得するデータセットを記述し、SQLエンジンは、テーブルをスキャンするか、インデックスを使用するか、どの副次句を最初に実行するかなどを決定します。
- オブジェクト指向プログラムは、オブジェクトのコレクションを操作します。 オブジェクトには内部状態があり、何らかの方法でこの内部状態を照会または変更するメソッドをサポートします。 SmalltalkとJavaはオブジェクト指向言語です。 C ++とPythonはオブジェクト指向プログラミングをサポートする言語ですが、オブジェクト指向機能の使用を強制するものではありません。
- Functional プログラミングは、問題を一連の関数に分解します。 理想的には、関数は入力を受け取り、出力を生成するだけであり、特定の入力に対して生成される出力に影響を与える内部状態はありません。 よく知られている関数型言語には、MLファミリー(Standard ML、OCaml、およびその他のバリアント)とHaskellが含まれます。
一部のコンピューター言語の設計者は、プログラミングに対する1つの特定のアプローチを強調することを選択します。 これにより、異なるアプローチを使用するプログラムを作成することが困難になることがよくあります。 他の言語は、いくつかの異なるアプローチをサポートするマルチパラダイム言語です。 Lisp、C ++、およびPythonはマルチパラダイムです。 これらすべての言語で、主に手続き型、オブジェクト指向、または関数型のプログラムまたはライブラリを作成できます。 大規模なプログラムでは、さまざまなセクションがさまざまなアプローチを使用して記述される場合があります。 たとえば、処理ロジックが手続き型または機能型である場合、GUIはオブジェクト指向である可能性があります。
関数型プログラムでは、入力は一連の関数を流れます。 各関数はその入力で動作し、いくつかの出力を生成します。 関数スタイルは、内部状態を変更したり、関数の戻り値に表示されないその他の変更を加えたりする副作用のある関数を思いとどまらせます。 副作用がまったくない機能を純粋に機能的と呼びます。 副作用を回避するということは、プログラムの実行時に更新されるデータ構造を使用しないことを意味します。 すべての関数の出力は、その入力のみに依存する必要があります。
一部の言語は純度が非常に厳しく、a=3
やc = a + b
などの代入ステートメントさえありませんが、すべての副作用を回避することは困難です。 たとえば、画面への印刷やディスクファイルへの書き込みは副作用です。 たとえば、Pythonでは、print
ステートメントまたはtime.sleep(1)
の両方が有用な値を返しません。 画面にテキストを送信したり、実行を1秒間一時停止したりするという副作用のためにのみ呼び出されます。
関数型で記述されたPythonプログラムは、通常、すべてのI / Oまたはすべての割り当てを回避するという極端なことはしません。 代わりに、機能的に見えるインターフェイスを提供しますが、内部的には機能しない機能を使用します。 たとえば、関数の実装では引き続きローカル変数への割り当てが使用されますが、グローバル変数が変更されたり、その他の副作用が発生したりすることはありません。
関数型プログラミングは、オブジェクト指向プログラミングの反対と見なすことができます。 オブジェクトは、いくつかの内部状態と、この状態を変更できるメソッド呼び出しのコレクションを含む小さなカプセルであり、プログラムは、適切な状態変更のセットを作成することで構成されます。 関数型プログラミングは、状態の変化を可能な限り回避したいと考えており、関数間を流れるデータを処理します。 Pythonでは、アプリケーション内のオブジェクト(電子メールメッセージ、トランザクションなど)を表すインスタンスを取得して返す関数を作成することにより、2つのアプローチを組み合わせることができます。
機能設計は、機能するための奇妙な制約のように見えるかもしれません。 なぜオブジェクトや副作用を避ける必要がありますか? 機能スタイルには理論的および実用的な利点があります。
- 正式な証明可能性。
- モジュール性。
- 構成可能性。
- デバッグとテストのしやすさ。
正式な証明可能性
理論上の利点は、関数型プログラムが正しいことを数学的に証明するのが簡単になることです。
長い間、研究者はプログラムが正しいことを数学的に証明する方法を見つけることに興味を持ってきました。 これは、多数の入力でプログラムをテストしてその出力が通常正しいと結論付けたり、プログラムのソースコードを読み取ってコードが正しく見えると結論付けたりすることとは異なります。 代わりに、プログラムがすべての可能な入力に対して正しい結果を生成するという厳密な証明が目標です。
プログラムが正しいことを証明するために使用される手法は、不変条件、入力データのプロパティ、および常に真であるプログラムの変数を書き留めることです。 次に、コードの各行について、行が実行される前に不変量XとYが真である場合、行が実行される前にわずかに異なる不変条件X 'とY'が真である後であることを示します。実行されました。 これは、プログラムの最後に到達するまで続きます。その時点で、不変条件はプログラムの出力で必要な条件に一致する必要があります。
関数型プログラミングによる割り当ての回避は、この手法では割り当てを処理するのが難しいために発生しました。 割り当ては、先に伝播できる新しい不変条件を生成することなく、割り当ての前に真であった不変条件を壊すことができます。
残念ながら、プログラムが正しいことを証明することはほとんど実用的ではなく、Pythonソフトウェアには関係ありません。 些細なプログラムでさえ、数ページの長さの証明が必要です。 適度に複雑なプログラムの正当性の証明は膨大であり、日常的に使用するプログラム(Pythonインタープリター、XMLパーサー、Webブラウザー)のほとんどまたはまったく正しいことが証明されない可能性があります。 証明を書き留めたり生成したりしたとしても、証明を検証するという問題があります。 おそらくエラーがあり、プログラムが正しいことを証明したと誤って信じています。
モジュール性
関数型プログラミングのより実用的な利点は、問題を細かく分割する必要があることです。 その結果、プログラムはよりモジュール化されます。 複雑な変換を実行する大きな関数よりも、1つのことを実行する小さな関数を指定して作成する方が簡単です。 小さな関数は、読みやすく、エラーをチェックするのも簡単です。
デバッグとテストの容易さ
機能スタイルのプログラムのテストとデバッグは簡単です。
関数は一般に小さく、明確に指定されているため、デバッグが簡素化されます。 プログラムが動作しない場合、各機能はデータが正しいことを確認できるインターフェースポイントです。 中間の入力と出力を調べて、バグの原因となっている関数をすばやく特定できます。
各機能は単体テストの対象となる可能性があるため、テストは簡単です。 関数は、テストを実行する前に複製する必要があるシステム状態に依存しません。 代わりに、正しい入力を合成してから、出力が期待値と一致することを確認するだけです。
構成可能性
関数型プログラムで作業するときは、さまざまな入力と出力を使用して多数の関数を記述します。 これらの機能の中には、必然的に特定のアプリケーションに特化したものもありますが、さまざまなプログラムで役立つものもあります。 たとえば、ディレクトリパスを取得してディレクトリ内のすべてのXMLファイルを返す関数や、ファイル名を取得してその内容を返す関数は、さまざまな状況に適用できます。
時間の経過とともに、ユーティリティの個人ライブラリを作成します。 多くの場合、既存の関数を新しい構成に配置し、現在のタスクに特化したいくつかの関数を作成することによって、新しいプログラムを組み立てます。
イテレータ
まず、関数型プログラムを作成するための重要な基盤であるPython言語機能であるイテレーターについて見ていきます。
イテレータは、データのストリームを表すオブジェクトです。 このオブジェクトは、一度に1要素ずつデータを返します。 Pythonイテレータは、引数を取らず、常にストリームの次の要素を返すnext()
というメソッドをサポートする必要があります。 ストリームにこれ以上要素がない場合、next()
はStopIteration
例外を発生させる必要があります。 ただし、イテレータは有限である必要はありません。 無限のデータストリームを生成するイテレータを作成することは完全に合理的です。
組み込みの iter()関数は、任意のオブジェクトを受け取り、オブジェクトのコンテンツまたは要素を返すイテレーターを返そうとします。オブジェクトが反復をサポートしていない場合は、TypeError
を発生させます。 Pythonの組み込みデータ型のいくつかは反復をサポートしており、最も一般的なのはリストと辞書です。 オブジェクトのイテレータを取得できる場合、そのオブジェクトは iterable オブジェクトと呼ばれます。
反復インターフェースを手動で試すことができます。
Pythonは、いくつかの異なるコンテキストで反復可能なオブジェクトを想定しています。最も重要なのはfor
ステートメントです。 ステートメントfor X in Y
では、Yはイテレータ、またはiter()
がイテレータを作成できるオブジェクトである必要があります。 これらの2つのステートメントは同等です。
イテレータは、list()
または tuple()コンストラクタ関数を使用してリストまたはタプルとして実体化できます。
シーケンスアンパックはイテレータもサポートします。イテレータがN個の要素を返すことがわかっている場合は、それらをNタプルにアンパックできます。
max()や min()などの組み込み関数は、単一のイテレーター引数を取ることができ、最大または最小の要素を返します。 "in"
および"not in"
演算子は、イテレーターもサポートします。X in iterator
は、イテレーターによって返されるストリームでXが見つかった場合にtrueになります。 イテレータが無限大の場合、明らかな問題が発生します。 max()
、min()
は返されません。また、要素Xがストリームに表示されない場合、"in"
および"not in"
演算子も返されません。
イテレータでのみ先に進むことができることに注意してください。 前の要素を取得したり、イテレータをリセットしたり、そのコピーを作成したりする方法はありません。 イテレータオブジェクトはオプションでこれらの追加機能を提供できますが、イテレータプロトコルはnext()
メソッドのみを指定します。 したがって、関数はイテレータのすべての出力を消費する可能性があり、同じストリームで別のことを行う必要がある場合は、新しいイテレータを作成する必要があります。
イテレータをサポートするデータ型
リストとタプルがイテレータをどのようにサポートするかについては、すでに見てきました。 実際、文字列などのPythonシーケンスタイプは、イテレータの作成を自動的にサポートします。
ディクショナリで iter()を呼び出すと、ディクショナリのキーをループするイテレータが返されます。
辞書内のオブジェクトのハッシュ順序に基づいているため、順序は基本的にランダムであることに注意してください。
iter()
をディクショナリに適用すると、常にキーがループしますが、ディクショナリには他のイテレータを返すメソッドがあります。 キー、値、またはキーと値のペアを反復処理する場合は、iterkeys()
、itervalues()
、またはiteritems()
メソッドを明示的に呼び出して、適切な反復子を取得できます。
dict()コンストラクターは、(key, value)
タプルの有限ストリームを返すイテレーターを受け入れることができます。
ファイルは、ファイルに行がなくなるまでreadline()
メソッドを呼び出すことにより、反復もサポートします。 これは、次のようにファイルの各行を読み取ることができることを意味します。
セットは、イテラブルからコンテンツを取得して、セットの要素を反復処理できます。
ジェネレータ式とリスト内包表記
イテレータの出力に対する2つの一般的な操作は、1)すべての要素に対して何らかの操作を実行すること、2)ある条件を満たす要素のサブセットを選択することです。 たとえば、文字列のリストが与えられた場合、各行から末尾の空白を削除したり、特定のサブ文字列を含むすべての文字列を抽出したりできます。
リスト内包表記とジェネレータ式(短縮形:「listcomps」と「genexps」)は、関数型プログラミング言語Haskell( https://www.haskell.org/)から借用した、このような操作の簡潔な表記法です。 。 次のコードを使用して、文字列のストリームからすべての空白を取り除くことができます。
"if"
条件を追加すると、特定の要素のみを選択できます。
リストを理解すると、Pythonリストが返されます。 stripped_list
は、イテレータではなく、結果の行を含むリストです。 ジェネレータ式は、必要に応じて値を計算するイテレータを返します。すべての値を一度に実体化する必要はありません。 これは、無限のストリームまたは非常に大量のデータを返すイテレータを使用している場合、リスト内包表記は役に立たないことを意味します。 このような状況では、ジェネレータ式が適しています。
ジェネレータ式は括弧(“()”)で囲まれ、リスト内包表記は角括弧(“ []”)で囲まれます。 ジェネレータ式の形式は次のとおりです。
繰り返しますが、リスト内包表記では、外側の角かっこのみが異なります(かっこではなく角かっこ)。
生成される出力の要素は、expression
の連続する値になります。 if
句はすべてオプションです。 存在する場合、expression
は、condition
が真の場合にのみ評価され、結果に追加されます。
ジェネレータ式は常に括弧内に記述する必要がありますが、関数呼び出しを示す括弧もカウントされます。 関数にすぐに渡されるイテレータを作成する場合は、次のように記述できます。
for...in
句には、繰り返されるシーケンスが含まれています。 シーケンスは、並列ではなく左から右に繰り返されるため、同じ長さである必要はありません。 sequence1
の各要素について、sequence2
は最初からループオーバーされます。 次に、sequence3
は、sequence1
およびsequence2
から得られた要素のペアごとにループされます。
別の言い方をすれば、リスト内包表記またはジェネレーター式は、次のPythonコードと同等です。
これは、複数のfor...in
句があり、if
句がない場合、結果の出力の長さは、すべてのシーケンスの長さの積に等しくなることを意味します。 長さ3のリストが2つある場合、出力リストの長さは9要素です。
>>> seq1 = 'abc'
>>> seq2 = (1,2,3)
>>> [(x,y) for x in seq1 for y in seq2]
[('a', 1), ('a', 2), ('a', 3),
('b', 1), ('b', 2), ('b', 3),
('c', 1), ('c', 2), ('c', 3)]
Pythonの文法にあいまいさが生じるのを避けるために、expression
がタプルを作成している場合は、括弧で囲む必要があります。 以下の最初のリスト内包表記は構文エラーですが、2番目のリスト内包表記は正しいです。
発電機
ジェネレーターは、イテレーターを作成するタスクを簡素化する特別なクラスの関数です。 通常の関数は値を計算して返しますが、ジェネレーターは値のストリームを返すイテレーターを返します。
あなたは間違いなく、PythonまたはCで通常の関数呼び出しがどのように機能するかを知っています。 関数を呼び出すと、ローカル変数が作成されるプライベート名前空間が取得されます。 関数がreturn
ステートメントに到達すると、ローカル変数が破棄され、値が呼び出し元に返されます。 後で同じ関数を呼び出すと、新しいプライベート名前空間とローカル変数の新しいセットが作成されます。 しかし、関数の終了時にローカル変数が破棄されなかった場合はどうなるでしょうか。 後で中断したところから機能を再開できるとしたらどうでしょうか。 これはジェネレーターが提供するものです。 それらは再開可能な機能と考えることができます。
ジェネレーター関数の最も簡単な例を次に示します。
yield
キーワードを含む関数はすべてジェネレーター関数です。 これは、結果として関数を特別にコンパイルするPythonの bytecode コンパイラによって検出されます。
ジェネレーター関数を呼び出すと、単一の値は返されません。 代わりに、イテレータプロトコルをサポートするジェネレータオブジェクトを返します。 yield
式を実行すると、ジェネレータはreturn
ステートメントと同様に、i
の値を出力します。 yield
ステートメントとreturn
ステートメントの大きな違いは、yield
に達すると、ジェネレーターの実行状態が一時停止され、ローカル変数が保持されることです。 ジェネレータの.next()
メソッドの次の呼び出しで、関数は実行を再開します。
generate_ints()
ジェネレーターの使用例は次のとおりです。
同様に、for i in generate_ints(5)
またはa,b,c = generate_ints(3)
と書くこともできます。
ジェネレーター関数内では、return
ステートメントは値なしでのみ使用でき、値の行列の終了を通知します。 return
を実行した後、ジェネレータはそれ以上の値を返すことができません。 return 5
などの値を持つreturn
は、ジェネレーター関数内の構文エラーです。 ジェネレーターの結果の終了は、StopIteration
を手動で上げるか、実行フローを関数の下部から外すことによっても示すことができます。
独自のクラスを作成し、ジェネレーターのすべてのローカル変数をインスタンス変数として格納することで、ジェネレーターの効果を手動で実現できます。 たとえば、整数のリストを返すには、self.count
を0に設定し、next()
メソッドでself.count
をインクリメントして返します。 ただし、適度に複雑なジェネレーターの場合、対応するクラスを作成するのは非常に面倒です。
Pythonのライブラリtest_generators.py
に含まれているテストスイートには、さらに興味深い例がいくつか含まれています。 これは、ジェネレーターを再帰的に使用してツリーのインオーダートラバーサルを実装するジェネレーターの1つです。
test_generators.py
の他の2つの例では、N-Queensの問題(NxNのチェス盤にNの女王を配置して、女王が他の女王を脅かさないようにする)とKnight's Tour(騎士を正方形を2回訪問せずにNxNチェス盤)。
ジェネレータに値を渡す
Python 2.4以前では、ジェネレーターは出力のみを生成していました。 イテレータを作成するためにジェネレータのコードが呼び出されると、実行が再開されたときに関数に新しい情報を渡す方法はありませんでした。 ジェネレーターにグローバル変数を参照させるか、呼び出し元が変更する可変オブジェクトを渡すことで、この機能を一緒にハックすることができますが、これらのアプローチは面倒です。
Python 2.5には、ジェネレーターに値を渡す簡単な方法があります。 yield は式になり、変数に割り当てるか、その他の方法で操作できる値を返します。
上記の例のように、戻り値を使用して何かを行う場合は、常にでyield
式を括弧で囲むことをお勧めします。 括弧は必ずしも必要ではありませんが、必要なときに覚えておくよりも、常に追加する方が簡単です。
(PEP 342は、yield
式は、割り当ての右側の最上位の式で発生する場合を除いて、常に括弧で囲む必要があるという正確な規則を説明しています。 つまり、val = yield i
と書くことはできますが、val = (yield i) + 12
のように、操作がある場合は括弧を使用する必要があります。)
値は、send(value)
メソッドを呼び出すことによってジェネレーターに送信されます。 このメソッドはジェネレーターのコードを再開し、yield
式は指定された値を返します。 通常のnext()
メソッドが呼び出されると、yield
はNone
を返します。
これは、1ずつインクリメントし、内部カウンターの値を変更できる単純なカウンターです。
そして、これがカウンターを変更する例です:
yield
はNone
を返すことが多いため、このケースを常に確認する必要があります。 send()
メソッドがジェネレーター関数を再開するために使用される唯一のメソッドであることが確実でない限り、式でその値を使用しないでください。
send()
に加えて、ジェネレーターには他に2つの新しいメソッドがあります。
throw(type, value=None, traceback=None)
は、ジェネレーター内で例外を発生させるために使用されます。 例外は、ジェネレータの実行が一時停止されるyield
式によって発生します。close()
は、ジェネレーター内でGeneratorExit
例外を発生させ、反復を終了します。 この例外を受け取ると、ジェネレーターのコードはGeneratorExit
またはStopIteration
のいずれかを発生させる必要があります。 例外をキャッチして他のことを行うことは違法であり、RuntimeError
をトリガーします。close()
は、ジェネレーターがガベージコレクションされるときに、Pythonのガベージコレクターによっても呼び出されます。GeneratorExit
が発生したときにクリーンアップコードを実行する必要がある場合は、GeneratorExit
をキャッチする代わりに、try: ... finally:
スイートを使用することをお勧めします。
これらの変更の累積的な効果は、情報の一方通行のプロデューサーからのジェネレーターをプロデューサーとコンシューマーの両方に変えることです。
ジェネレーターは、コルーチン、より一般化された形式のサブルーチンにもなります。 サブルーチンはあるポイントで入力され、別のポイントで終了します(関数の先頭、およびreturn
ステートメント)が、コルーチンは多くの異なるポイントで開始、終了、および再開できます( [X204X ]ステートメント)。
組み込み関数
イテレータでよく使用される組み込み関数について詳しく見ていきましょう。
Pythonの組み込み関数の2つ、 map()と filter()はやや時代遅れです。 それらはリスト内包の機能を複製しますが、イテレータの代わりに実際のリストを返します。
map(f, iterA, iterB, ...)
は、f(iterA[0], iterB[0]), f(iterA[1], iterB[1]), f(iterA[2], iterB[2]), ...
を含むリストを返します。
上に示したように、リスト内包表記でも同じ効果を得ることができます。 itertools.imap()関数は同じことを行いますが、無限のイテレーターを処理できます。 これについては、後で itertools モジュールのセクションで説明します。
filter(predicate, iter)
は、特定の条件を満たすすべてのシーケンス要素を含むリストを返します。同様に、リスト内包表記によって複製されます。 述語は、ある条件の真理値を返す関数です。 filter()で使用するには、述部は単一の値を取る必要があります。
これはリスト内包として書くこともできます:
filter()には、 itertools モジュールに対応する itertools.ifilter()もあります。これは、イテレーターを返すため、と同じように無限シーケンスを処理できます。 ] itertools.imap()できます。
reduce(func, iter, [initial_value])
は、 itertools モジュールに対応するものがありません。これは、すべての反復可能要素に対して累積的に操作を実行するため、無限反復可能に適用できないためです。 func
は、2つの要素を受け取り、1つの値を返す関数である必要があります。 reduce()は、イテレータによって返された最初の2つの要素AとBを受け取り、func(A, B)
を計算します。 次に、3番目の要素Cを要求し、func(func(A, B), C)
を計算し、この結果を返された4番目の要素と組み合わせて、反復可能要素がなくなるまで続行します。 iterableが値をまったく返さない場合、TypeError
例外が発生します。 初期値が指定されている場合は、それが開始点として使用され、func(initial_value, A)
が最初の計算になります。
operator.add()を reduce()と一緒に使用すると、iterableのすべての要素が合計されます。 このケースは非常に一般的であるため、 sum()と呼ばれる特別な組み込みがあります。
ただし、 reduce()の多くの用途では、明らかな for ループを記述する方が明確な場合があります。
enumerate(iter)
は、イテラブル内の要素をカウントオフし、カウントと各要素を含む2タプルを返します。
enumerate()は、リストをループして特定の条件が満たされたインデックスを記録するときによく使用されます。
sorted(iterable, [cmp=None], [key=None], [reverse=False])
は、イテラブルのすべての要素をリストに収集し、リストを並べ替えて、並べ替えられた結果を返します。 cmp
、key
、およびreverse
引数は、作成されたリストの.sort()
メソッドに渡されます。
(並べ替えの詳細については、Pythonwikiの https://wiki.python.org/moin/HowTo/Sorting にあるSortingmini-HOWTOを参照してください。)
any(iter)
およびall(iter)
ビルトインは、反復可能コンテンツの真理値を調べます。 any()は、反復可能要素のいずれかの要素がtrue値の場合、True
を返し、 all()は、すべての要素がtrueの場合、True
を返します。真の値:
小さな関数とラムダ式
関数型プログラムを作成する場合、述語として機能したり、何らかの方法で要素を組み合わせたりする小さな関数が必要になることがよくあります。
Pythonの組み込み関数または適切なモジュール関数がある場合は、新しい関数を定義する必要はまったくありません。
必要な関数が存在しない場合は、それを作成する必要があります。 小さな関数を作成する1つの方法は、lambda
ステートメントを使用することです。 lambda
は、いくつかのパラメーターとこれらのパラメーターを組み合わせた式を受け取り、式の値を返す小さな関数を作成します。
別の方法は、def
ステートメントを使用して、通常の方法で関数を定義することです。
どちらの選択肢が望ましいですか? それはスタイルの質問です。 私の通常のコースは、lambda
の使用を避けることです。
私が好む理由の1つは、lambda
が定義できる関数がかなり制限されていることです。 結果は単一の式として計算可能である必要があります。つまり、多方向のif... elif... else
比較またはtry... except
ステートメントを使用することはできません。 lambda
ステートメントで多くのことを行おうとすると、非常に複雑な式になり、読みにくくなります。 早く、次のコードは何をしているのですか?
あなたはそれを理解することができますが、何が起こっているのかを理解するために表現を解きほぐすには時間がかかります。 ネストされた短いdef
ステートメントを使用すると、状況が少し良くなります。
しかし、単にfor
ループを使用した場合は、何よりも最善です。
または、 sum()組み込みおよびジェネレータ式:
reduce()の多くの使用法は、for
ループとして記述された場合により明確になります。
Fredrik Lundhはかつて、lambda
の使用をリファクタリングするための次の一連のルールを提案しました。
- ラムダ関数を記述します。
- ラムダが何をするのかを説明するコメントを書いてください。
- コメントをしばらく調べて、コメントの本質を捉えた名前を考えてください。
- その名前を使用して、ラムダをdefステートメントに変換します。
- コメントを削除します。
私はこれらのルールが本当に好きですが、このラムダフリースタイルが優れているかどうかについては自由に意見が分かれます。
itertoolsモジュール
itertools モジュールには、一般的に使用される多数のイテレーターと、複数のイテレーターを組み合わせるための関数が含まれています。 このセクションでは、小さな例を示してモジュールの内容を紹介します。
モジュールの機能は、いくつかの大まかなクラスに分類されます。
- 既存のイテレータに基づいて新しいイテレータを作成する関数。
- イテレータの要素を関数の引数として扱うための関数。
- イテレータの出力の一部を選択するための関数。
- イテレータの出力をグループ化するための関数。
新しいイテレータの作成
itertools.count(n)
は、整数の無限ストリームを返し、毎回1ずつ増加します。 オプションで開始番号を指定できます。デフォルトは0です。
itertools.cycle(iter)
は、提供されたイテレータの内容のコピーを保存し、その要素を最初から最後まで返す新しいイテレータを返します。 新しいイテレータは、これらの要素を無限に繰り返します。
itertools.repeat(elem, [n])
は、指定された要素n
回を返します。n
が指定されていない場合は、要素を無限に返します。
itertools.chain(iterA, iterB, ...)
は、入力として任意の数のイテレータを受け取り、すべてのイテレータが使い果たされるまで、最初のイテレータのすべての要素を返し、次に2番目のイテレータのすべての要素を返します。
itertools.izip(iterA, iterB, ...)
は、各反復可能オブジェクトから1つの要素を取得し、それらをタプルで返します。
組み込みの zip()関数に似ていますが、メモリ内リストを作成せず、戻る前にすべての入力イテレータを使い果たします。 代わりに、タプルが作成され、要求された場合にのみ返されます。 (この動作の専門用語は遅延評価です。)
このイテレータは、すべて同じ長さのイテレータで使用することを目的としています。 イテラブルの長さが異なる場合、結果のストリームは最短のイテラブルと同じ長さになります。
ただし、要素がより長いイテレータから取得されて破棄される可能性があるため、これは避ける必要があります。 これは、破棄された要素をスキップするリスクがあるため、イテレータをこれ以上使用できないことを意味します。
itertools.islice(iter, [start], stop, [step])
は、イテレータのスライスであるストリームを返します。 単一のstop
引数を使用すると、最初のstop
要素が返されます。 開始インデックスを指定すると、stop-start
要素が取得され、step
の値を指定すると、それに応じて要素がスキップされます。 Pythonの文字列とリストのスライスとは異なり、start
、stop
、またはstep
に負の値を使用することはできません。
itertools.tee(iter, [n])
はイテレータを複製します。 n
の独立したイテレータを返します。これらはすべて、ソースイテレータの内容を返します。 n
の値を指定しない場合、デフォルトは2です。 イテレータを複製するには、ソースイテレータの内容の一部を保存する必要があるため、イテレータが大きく、新しいイテレータの1つが他のイテレータよりも多く消費される場合、これはかなりのメモリを消費する可能性があります。
要素の関数を呼び出す
iterableのコンテンツで他の関数を呼び出すために2つの関数が使用されます。
itertools.imap(f, iterA, iterB, ...)
は、f(iterA[0], iterB[0]), f(iterA[1], iterB[1]), f(iterA[2], iterB[2]), ...
を含むストリームを返します。
operator
モジュールには、Pythonの演算子に対応する一連の関数が含まれています。 例としては、operator.add(a, b)
(2つの値を追加)、operator.ne(a, b)
(a!=b
と同じ)、operator.attrgetter('id')
( [をフェッチする呼び出し可能オブジェクトを返す)]があります。 X133X]属性)。
itertools.starmap(func, iter)
は、iterableがタプルのストリームを返すことを想定し、これらのタプルを引数として使用してf()
を呼び出します。
要素の選択
関数の別のグループは、述語に基づいてイテレーターの要素のサブセットを選択します。
itertools.ifilter(predicate, iter)
は、述語がtrueを返すすべての要素を返します。
itertools.ifilterfalse(predicate, iter)
は反対で、述語がfalseを返すすべての要素を返します。
itertools.takewhile(predicate, iter)
は、述語がtrueを返す限り、要素を返します。 述語がfalseを返すと、イテレータは結果の終了を通知します。
itertools.dropwhile(predicate, iter)
は、述語がtrueを返す間、要素を破棄してから、反復可能オブジェクトの残りの結果を返します。
グループ化要素
最後に説明する関数itertools.groupby(iter, key_func=None)
は、最も複雑です。 key_func(elem)
は、iterableによって返される各要素のキー値を計算できる関数です。 キー関数を指定しない場合、キーは単に各要素自体です。
groupby()
は、同じキー値を持つ基になるイテレータからすべての連続する要素を収集し、キー値を含む2タプルのストリームと、そのキーを持つ要素のイテレータを返します。
groupby()
は、基になるイテラブルのコンテンツがすでにキーに基づいてソートされていることを前提としています。 返されたイテレータも基になるイテレータを使用するため、イテレータ2とそれに対応するキーを要求する前に、イテレータ1の結果を消費する必要があることに注意してください。
functoolsモジュール
Python2.5の functools モジュールには、いくつかの高階関数が含まれています。 高階関数は、1つ以上の関数を入力として受け取り、新しい関数を返します。 このモジュールで最も便利なツールは、 functools.partial()関数です。
関数型で記述されたプログラムの場合、いくつかのパラメーターが入力された既存の関数のバリアントを作成したい場合があります。 Python関数f(a, b, c)
について考えてみましょう。 f(1, b, c)
と同等の新しい関数g(b, c)
を作成することをお勧めします。 f()
のパラメータの1つに値を入力しています。 これを「部分機能適用」といいます。
partial
のコンストラクターは、引数(function, arg1, arg2, ... kwarg1=value1, kwarg2=value2)
を取ります。 結果のオブジェクトは呼び出し可能であるため、それを呼び出すだけで、引数を入力してfunction
を呼び出すことができます。
小さいながらも現実的な例を次に示します。
オペレーターモジュール
operator モジュールについては前述しました。 Pythonの演算子に対応する一連の関数が含まれています。 これらの関数は、単一の操作を実行する簡単な関数を作成する手間を省くため、関数型コードで役立つことがよくあります。
このモジュールの機能の一部は次のとおりです。
- 数学演算:
add()
、sub()
、mul()
、div()
、floordiv()
、abs()
、… - 論理演算:
not_()
、truth()
。 - ビット演算:
and_()
、or_()
、invert()
。 - 比較:
eq()
、ne()
、lt()
、le()
、gt()
、およびge()
。 - オブジェクトID:
is_()
、is_not()
。
完全なリストについては、オペレータモジュールのドキュメントを参照してください。
改訂履歴と謝辞
著者は、この記事のさまざまなドラフトについて提案、修正、支援を提供してくれた次の人々に感謝します:Ian Bicking、Nick Coghlan、Nick Efford、Raymond Hettinger、Jim Jewett、Mike Krell、Leandro Lameiro、Jussi Salmela、Collin Winter、ブレイクウィントン。
バージョン0.1:2006年6月30日投稿。
バージョン0.11:2006年7月1日投稿。 タイプミスの修正。
バージョン0.2:2006年7月10日投稿。 genexpセクションとlistcompセクションを1つにマージしました。 タイプミスの修正。
バージョン0.21:家庭教師のメーリングリストで提案されている参照をさらに追加しました。
バージョン0.30:CollinWinterによって作成されたfunctional
モジュールにセクションを追加します。 オペレータモジュールに短いセクションを追加します。 他のいくつかの編集。
参考文献
全般的
コンピュータープログラムの構造と解釈、ハロルド・アベルソンとジェラルド・ジェイ・サスマン、ジュリー・サスマン。 全文は https://mitpress.mit.edu/sicp/ にあります。 このコンピュータサイエンスの古典的な教科書では、第2章と第3章で、プログラム内のデータフローを整理するためのシーケンスとストリームの使用について説明しています。 この本では例としてSchemeを使用していますが、これらの章で説明されている設計アプローチの多くは、機能スタイルのPythonコードに適用できます。
http://www.defmacro.org/ramblings/fp.html :Javaの例を使用し、長い歴史的な紹介がある関数型プログラミングの一般的な紹介。
https://en.wikipedia.org/wiki/Functional_programming :関数型プログラミングを説明する一般的なウィキペディアのエントリ。
https://en.wikipedia.org/wiki/Coroutine :コルーチンのエントリ。
https://en.wikipedia.org/wiki/Currying :カリー化の概念のエントリ。
Python固有
http://gnosis.cx/TPiP/ :DavidMertzの著書 Pythonでのテキスト処理の最初の章では、「高階の利用」というタイトルのセクションで、テキスト処理の関数型プログラミングについて説明しています。テキスト処理の機能」。
Mertzは、IBMのDeveloperWorksサイトの関数型プログラミングに関する3部構成の一連の記事も執筆しました。 見る