関数型Pythonプログラミングの概要

関数型プログラミングについて話すと、多くの場合、多くの「関数型」特性が与えられ始めます。 不変データ、一流の関数、および末尾再帰の最適化。 これらは、機能的なプログラムの作成に役立つ言語機能です。 彼らは、マッピング、カリー化、および高階関数の使用に言及しています。 これらは、機能コードを記述するために使用されるプログラミング手法です。 彼らは、並列化、遅延計算、および決定論に言及しています。 これらは、機能プログラムの利点です。



ハンマーイン 機能コードは、副作用がないという1つのプロパティによって区別されます。 現在の関数外のデータに依存せず、関数外のデータを変更しません。 他のすべての「プロパティ」はこれから推測できます。



機能しない機能:



a = 0 def increment1(): global a a += 1
      
      







機能的機能:



 def increment2(a): return a + 1
      
      







リストを調べる代わりに、mapとreduceを使用します



地図



関数とデータセットを受け入れます。 新しいコレクションを作成し、各データ項目で機能を実行し、新しいコレクションに戻り値を追加します。 新しいコレクションを返します。



名前のリストを受け入れ、長さのリストを返す単純なマップ:



 name_lengths = map(len, ['', '', '']) print name_lengths # => [4, 4, 3]
      
      







このマップは各要素を二乗します:



 squares = map(lambda x: x * x, [0, 1, 2, 3, 4]) print squares # => [0, 1, 4, 9, 16]
      
      







名前付き関数を受け入れませんが、ラムダを介して定義された匿名関数を取ります。 ラムダパラメーターは、コロンの左側に定義されます。 関数の本体は右側にあります。 結果は暗黙的に返されます。



次の例の機能しないコードは、名前のリストを取得し、それらをランダムなニックネームに置き換えます。



 import random names = ['', '', ''] code_names = ['', '', ''] for i in range(len(names)): names[i] = random.choice(code_names) print names # => ['', '', '']
      
      







アルゴリズムは、同じニックネームを異なるシークレットエージェントに割り当てることができます。 うまくいけば、これは秘密の任務中に問題の原因にならないでしょう。



マップを使用してこれを書き換えます。



 import random names = ['', '', ''] secret_names = map(lambda x: random.choice(['', '', '']), names)
      
      







演習1 。 mapを使用して次のコードを書き直してください。 実名のリストを取得し、より信頼性の高い方法を使用してニックネームに置き換えます。



 names = ['', '', ''] for i in range(len(names)): names[i] = hash(names[i]) print names # => [6306819796133686941, 8135353348168144921, -1228887169324443034]
      
      







私の解決策:
 names = ['', '', ''] secret_names = map(hash, names)
      
      









減らす



Reduceは、関数とアイテムのセットを受け入れます。 すべてのアイテムを組み合わせて取得した値を返します。



単純なreduceの例。 セット内のすべてのアイテムの合計を返します。



 sum = reduce(lambda a, x: a + x, [0, 1, 2, 3, 4]) print sum # => 10
      
      







xは現在のアイテムであり、バッテリーです。 これは、ラムダが前の段落で返す値です。 reduce()は、すべての値を反復処理し、aとxの現在の値で各ラムダに対して実行し、次の反復の結果をaで返します。



しかし、最初の反復では何が同じですか? コレクションの最初の要素と等しく、reduce()は2番目の要素から始まります。 つまり、最初のxはセット内の2番目のアイテムと等しくなります。



次の例では、「captain」という単語が行のリストに表示される頻度を考慮しています。



 sentences = ['  ', '  ', '  , '] cap_count = 0 for sentence in sentences: cap_count += sentence.count('') print cap_count # => 3
      
      







reduceを使用した同じコード:



 sentences = ['  ', '  ', '  , '] cap_count = reduce(lambda a, x: a + x.count(''), sentences, 0)
      
      







そして、aの初期値はどこから来ますか? 最初の行の繰り返し数から計算することはできません。 したがって、reduce()関数の3番目の引数として指定されます。



なぜmap and reduceが優れているのですか?



まず、通常は1行に収まります。



第二に、反復の重要な部分であるコレクション、操作、および戻り値は、常にmap and reduceと同じ場所にあります。



第三に、ループ内のコードは、以前に定義された変数の値を変更したり、その後にあるコードに影響を与えたりする可能性があります。 慣例により、mapおよびreduceは機能します。



