関数型プログラミングHOWTO—Pythonドキュメント

提供:Dev Guides
< PythonPython/docs/3.9/howto/functional
移動先:案内検索

関数型プログラミングHOWTO

著者
    1. Kuchling

リリース

0.32

このドキュメントでは、プログラムを機能的なスタイルで実装するのに適した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=3c = a + bなどの代入ステートメントさえありませんが、画面への印刷や書き込みなど、すべての副作用を回避することは困難です。ディスクファイル。 もう1つの例は、 print()または time.sleep()関数の呼び出しです。どちらも、有用な値を返しません。 どちらも、画面にテキストを送信したり、実行を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 と呼ばれます。

反復インターフェースを手動で試すことができます。

>>> L = [1, 2, 3]
>>> it = iter(L)
>>> it  
<...iterator object at ...>
>>> it.__next__()  # same as next(it)
1
>>> next(it)
2
>>> next(it)
3
>>> next(it)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>>

Pythonは、いくつかの異なるコンテキストで反復可能なオブジェクトを想定しています。最も重要なのは、 for ステートメントです。 ステートメントfor X in Yでは、Yはイテレーター、または iter()がイテレーターを作成できるオブジェクトである必要があります。 これらの2つのステートメントは同等です。

for i in iter(obj):
    print(i)

for i in obj:
    print(i)

イテレータは、 list()または tuple()コンストラクタ関数を使用してリストまたはタプルとして実体化できます。

>>> L = [1, 2, 3]
>>> iterator = iter(L)
>>> t = tuple(iterator)
>>> t
(1, 2, 3)

シーケンスアンパックはイテレータもサポートします。イテレータがN個の要素を返すことがわかっている場合は、それらをNタプルにアンパックできます。

>>> L = [1, 2, 3]
>>> iterator = iter(L)
>>> a, b, c = iterator
>>> a, b, c
(1, 2, 3)

max()min()などの組み込み関数は、単一のイテレーター引数を取ることができ、最大または最小の要素を返します。 "in"および"not in"演算子は、イテレーターもサポートします。X in iteratorは、イテレーターによって返されるストリームでXが見つかった場合にtrueになります。 イテレータが無限大の場合、明らかな問題が発生します。 max()min()は返されません。要素Xがストリームに表示されない場合、"in"および"not in"演算子が勝ちます。どちらも返しません。

イテレータでのみ先に進むことができることに注意してください。 前の要素を取得したり、イテレータをリセットしたり、そのコピーを作成したりする方法はありません。 イテレータオブジェクトはオプションでこれらの追加機能を提供できますが、イテレータプロトコルは __ next __()メソッドのみを指定します。 したがって、関数はイテレータのすべての出力を消費する可能性があり、同じストリームで別のことを行う必要がある場合は、新しいイテレータを作成する必要があります。

イテレータをサポートするデータ型

リストとタプルがイテレータをどのようにサポートするかについては、すでに見てきました。 実際、文字列などのPythonシーケンスタイプは、イテレータの作成を自動的にサポートします。

ディクショナリで iter()を呼び出すと、ディクショナリのキーをループするイテレータが返されます。

>>> m = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
...      'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
>>> for key in m:
...     print(key, m[key])
Jan 1
Feb 2
Mar 3
Apr 4
May 5
Jun 6
Jul 7
Aug 8
Sep 9
Oct 10
Nov 11
Dec 12

Python 3.7以降、辞書の反復順序は挿入順序と同じであることが保証されていることに注意してください。 以前のバージョンでは、動作は指定されておらず、実装間で異なる可能性がありました。

iter()をディクショナリに適用すると、常にキーがループしますが、ディクショナリには他のイテレータを返すメソッドがあります。 値またはキーと値のペアを反復処理する場合は、 values()または items()メソッドを明示的に呼び出して、適切な反復子を取得できます。

dict()コンストラクターは、(key, value)タプルの有限ストリームを返すイテレーターを受け入れることができます。

>>> L = [('Italy', 'Rome'), ('France', 'Paris'), ('US', 'Washington DC')]
>>> dict(iter(L))
{'Italy': 'Rome', 'France': 'Paris', 'US': 'Washington DC'}

