functools (これは、私にとって不必要なあらゆる種類のダンプです:-)。
-グイド・ファン・ロッサム
AFに関する記事のように思えるかもしれませんが、パラダイムについては説明しません。 コードの再利用と簡素化についてです-コードを書きすぎていることを証明しようとするので、テストが複雑で難しくなりますが、最も重要なのは、読み取りと変更に時間がかかることです。
この記事では、 ファンシーライブラリの例や概念を取り上げています。 第一に、それはクールで、第二に、すぐに使用を開始できます。 はい、FPが必要です。
FPについて簡単に説明します
- 純関数
- 高階関数
- 機能的に書かない人に対する自尊心(オプション)
AFには次の手法もあります。
- 部分適用
- 構成(Pythonにはまだデコレータがあります)
- レイジーコンピューティング
これをすべて知っている場合は、例に直接進んでください。
純粋な機能
純粋な関数はパラメーターのみに依存し、結果のみを返します。 同じ引数で複数回呼び出される次の関数は、異なるオブジェクトになります(この場合は同じオブジェクトですが)。
労働力の値を持つ要素のリストを返すフィルター関数を作成します。
pred = bool result = [] def filter_bool(seq): for x in seq: if pred(x): result.append(x) return result
きれいにしましょう:
pred = bool def filter_bool(seq): result = [] for x in seq: if pred(x): result.append(x) return result
これで、彼女のラードを1回連続で呼び出すことができ、結果は同じになります。
高階関数
これらは 、他の関数を引数として受け取るか、結果として別の関数を返す関数です。
def my_filter(pred, seq): result = [] for x in seq: if pred(x): result.append(x) return result
関数の名前を変更する必要がありました。これは、今でははるかに便利だからです。
above_zero = my_filter(bool, seq) only_odd = my_filter(is_odd, seq) only_even = my_filter(is_even, seq)
1つの関数が既に多くのことを行っていることに注意してください。 実際、彼女は怠け者である必要があります。
def my_filter(pred, seq): for x in seq: if pred(x): yield x
コードを削除したことに気づきましたか? これはほんの始まりに過ぎず、まもなく休日にのみ関数を作成します。 ここを見てください:
my_filter = filter
pythonのビルトイン機能は、一生涯ほぼ十分であり、それらを正しく構成する必要があります。
部分適用
これは、関数の引数の一部を修正するプロセスであり、アリティの低い別の関数を作成します。 私たちのものに翻訳されているのはfunctools.partial
です。
filter_bool = partial(filter, bool) filter_odd = partial(filter, is_odd) filter_even = partial(filter, is_even)
これがすべてFPの基本であることは理解していますが、新しいものは何も書いていないことに注意してください。既製の関数を作成し、他の関数を作成しました。 新しい機能の基本は、非常に小さく、シンプルで、簡単にテストできる機能です。これらを安全に使用して、より複雑な機能を作成できます。
構成
Pythonには、そのようなシンプルでクールで必要なものはありません。 あなたはそれを自分で書くことができますが、私は正気の実装が欲しいです:(
def compose(*fns): init, *rest = reversed(fns) return lambda *a, **kw: reduce(lambda a, b: b(a), rest, init(*a, **kw))
これであらゆることを実行できます(実行は右から左に進みます)。
mapv = compose(list, map) filterv = compose(list, filter)
これらは、Pythonの2番目のバージョンからのmap
とfilter
以前のバージョンです。 遅延map
が必要な場合、 mapv
を呼び出すことができます。 または、昔ながらの方法でもう少しコードを記述します。 毎回。
compose
関数とpartial
関数は、既製のテスト済み関数を再利用できるという点で優れています。 しかし、最も重要なことは、このアプローチの利点を理解していれば、時間の経過とともにすぐにそれらを作曲の準備ができるようになることです。
これは非常に重要なポイントです-関数は1つの簡単な問題を解決する必要があります:
- 彼女は小さくなります
- テストが簡単になります
- 作りやすい
- 読んで変更するだけ
- 壊れにくい
例
タスク:シーケンスからNone
をドロップします。
昔ながらの決定(ほとんどの場合、関数として記述されていません):
no_none = (x for x in seq if x is not None)
注:式に含まれる変数名に関係なく。 あまり重要ではないので、ほとんどのプログラマーは気にしないように愚かにx
書いています。 誰もがこの無意味なコードを何度も書きます。 各検閲時間: for
、 in
、 if
、および何度もx
スコープを補正する必要があり、独自の構文があるため。 ループの各反復に対して変数に値を割り当てます。 そして、それが割り当てられ、条件がチェックされます。
このボイラープレートを作成し、このボイラープレートのテストを作成するたびに。 なんで?
書き直しましょう:
from operator import is_ from itertools import filterfalse from functools import partial is_none = partial(is_, None) filter_none = partial(filterfalse, is_none) # no_none = filter_none(seq) # all_none = compose(all, partial(map, is_none))
それだけです 余分なコードはありません。 このコード( no_none = filter_none(seq)
)は非常に単純なので、私はこれを読むことを楽しんでいます。 この関数が機能する方法は、プロジェクトで常に1回だけ読み取る必要があります。 補償は、その機能を正確に理解するために毎回読む必要があります。 まあ、または関数に入れて、違いはありませんが、テストを忘れないでください。
例2
かなり一般的なタスクは、辞書の配列からキー値を取得することです。
names = (x['name'] for x in users)
ちなみに、それは非常に迅速に動作しますが、再び不必要なゴミの束を書きました。 さらに速く動作するように書き直します。
from operator import itemgetter def pluck(key, seq): return map(itemgetter(key), seq) # names = pluck('name', users)
そして、これをどのくらいの頻度で行いますか?
get_names = partial(pluck, 'name') get_ages = partial(pluck, 'age') # get_names_ages = partial(pluck, ('name', 'age')) users_by_age = compose(dict, get_names_ages) ages = users_by_ages(users) # {x['name']: x['age'] for x in users}
そして、オブジェクトがあれば? Pf、これをパラメーター化:
from operator import itemgetter, attrgetter def plucker(getter, key, seq): return map(getter(key), seq) pluck = partial(plucker, itemgetter) apluck = partial(plucker, attrgetter) # names = pluck('name', users) # (x['name'] for x in users) object_names = apluck('name', users) # (x.name for x in users) # object_data = apluck(('name', 'age', 'gender'), users) # ((x.name, x.age, x.gender) for x in users)
例3
シンプルなジェネレーターを想像してください:
def dumb_gen(seq): result = [] for x in seq: # - c result.append(x) return result
ボイラープレートはたくさんあります。空のリストを作成してから、ループを作成し、リストに要素を追加して、それを渡します。 私は文字通り関数の本体全体をリストしたようです:(
正しい解決策はfilter(pred, seq)
またはmap(func, seq)
を使用filter(pred, seq)
ことですが、時にはもっと複雑なものを作成する必要があります。 ジェネレーターを作成する必要があります。 そして、結果がリストまたはタプラの形で常に必要な場合はどうなりますか? はい、簡単です:
@post_processing(list) def dumb_gen(seq): for x in seq: ... yield x
これはパラメトリックデコレータで、次のように機能します。
result = post_processing(list)(dumb_gen)(seq)
すなわち 最初の呼び出しの結果は、関数を引数として受け取り、別の関数を返す新しい関数になります。 それよりも難しい音:
def post_processing(post): return lambda func: compose(post, func)
既存のcompose
を使用したことに注意してください。 結果は誰も書いていない新しい機能です。
そして今、詩:
post_list = post_processing(list) post_tuple = post_processing(tuple) post_set = post_processing(set) post_dict = post_processing(dict) join_comma = post_processing(', '.join) @post_list def dumb_gen(pred, seq): for x in seq: ... yield x
1つの価格で多数の新機能! そして、定型文を削除すると、関数は小さくなり、ずっときれいになりました。
まとめ
鉄筋コンクリート機能(クリーン、高)を使用してデータを調べ、実装の容易さを維持し、テストの容易なプログラムの安定性を確保します。
- 純粋な関数を書くと、プログラムの安定性が保証されます
- 高階関数を書くと、コードははるかにコンパクトで信頼性が高くなります
- コードの作成、装飾、部分的な適用、再利用
- sishnymiを使用すると、ソフトウェアに速度が与えられます
ツールキットを作成するとすぐに、問題の一部を解決できるものがあることを知って、新しいコードが作成されます。 したがって、ソフトウェアはより小さくて簡単になります。
どこから始めますか?
- 特に最後に例とともに、 itertools 、 functools 、 operator 、 collectionsを必ずチェックしてください
- funcyまたは他のfpshのドキュメントを見て、ソースコードを読んでください
- あなたの
funcy
書いてください、あなたはそれのすべてをすぐに必要としません、しかし、経験は非常に役に立ちます
クレジット
私の場合、AFの使用はclojureの知識から始まりました-このことは完全に脳を回します。YouTubeで少なくともvidos を見ることを強くお勧めします 。
Clojureは、通常のことなしに、簡単に書かなければならないように何らかの形で配置されています。変数なしで、お気に入りのスタイルの「小説」なしで、最初にヒーローの性格を明らかにし、次に彼の心の問題に取り組みます。 clojureでは、考えなければなりません%)基本的なデータ型と「構文の欠如」 (c)のみを持っています。 そして、この「単純な」概念は、Pythonに移植できることがわかりました。
UPD
読者は、私が継続的なFPで書いているという印象を持っているようです。 すべての人を安心させたい:私は、 すでに書いたコードが書かれている場所でのみ機能的なアプローチを使用します。 私の意見では、「働く」トリックを毎回繰り返すことは愚かで無意味なので、そのようなピースを関数に変換して再利用します。 実例はコメントにあります 。