第4に、mapとreduceは基本的な操作です。 ループを1行ずつ読み取るのではなく、読者がマップを認識し、組み込みの複雑なアルゴリズムを削減する方が簡単です。



第5に、これらの関数の便利でわずかに変更された動作を許可する多くの友人がいます。 たとえば、filter、all、any、およびfind。



演習2 :map、reduce、filterを使用して次のコードを書き換えます。 フィルターは関数とコレクションを受け入れます。 関数がTrueを返す対象のコレクションを返します。



 people = [{'': '', '': 160}, {'  ': '', '  ': 80}, {'name': ''}] height_total = 0 height_count = 0 for person in people: if '' in person: height_total += person['  '] height_count += 1 if height_count > 0: average_height = height_total / height_count print average_height # => 120
      
      







私の解決策:
 people = [{'': '', '': 160}, {'  ': '', '  ': 80}, {'name': ''}] heights = map(lambda x: x[''], filter(lambda x: '' in x, people)) if len(heights) > 0: from operator import add average_height = reduce(add, heights) / len(heights)
      
      









命令ではなく宣言的に書く



次のプログラムは、3台の車のレースをエミュレートします。 各時点で、車は前進するかしないかのいずれかです。 プログラムが車で移動したパスを表示するたびに。 5つの間隔の後、レースは終了します。



出力例:



  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      
      







プログラムテキスト:



 from random import random time = 5 car_positions = [1, 1, 1] while time: # decrease time time -= 1 print '' for i in range(len(car_positions)): # move car if random() > 0.3: car_positions[i] += 1 # draw car print '-' * car_positions[i]
      
      







コードは必須です。 機能的なバージョンは宣言的です-それはどのように行われるべきかではなく、何が行われる必要があるかを記述します。



関数を使用します



宣言可能性は、コードを関数に挿入することで実現できます。



 from random import random def move_cars(): for i, _ in enumerate(car_positions): if random() > 0.3: car_positions[i] += 1 def draw_car(car_position): print '-' * car_position def run_step_of_race(): global time time -= 1 move_cars() def draw(): print '' for car_position in car_positions: draw_car(car_position) time = 5 car_positions = [1, 1, 1] while time: run_step_of_race() draw()
      
      







プログラムを理解するために、読者はメインループを調べます。 「時間が残っている場合は、レースの1ステップを経て結果を表示します。 もう一度時間を確認してください。」 読者がレースステップの仕組みを理解する必要がある場合、コードを個別に読むことができます。



コメントは不要です。コード自体が説明しています。



コードを関数に分割すると、コードが読みやすくなります。 この手法は関数を使用しますが、ルーチンとしてのみ使用します。 彼らはコードをパックしますが、機能的にしません。 関数は、値を返すのではなく、それらを囲むコードに影響を与え、グローバル変数を変更します。 読者が変数に遭遇した場合、それがどこから来たのかを見つける必要があります。



このプログラムの機能バージョンは次のとおりです。



 from random import random def move_cars(car_positions): return map(lambda x: x + 1 if random() > 0.3 else x, car_positions) def output_car(car_position): return '-' * car_position def run_step_of_race(state): return {'time': state['time'] - 1, 'car_positions': move_cars(state['car_positions'])} def draw(state): print '' print '\n'.join(map(output_car, state['car_positions'])) def race(state): draw(state) if state['time']: race(run_step_of_race(state)) race({'time': 5, 'car_positions': [1, 1, 1]})
      
      







現在、コードは機能的な関数に分割されています。 これには3つの兆候があります。 最初の-共有変数はありません。 timeとcar_positionsはrace()に直接渡されます。 2番目は、関数がパラメーターを取ることです。 第三に、変数は関数内で変化せず、すべての値が返されます。 run_step_of_race()が次のステップを実行するたびに、次のステップに再び渡されます。



以下に2つの関数zero()とone()を示します。



 def zero(s): if s[0] == "0": return s[1:] def one(s): if s[0] == "1": return s[1:]
      
      







ゼロ()は文字列sを取ります。 最初の文字が0の場合、文字列の残りを返します。 そうでない場合は、なし。 最初の文字が1の場合、one()は同じことを行います。