ファイルは、ファイルに行がなくなるまで readline()メソッドを呼び出すことにより、反復もサポートします。 これは、次のようにファイルの各行を読み取ることができることを意味します。

for line in file:
    # do something for each line
    ...

セットは、イテラブルからコンテンツを取得して、セットの要素を反復処理できます。

S = {2, 3, 5, 7, 11, 13}
for i in S:
    print(i)

ジェネレータ式とリスト内包表記

イテレータの出力に対する2つの一般的な操作は、1)すべての要素に対して何らかの操作を実行すること、2)ある条件を満たす要素のサブセットを選択することです。 たとえば、文字列のリストが与えられた場合、各行から末尾の空白を削除したり、特定のサブ文字列を含むすべての文字列を抽出したりできます。

リスト内包表記とジェネレータ式(短縮形:「listcomps」と「genexps」)は、関数型プログラミング言語Haskell( https://www.haskell.org/)から借用した、このような操作の簡潔な表記法です。 。 次のコードを使用して、文字列のストリームからすべての空白を取り除くことができます。

line_list = ['  line 1\n', 'line 2  \n', ...]

# Generator expression -- returns iterator
stripped_iter = (line.strip() for line in line_list)

# List comprehension -- returns list
stripped_list = [line.strip() for line in line_list]

"if"条件を追加すると、特定の要素のみを選択できます。

stripped_list = [line.strip() for line in line_list
                 if line != ""]

リストを理解すると、Pythonリストが返されます。 stripped_listは、イテレータではなく、結果の行を含むリストです。 ジェネレータ式は、必要に応じて値を計算するイテレータを返します。すべての値を一度に実体化する必要はありません。 これは、無限のストリームまたは非常に大量のデータを返すイテレータを使用している場合、リスト内包表記は役に立たないことを意味します。 このような状況では、ジェネレータ式が適しています。

ジェネレータ式は括弧(“()”)で囲まれ、リスト内包表記は角括弧(“ []”)で囲まれます。 ジェネレータ式の形式は次のとおりです。

( expression for expr in sequence1
             if condition1
             for expr2 in sequence2
             if condition2
             for expr3 in sequence3 ...
             if condition3
             for exprN in sequenceN
             if conditionN )

繰り返しますが、リスト内包表記では、外側の角かっこのみが異なります(かっこではなく角かっこ)。

生成される出力の要素は、expressionの連続する値になります。 if句はすべてオプションです。 存在する場合、expressionは、conditionが真の場合にのみ評価され、結果に追加されます。

ジェネレータ式は常に括弧内に記述する必要がありますが、関数呼び出しを示す括弧もカウントされます。 関数にすぐに渡されるイテレータを作成する場合は、次のように記述できます。

obj_total = sum(obj.count for obj in list_all_objects())

for...in句には、繰り返されるシーケンスが含まれています。 シーケンスは、並列ではなく左から右に繰り返されるため、同じ長さである必要はありません。 sequence1の各要素について、sequence2は最初からループオーバーされます。 次に、sequence3は、sequence1およびsequence2から得られた要素のペアごとにループされます。

別の言い方をすれば、リスト内包表記またはジェネレーター式は、次のPythonコードと同等です。

for expr1 in sequence1:
    if not (condition1):
        continue   # Skip this element
    for expr2 in sequence2:
        if not (condition2):
            continue   # Skip this element
        ...
        for exprN in sequenceN:
            if not (conditionN):
                continue   # Skip this element

            # Output the value of
            # the expression.

これは、複数の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番目のリスト内包表記は正しいです。

# Syntax error
[x, y for x in seq1 for y in seq2]
# Correct
[(x, y) for x in seq1 for y in seq2]

発電機

ジェネレーターは、イテレーターを作成するタスクを簡素化する特別なクラスの関数です。 通常の関数は値を計算して返しますが、ジェネレーターは値のストリームを返すイテレーターを返します。

あなたは間違いなく、PythonまたはCで通常の関数呼び出しがどのように機能するかを知っています。 関数を呼び出すと、ローカル変数が作成されるプライベート名前空間が取得されます。 関数がreturnステートメントに到達すると、ローカル変数が破棄され、値が呼び出し元に返されます。 後で同じ関数を呼び出すと、新しいプライベート名前空間とローカル変数の新しいセットが作成されます。 しかし、関数の終了時にローカル変数が破棄されなかった場合はどうなるでしょうか。 後で中断したところから機能を再開できるとしたらどうでしょうか。 これはジェネレーターが提供するものです。 それらは再開可能な機能と考えることができます。

ジェネレーター関数の最も簡単な例を次に示します。

>>> def generate_ints(N):
...    for i in range(N):
...        yield i

yield キーワードを含む関数はすべてジェネレーター関数です。 これは、結果として関数を特別にコンパイルするPythonの bytecode コンパイラによって検出されます。

ジェネレーター関数を呼び出すと、単一の値は返されません。 代わりに、イテレータプロトコルをサポートするジェネレータオブジェクトを返します。 yield式を実行すると、ジェネレータはreturnステートメントと同様に、iの値を出力します。 yieldステートメントとreturnステートメントの大きな違いは、yieldに達すると、ジェネレーターの実行状態が一時停止され、ローカル変数が保持されることです。 ジェネレーターの __ next __()メソッドを次に呼び出すと、関数は実行を再開します。

generate_ints()ジェネレーターの使用例は次のとおりです。

>>> gen = generate_ints(3)
>>> gen  
<generator object generate_ints at ...>
>>> next(gen)
0
>>> next(gen)
1
>>> next(gen)
2
>>> next(gen)
Traceback (most recent call last):
  File "stdin", line 1, in <module>
  File "stdin", line 2, in generate_ints
StopIteration

for i in generate_ints(5)またはa, b, c = generate_ints(3)と同じように書くことができます。

ジェネレーター関数内で、return valueにより、StopIteration(value)__ next __()メソッドから発生します。 これが発生するか、関数の最下部に達すると、値の行列が終了し、ジェネレーターはそれ以上の値を生成できなくなります。

独自のクラスを作成し、ジェネレーターのすべてのローカル変数をインスタンス変数として格納することで、ジェネレーターの効果を手動で実現できます。 たとえば、整数のリストを返すには、self.countを0に設定し、 __ next __()メソッドにself.countをインクリメントして返します。 ただし、適度に複雑なジェネレーターの場合、対応するクラスを作成するのは非常に面倒です。

Pythonのライブラリ:source: `Lib / test / test_generators.py` に含まれているテストスイートには、さらに興味深い例がいくつか含まれています。 これは、ジェネレーターを再帰的に使用してツリーのインオーダートラバーサルを実装するジェネレーターの1つです。

# A recursive generator that generates Tree leaves in in-order.
def inorder(t):
    if t:
        for x in inorder(t.left):
            yield x

        yield t.label

        for x in inorder(t.right):
            yield x

test_generators.pyの他の2つの例では、N-Queensの問題(NxNのチェス盤にNの女王を配置して、女王が他の女王を脅かさないようにする)とKnight's Tour(騎士を正方形を2回訪問せずにNxNチェス盤)。

ジェネレータに値を渡す

Python 2.4以前では、ジェネレーターは出力のみを生成していました。 イテレータを作成するためにジェネレータのコードが呼び出されると、実行が再開されたときに関数に新しい情報を渡す方法はありませんでした。 ジェネレーターにグローバル変数を参照させるか、呼び出し元が変更する可変オブジェクトを渡すことで、この機能を一緒にハックすることができますが、これらのアプローチは面倒です。

Python 2.5には、ジェネレーターに値を渡す簡単な方法があります。 yield は式になり、変数に割り当てるか、その他の方法で操作できる値を返します。

val = (yield i)

上記の例のように、戻り値を使用して何かを行う場合は、常にyield式を括弧で囲むことをお勧めします。 括弧は必ずしも必要ではありませんが、必要なときに覚えておくよりも、常に追加する方が簡単です。

PEP 342 は、yield-式は、右側の最上位の式で発生する場合を除いて、常に括弧で囲む必要があるという正確な規則を説明しています-課題の手側。 つまり、val = yield iと書くことはできますが、val = (yield i) + 12のように、操作がある場合は括弧を使用する必要があります。)

値は、 send(value)メソッドを呼び出すことによってジェネレーターに送信されます。 このメソッドはジェネレーターのコードを再開し、yield式は指定された値を返します。 通常の __ next __()メソッドが呼び出されると、yieldNoneを返します。

これは、1ずつインクリメントし、内部カウンターの値を変更できる単純なカウンターです。

def counter(maximum):
    i = 0
    while i < maximum:
        val = (yield i)
        # If value provided, change counter
        if val is not None:
            i = val
        else:
            i += 1

そして、これがカウンターを変更する例です:

>>> it = counter(10)  
>>> next(it)  
0
>>> next(it)  
1
>>> it.send(8)  
8
>>> next(it)  
9
>>> next(it)  
Traceback (most recent call last):
  File "t.py", line 15, in <module>
    it.next()
StopIteration

yieldNoneを返すことが多いため、このケースを常に確認する必要があります。 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]), ...

>>> def upper(s):
...     return s.upper()
>>> list(map(upper, ['sentence', 'fragment']))
['SENTENCE', 'FRAGMENT']
>>> [upper(s) for s in ['sentence', 'fragment']]
['SENTENCE', 'FRAGMENT']

もちろん、リスト内包表記でも同じ効果を得ることができます。

filter(predicate、iter)は、特定の条件を満たすすべてのシーケンス要素に対してイテレーターを返し、同様にリスト内包表記によって複製されます。 述語は、ある条件の真理値を返す関数です。 filter()で使用するには、述部は単一の値を取る必要があります。