rule_sequence()関数を想像してください。 文字列と、0個と1個の関数で構成されるルール関数のリストを受け取ります。 最初のルールを呼び出して、文字列を渡します。 Noneが返されない場合、返された値を受け取り、次のルールを呼び出します。 などなど。 Noneが返された場合、rule_sequence()は停止し、Noneを返します。 それ以外の場合、最後のルールの意味。



入力および出力データの例:



 print rule_sequence('0101', [zero, one, zero]) # => 1 print rule_sequence('0101', [zero, zero]) # => None
      
      







rule_sequence()の命令型バージョン:



 def rule_sequence(s, rules): for rule in rules: s = rule(s) if s == None: break return s
      
      







演習3 。 このコードはループを使用します。 再帰を使用して宣言的に書き換えます。



私の解決策:
 def rule_sequence(s, rules): if s == None or not rules: return s else: return rule_sequence(rules[0](s), rules[1:])
      
      









パイプラインを使用する



次に、パイプラインと呼ばれる手法を使用して、他の種類のサイクルを書き換えます。



次のサイクルでは、名前、間違った原産国、いくつかのグループのステータスを含む辞書を変更します。



 bands = [{'name': 'sunset rubdown', 'country': 'UK', 'active': False}, {'name': 'women', 'country': 'Germany', 'active': False}, {'name': 'a silver mt. zion', 'country': 'Spain', 'active': True}] def format_bands(bands): for band in bands: band['country'] = 'Canada' band['name'] = band['name'].replace('.', '') band['name'] = band['name'].title() format_bands(bands) print bands # => [{'name': 'Sunset Rubdown', 'active': False, 'country': 'Canada'}, # {'name': 'Women', 'active': False, 'country': 'Canada' }, # {'name': 'A Silver Mt Zion', 'active': True, 'country': 'Canada'}]
      
      







関数「フォーマット」の名前は一般的すぎます。 一般に、コードはいくつかの懸念を引き起こします。 1つのサイクルで3つの異なることが起こります。 「国」キーの値が「カナダ」に変わります。 ドットが削除され、名前の最初の文字が大文字に変更されます。 コードが何をすべきかを理解することは困難であり、そうするかどうかを言うことは困難です。 使用、テスト、および並列化は困難です。



比較する:



 print pipeline_each(bands, [set_canada_as_country, strip_punctuation_from_name, capitalize_names])
      
      







すべてがシンプルです。 ヘルパー関数は互いに連鎖しているため、機能しているように見えます。 前を終了-次を入力します。 検証、再利用、検証、並列化が簡単です。



pipeline_each()は一度に1つずつグループを反復処理し、set_canada_as_country()などの変換関数に渡します。 関数をすべてのグループに適用した後、pipeline_each()はそれらからリストを作成し、次のリストに渡します。



変換関数を見てみましょう。



 def assoc(_d, key, value): from copy import deepcopy d = deepcopy(_d) d[key] = value return d def set_canada_as_country(band): return assoc(band, 'country', "Canada") def strip_punctuation_from_name(band): return assoc(band, 'name', band['name'].replace('.', '')) def capitalize_names(band): return assoc(band, 'name', band['name'].title())
      
      







それぞれがグループキーを新しい値に関連付けます。 元のデータを変更せずにこれを行うのは難しいため、assoc()で解決します。 deepcopy()を使用して、渡された辞書のコピーを作成します。 各関数はコピーを変換し、そのコピーを返します。



すべてが正常なようです。 変更から保護された元のデータ。 ただし、コード内でデータを変更できる場所は2つあります。 strip_punctuation_from_name()で、ドットなしの名前は、元の名前でreplace()を呼び出すことで作成されます。 capitalize_names()では、タイトル()と元の名前に基づいて、最初の大文字で名前が作成されます。 replaceとtimeが機能しない場合、strip_punctuation_from_name()with capitalize_names()は機能しません。



幸いなことに、それらは機能的です。 Pythonでは、文字列は不変です。 これらの関数は、文字列のコピーで機能します。 うーん、神に感謝します。



Pythonの文字列と辞書(その可変性)のこの対照は、Clojureのような言語の利点を示しています。 そこで、プログラマはデータを変更するかどうかを考える必要はありません。 変わりません。



演習4 。 pipeline_each関数を作成してみてください。 操作のシーケンスについて考えてください。 グループ-配列では、最初の変換関数のために一度に1つずつ渡されます。 次に、結果の配列には、2番目の関数に対して1つの小さなものが渡されます。