>>> def is_even(x):
...     return (x % 2) == 0
>>> list(filter(is_even, range(10)))
[0, 2, 4, 6, 8]

これはリスト内包として書くこともできます:

>>> list(x for x in range(10) if is_even(x))
[0, 2, 4, 6, 8]

enumerate(iter、start = 0)は、カウント( start から)と各要素を含む反復可能な戻り2タプルの要素をカウントオフします。

>>> for item in enumerate(['subject', 'verb', 'object']):
...     print(item)
(0, 'subject')
(1, 'verb')
(2, 'object')

enumerate()は、リストをループして特定の条件が満たされたインデックスを記録するときによく使用されます。

f = open('data.txt', 'r')
for i, line in enumerate(f):
    if line.strip() == '':
        print('Blank line at line #%i' % i)

sorted(iterable、key = None、reverse = False)は、iterableのすべての要素をリストに収集し、リストをソートして、ソートされた結果を返します。 key および reverse 引数は、作成されたリストの sort()メソッドに渡されます。

>>> import random
>>> # Generate 8 random numbers between [0, 10000)
>>> rand_list = random.sample(range(10000), 8)
>>> rand_list  
[769, 7953, 9828, 6431, 8442, 9878, 6213, 2207]
>>> sorted(rand_list)  
[769, 2207, 6213, 6431, 7953, 8442, 9828, 9878]
>>> sorted(rand_list, reverse=True)  
[9878, 9828, 8442, 7953, 6431, 6213, 2207, 769]

(並べ替えの詳細については、並べ替え方法を参照してください。)

any(iter)および all(iter)ビルトインは、iterableのコンテンツの真理値を調べます。 any()は、反復可能要素のいずれかの要素がtrue値の場合、Trueを返し、 all()は、すべての要素がtrueの場合、Trueを返します。真の値:

>>> any([0, 1, 0])
True
>>> any([0, 0, 0])
False
>>> any([1, 1, 1])
True
>>> all([0, 1, 0])
False
>>> all([0, 0, 0])
False
>>> all([1, 1, 1])
True

zip(iterA、iterB、...)は、各iterableから1つの要素を取得し、それらをタプルで返します。

zip(['a', 'b', 'c'], (1, 2, 3)) =>
  ('a', 1), ('b', 2), ('c', 3)

インメモリリストを作成せず、戻る前にすべての入力イテレータを使い果たします。 代わりに、タプルが作成され、要求された場合にのみ返されます。 (この動作の専門用語は遅延評価です。)

このイテレータは、すべて同じ長さのイテレータで使用することを目的としています。 イテラブルの長さが異なる場合、結果のストリームは最短のイテラブルと同じ長さになります。

zip(['a', 'b'], (1, 2, 3)) =>
  ('a', 1), ('b', 2)

ただし、要素がより長いイテレータから取得されて破棄される可能性があるため、これは避ける必要があります。 これは、破棄された要素をスキップするリスクがあるため、イテレータをこれ以上使用できないことを意味します。


itertoolsモジュール

itertools モジュールには、一般的に使用される多数のイテレーターと、複数のイテレーターを組み合わせるための関数が含まれています。 このセクションでは、小さな例を示してモジュールの内容を紹介します。

モジュールの機能は、いくつかの大まかなクラスに分類されます。

  • 既存のイテレータに基づいて新しいイテレータを作成する関数。
  • イテレータの要素を関数の引数として扱うための関数。
  • イテレータの出力の一部を選択するための関数。
  • イテレータの出力をグループ化するための関数。

新しいイテレータの作成

itertools.count(start、step)は、等間隔の値の無限ストリームを返します。 オプションで、開始番号(デフォルトは0)と番号間の間隔(デフォルトは1)を指定できます。

itertools.count() =>
  0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ...
itertools.count(10) =>
  10, 11, 12, 13, 14, 15, 16, 17, 18, 19, ...
itertools.count(10, 5) =>
  10, 15, 20, 25, 30, 35, 40, 45, 50, 55, ...

itertools.cycle(iter)は、提供されたイテレータの内容のコピーを保存し、その要素を最初から最後まで返す新しいイテレータを返します。 新しいイテレータは、これらの要素を無限に繰り返します。

itertools.cycle([1, 2, 3, 4, 5]) =>
  1, 2, 3, 4, 5, 1, 2, 3, 4, 5, ...

itertools.repeat(elem、[n])は、指定された要素 n 回を返すか、 n が指定されていない場合は要素を無限に返します。

itertools.repeat('abc') =>
  abc, abc, abc, abc, abc, abc, abc, abc, abc, abc, ...
itertools.repeat('abc', 5) =>
  abc, abc, abc, abc, abc

itertools.chain(iterA、iterB、...)は、入力として任意の数のイテレータを受け取り、最初のイテレータのすべての要素を返し、次に2番目のイテレータのすべての要素を返します。すべてのイテレータが使い果たされました。

itertools.chain(['a', 'b', 'c'], (1, 2, 3)) =>
  a, b, c, 1, 2, 3

itertools.islice(iter、[start]、stop、[step])は、イテレーターのスライスであるストリームを返します。 単一の stop 引数を使用すると、最初の stop 要素が返されます。 開始インデックスを指定すると、 stop-start 要素が取得され、 step の値を指定すると、それに応じて要素がスキップされます。 Pythonの文字列とリストのスライスとは異なり、 startstop 、または step に負の値を使用することはできません。

itertools.islice(range(10), 8) =>
  0, 1, 2, 3, 4, 5, 6, 7
itertools.islice(range(10), 2, 8) =>
  2, 3, 4, 5, 6, 7
itertools.islice(range(10), 2, 8, 2) =>
  2, 4, 6

itertools.tee(iter、[n])はイテレーターを複製します。 n の独立したイテレータを返します。これらはすべて、ソースイテレータの内容を返します。 n の値を指定しない場合、デフォルトは2です。 イテレータを複製するには、ソースイテレータの内容の一部を保存する必要があるため、イテレータが大きく、新しいイテレータの1つが他のイテレータよりも多く消費される場合、これはかなりのメモリを消費する可能性があります。

itertools.tee( itertools.count() ) =>
   iterA, iterB

where iterA ->
   0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ...

and   iterB ->
   0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ...

要素の関数を呼び出す

operator モジュールには、Pythonの演算子に対応する一連の関数が含まれています。 例としては、 operator.add(a、b)(2つの値を追加)、 operator.ne(a、b)a != bと同じ)、[ X117X] operator.attrgetter(' id ')(.id属性をフェッチする呼び出し可能オブジェクトを返します)。