私の解決策:
 def pipeline_each(data, fns): return reduce(lambda a, x: map(x, a), fns, data)
      
      









結果として、3つの変換関数はすべて、グループの特定のフィールドを変更します。 call()を使用して、このための抽象化を作成できます。 関数と、それが適用されるキーを受け入れます。



 set_canada_as_country = call(lambda x: 'Canada', 'country') strip_punctuation_from_name = call(lambda x: x.replace('.', ''), 'name') capitalize_names = call(str.title, 'name') print pipeline_each(bands, [set_canada_as_country, strip_punctuation_from_name, capitalize_names])
      
      







または、読みやすさを犠牲にします。



 print pipeline_each(bands, [call(lambda x: 'Canada', 'country'), call(lambda x: x.replace('.', ''), 'name'), call(str.title, 'name')])
      
      







呼び出しのコード():



 def assoc(_d, key, value): from copy import deepcopy d = deepcopy(_d) d[key] = value return d def call(fn, key): def apply_fn(record): return assoc(record, key, fn(record.get(key))) return apply_fn
      
      







ここで何が起こっていますか?



一。 呼び出しは高階関数です 別の関数を引数として受け取り、関数を返します。



二。 apply_fn()は変換関数に似ています。 エントリ(グループ)を取得します。 レコード[キー]の値を検索します。 fnを呼び出します。 結果をレコードのコピーに割り当てて返します。



三。 呼び出し自体は何もしません。 すべての作業はapply_fn()によって行われます。 pipeline_each()の例では、apply_fn()の1つのインスタンスが「country」を「Canada」に設定します。 別の-最初の文字を大文字にします。



4。 apply_fn()インスタンスが実行されると、fnおよびキー関数はスコープ内で使用できなくなります。 これらはapply_fn()引数またはローカル変数ではありません。 しかし、それらへのアクセスは可能になります。 関数を定義するとき、閉じる変数への参照を保存します—変数は関数の外部で定義され、内部で使用されます。 関数が開始されると、変数はローカル間で検索され、次に引数間で検索され、次に閉じたものへの参照間で検索されます。 fnとキーがあります。



5。 通話中のグループに関する言及はありません。 これは、呼び出しがパイプラインの内容に関係なく、パイプラインの作成に使用できるためです。 特に、関数型プログラミングは、作曲と再利用に適した一般的な関数のライブラリを構築します。



よくできました。 クロージャ、高階関数、およびスコープはすべていくつかの段落にあります。 クッキーと一緒にお茶を飲むこともできます。



これらのグループの処理がもう1つ残っています。 名前と国を除くすべてを削除します。 Extract_name_and_country()関数:



 def extract_name_and_country(band): plucked_band = {} plucked_band['name'] = band['name'] plucked_band['country'] = band['country'] return plucked_band print pipeline_each(bands, [call(lambda x: 'Canada', 'country'), call(lambda x: x.replace('.', ''), 'name'), call(str.title, 'name'), extract_name_and_country]) # => [{'name': 'Sunset Rubdown', 'country': 'Canada'}, # {'name': 'Women', 'country': 'Canada'}, # {'name': 'A Silver Mt Zion', 'country': 'Canada'}]
      
      







extract_name_and_country()は、pluck()と呼ばれる一般化された形式で記述できます。 次のように使用されます。



 print pipeline_each(bands, [call(lambda x: 'Canada', 'country'), call(lambda x: x.replace('.', ''), 'name'), call(str.title, 'name'), pluck(['name', 'country'])])
      
      







演習5 。 pluckは、レコードから抽出するキーのリストを受け入れます。 書いてみてください。 これは高次関数です。



私の解決策:
 def pluck(keys): def pluck_fn(record): return reduce(lambda a, x: assoc(a, x, record[x]), keys, {}) return pluck_fn
      
      









そして今何?



機能的なコードは従来のコードとよく合います。 この記事からの変換は、どの言語でも使用できます。 コードを試してみてください。



マーシャ、ペティア、ヴァシャについて覚えておいてください。 リストの反復をマップに変換して削減します。



レースを覚えてください。 コードを関数に分割し、それらを機能的にします。 ループを再帰に変えます。



グループを覚えておいてください。 一連の操作をパイプラインに変換します。



All Articles