itertools.starmap(func、iter)は、iterableがタプルのストリームを返すと想定し、これらのタプルを引数として使用して func を呼び出します。

itertools.starmap(os.path.join,
                  [('/bin', 'python'), ('/usr', 'bin', 'java'),
                   ('/usr', 'bin', 'perl'), ('/usr', 'bin', 'ruby')])
=>
  /bin/python, /usr/bin/java, /usr/bin/perl, /usr/bin/ruby

要素の選択

関数の別のグループは、述語に基づいてイテレーターの要素のサブセットを選択します。

itertools.filterfalse(predicate、iter)filter()の反対であり、述語がfalseを返すすべての要素を返します。

itertools.filterfalse(is_even, itertools.count()) =>
  1, 3, 5, 7, 9, 11, 13, 15, ...

itertools.takewhile(predicate、iter)は、述語がtrueを返す限り、要素を返します。 述語がfalseを返すと、イテレータは結果の終了を通知します。

def less_than_10(x):
    return x < 10

itertools.takewhile(less_than_10, itertools.count()) =>
  0, 1, 2, 3, 4, 5, 6, 7, 8, 9

itertools.takewhile(is_even, itertools.count()) =>
  0

itertools.dropwhile(predicate、iter)は、述語がtrueを返す間、要素を破棄し、その後、iterableの残りの結果を返します。

itertools.dropwhile(less_than_10, itertools.count()) =>
  10, 11, 12, 13, 14, 15, 16, 17, 18, 19, ...

itertools.dropwhile(is_even, itertools.count()) =>
  1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...

itertools.compress(data、selectors)は2つのイテレーターを取り、セレクターの対応する要素がtrueであるデータの要素のみを返し、いずれか1つが停止するたびに停止します使い果たされている:

itertools.compress([1, 2, 3, 4, 5], [True, True, False, False, True]) =>
   1, 2, 5

組み合わせ関数

itertools.combinations(iterable、r)は、 iterable に含まれる要素のすべての可能な r タプルの組み合わせを提供するイテレーターを返します。

itertools.combinations([1, 2, 3, 4, 5], 2) =>
  (1, 2), (1, 3), (1, 4), (1, 5),
  (2, 3), (2, 4), (2, 5),
  (3, 4), (3, 5),
  (4, 5)

itertools.combinations([1, 2, 3, 4, 5], 3) =>
  (1, 2, 3), (1, 2, 4), (1, 2, 5), (1, 3, 4), (1, 3, 5), (1, 4, 5),
  (2, 3, 4), (2, 3, 5), (2, 4, 5),
  (3, 4, 5)

各タプル内の要素は、 iterable が返したのと同じ順序のままです。 たとえば、上記の例では、番号1は常に2、3、4、または5の前にあります。 同様の関数 itertools.permutations(iterable、r = None)は、順序に対するこの制約を削除し、長さ r の可能なすべての配置を返します。

itertools.permutations([1, 2, 3, 4, 5], 2) =>
  (1, 2), (1, 3), (1, 4), (1, 5),
  (2, 1), (2, 3), (2, 4), (2, 5),
  (3, 1), (3, 2), (3, 4), (3, 5),
  (4, 1), (4, 2), (4, 3), (4, 5),
  (5, 1), (5, 2), (5, 3), (5, 4)

itertools.permutations([1, 2, 3, 4, 5]) =>
  (1, 2, 3, 4, 5), (1, 2, 3, 5, 4), (1, 2, 4, 3, 5),
  ...
  (5, 4, 3, 2, 1)

r の値を指定しない場合、イテラブルの長さが使用されます。つまり、すべての要素が並べ替えられます。

これらの関数は、位置ごとに可能なすべての組み合わせを生成し、 iterable の内容が一意である必要はないことに注意してください。

itertools.permutations('aba', 3) =>
  ('a', 'b', 'a'), ('a', 'a', 'b'), ('b', 'a', 'a'),
  ('b', 'a', 'a'), ('a', 'a', 'b'), ('a', 'b', 'a')

同じタプル('a', 'a', 'b')が2回発生しますが、2つの「a」文字列は異なる位置から来ています。

itertools.combinations_with_replacement(iterable、r)関数は、異なる制約を緩和します。要素は単一のタプル内で繰り返すことができます。 概念的には、各タプルの最初の位置に要素が選択され、2番目の要素が選択される前に置き換えられます。

itertools.combinations_with_replacement([1, 2, 3, 4, 5], 2) =>
  (1, 1), (1, 2), (1, 3), (1, 4), (1, 5),
  (2, 2), (2, 3), (2, 4), (2, 5),
  (3, 3), (3, 4), (3, 5),
  (4, 4), (4, 5),
  (5, 5)

グループ化要素

最後に説明する関数 itertools.groupby(iter、key_func = None)は、最も複雑です。 key_func(elem)は、iterableによって返される各要素のキー値を計算できる関数です。 キー関数を指定しない場合、キーは単に各要素自体です。

groupby()は、基になる反復可能要素から同じキー値を持つすべての連続する要素を収集し、キー値を含む2タプルのストリームと、そのキーを持つ要素のイテレーターを返します。

city_list = [('Decatur', 'AL'), ('Huntsville', 'AL'), ('Selma', 'AL'),
             ('Anchorage', 'AK'), ('Nome', 'AK'),
             ('Flagstaff', 'AZ'), ('Phoenix', 'AZ'), ('Tucson', 'AZ'),
             ...
            ]

def get_state(city_state):
    return city_state[1]

itertools.groupby(city_list, get_state) =>
  ('AL', iterator-1),
  ('AK', iterator-2),
  ('AZ', iterator-3), ...

where
iterator-1 =>
  ('Decatur', 'AL'), ('Huntsville', 'AL'), ('Selma', 'AL')
iterator-2 =>
  ('Anchorage', 'AK'), ('Nome', 'AK')
iterator-3 =>
  ('Flagstaff', 'AZ'), ('Phoenix', 'AZ'), ('Tucson', 'AZ')

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を呼び出すために呼び出すことができます。

小さいながらも現実的な例を次に示します。

import functools

def log(message, subsystem):
    """Write the contents of 'message' to the specified subsystem."""
    print('%s: %s' % (subsystem, message))
    ...

server_log = functools.partial(log, subsystem='server')
server_log('Unable to open socket')

functools.reduce(func、iter、[initial_value])は、すべてのiterableの要素に対して累積的に操作を実行するため、無限のiterableに適用することはできません。 func は、2つの要素を受け取り、1つの値を返す関数である必要があります。 functools.reduce()は、イテレーターによって返された最初の2つの要素AとBを受け取り、func(A, B)を計算します。 次に、3番目の要素Cを要求し、func(func(A, B), C)を計算し、この結果を返された4番目の要素と組み合わせて、反復可能要素がなくなるまで続行します。 iterableが値をまったく返さない場合、 TypeError 例外が発生します。 初期値が指定されている場合は、それが開始点として使用され、func(initial_value, A)が最初の計算になります。

>>> import operator, functools
>>> functools.reduce(operator.concat, ['A', 'BB', 'C'])
'ABBC'
>>> functools.reduce(operator.concat, [])
Traceback (most recent call last):
  ...
TypeError: reduce() of empty sequence with no initial value
>>> functools.reduce(operator.mul, [1, 2, 3], 1)
6
>>> functools.reduce(operator.mul, [], 1)
1

operator.add()functools.reduce()と一緒に使用すると、iterableのすべての要素が合計されます。 このケースは非常に一般的であるため、 sum()と呼ばれる特別な組み込みがあります。

>>> import functools, operator
>>> functools.reduce(operator.add, [1, 2, 3, 4], 0)
10
>>> sum([1, 2, 3, 4])
10
>>> sum([])
0

ただし、 functools.reduce()の多くの用途では、明らかな for ループを記述する方が明確な場合があります。

import functools
# Instead of:
product = functools.reduce(operator.mul, [1, 2, 3], 1)

# You can write:
product = 1
for i in [1, 2, 3]:
    product *= i

関連する関数は itertools.accumulate(iterable、func = operator.add)です。 同じ計算を実行しますが、accumulate()は、最終結果のみを返す代わりに、各部分結果も生成するイテレーターを返します。

itertools.accumulate([1, 2, 3, 4, 5]) =>
  1, 3, 6, 10, 15

itertools.accumulate([1, 2, 3, 4, 5], operator.mul) =>
  1, 2, 6, 24, 120

オペレーターモジュール

operator モジュールについては前述しました。 Pythonの演算子に対応する一連の関数が含まれています。 これらの関数は、単一の操作を実行する簡単な関数を作成する手間を省くため、関数型コードで役立つことがよくあります。

このモジュールの機能の一部は次のとおりです。

  • 数学演算:add()sub()mul()floordiv()abs()、…
  • 論理演算:not_()truth()
  • ビット演算:and_()or_()invert()
  • 比較:eq()ne()lt()le()gt()、およびge()
  • オブジェクトID:is_()is_not()

完全なリストについては、オペレータモジュールのドキュメントを参照してください。


小さな関数とラムダ式

関数型プログラムを作成する場合、述語として機能したり、何らかの方法で要素を組み合わせたりする小さな関数が必要になることがよくあります。

Pythonの組み込み関数または適切なモジュール関数がある場合は、新しい関数を定義する必要はまったくありません。

stripped_lines = [line.strip() for line in lines]
existing_files = filter(os.path.exists, file_list)

必要な関数が存在しない場合は、それを作成する必要があります。 小さな関数を作成する1つの方法は、 lambda 式を使用することです。 lambdaは、いくつかのパラメーターとこれらのパラメーターを組み合わせた式を受け取り、式の値を返す無名関数を作成します。

adder = lambda x, y: x+y

print_assign = lambda name, value: name + '=' + str(value)

別の方法は、defステートメントを使用して、通常の方法で関数を定義することです。

def adder(x, y):
    return x + y

def print_assign(name, value):
    return name + '=' + str(value)

どちらの選択肢が望ましいですか? それはスタイルの質問です。 私の通常のコースは、lambdaの使用を避けることです。

私が好む理由の1つは、lambdaが定義できる関数がかなり制限されていることです。 結果は単一の式として計算可能である必要があります。つまり、多方向のif... elif... else比較またはtry... exceptステートメントを使用することはできません。 lambdaステートメントでやりすぎると、読みにくい過度に複雑な式になってしまいます。 早く、次のコードは何をしているのですか?

import functools
total = functools.reduce(lambda a, b: (0, a[1] + b[1]), items)[1]

あなたはそれを理解することができますが、何が起こっているのかを理解するために表現を解きほぐすには時間がかかります。 ネストされた短いdefステートメントを使用すると、状況が少し良くなります。

import functools
def combine(a, b):
    return 0, a[1] + b[1]

total = functools.reduce(combine, items)[1]

しかし、単にforループを使用した場合は、何よりも最善です。

total = 0
for a, b in items:
    total += b

または、 sum()組み込みおよびジェネレータ式:

total = sum(b for a, b in items)

functools.reduce()の多くの使用法は、forループとして記述された場合により明確になります。

Fredrik Lundhはかつて、lambdaの使用をリファクタリングするための次の一連のルールを提案しました。

  1. ラムダ関数を記述します。
  2. ラムダが何をするのかを説明するコメントを書いてください。
  3. コメントをしばらく調べて、コメントの本質を捉えた名前を考えてください。
  4. その名前を使用して、ラムダをdefステートメントに変換します。
  5. コメントを削除します。

私はこれらのルールが本当に好きですが、このラムダフリースタイルが優れているかどうかについては自由に意見が分かれます。


改訂履歴と謝辞

著者は、この記事のさまざまなドラフトについて提案、修正、支援を提供してくれた次の人々に感謝します: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部構成の一連の記事も執筆しました。 パート1パート2 、およびパート3 を参照してください。


Pythonドキュメント

itertools モジュールのドキュメント。

functools モジュールのドキュメント。

operator モジュールのドキュメント。

PEP 289 :「ジェネレータ式」

PEP 342 :「拡張ジェネレーターを介したコルーチン」では、Python2.5の新しいジェネレーター機能について説明しています。