reduce関数によるパイプライン処理パターン
1. 背景
シェルスクリプトのパイプ的な処理を書きたい
- 関数の出力を次の関数の入力として使いたい場合がある
- 普通に関数をネストさせると最初に実行される関数が最も内側に配置されるので読みにくい
- シェルスクリプトのパイプのように関数の返り値を次の関数に直列に繋いだように記述したい
2. 課題
関数のネストは読みにくい
- シンプルに関数をネストさせることで目的は達成されるが、一番内側の関数から実行されるので直感に反するコードになってしまい実際読みにくい
def run(value):
return f3(f2(f1(value)))
3. 解決策
reduce関数にデータと関数のリストを渡す
- reduce関数に関数のリストと初期値を与えることで擬似的にパイプライン演算子やシェルスクリプトのパイプを模倣することができる
- ↓の関数は初期値となる
value
と関数のリストであるfns
を引数に持つ
def pipe(value, *fns):
return reduce(lambda v, f: f(v), fns, value)
- 動作としては、
reduce
関数のアキュムレータにデータとクロージャを受け取る関数を与え、第二引数のリストに関数のリスト(fns
)与え、関数のリストを連続して実行するというものになる
- この時、先に実行された関数の戻り値は次の関数の引数になるので、あたかもシェルスクリプトのパイプを書いているような感覚で記述することができる
- このように使う↓
p = pipe(
[1, 2, 3, 4, 5, 6, 7, 8, 9],
fn1,
fn2,
fn3,
fn4,
)
print(p)
4. メリット
関数を直列して実行する処理をパイプのように記述することができる
- シェルスクリプトのパイプのように関数の返り値を直接次の関数に渡すかのような記述ができるので、直列した処理を記述したい場合は読みやすいコードになる
- テンポラリ変数を使わないので変数の取り違えバグが原理的に発生しない
- 処理の途中に処理を挿入したい場合も新しい関数を定義して差し込めばよいだけなので、機能追加も(まぁまぁ)容易である
5. デメリット
初見では挙動を想像しにくい
- 「関数の直列実行をパイプのように記述したい」という目的を知らなければ何がしたいのか理解するのが難しいコードになる
- コードの意図や目的について適切なコメントや説明が必要となり、その分余計なコストがかかる
条件分岐に対応できない(しづらい)
- シェルスクリプトでもそうだが、パイプで連結した処理は条件分岐で処理を分岐させることができない
- 条件分岐をしたくなったら処理を大幅に変えることになる
- 無理矢理書くとしたら↓のようになるが、いかにも冗長である
def run(value):
v1 = pipe(
value,
fn1,
fn2,
fn3,
)
v2 = branch(v1)
print(v2)
def branch(value)
if value == xxx:
result = pipe(
value,
fn4,
fn5,
fn6,
)
else:
# 条件にマッチしない場合 fn4 をスキップする
result = pipe(
value,
fn5,
fn6,
)
return result
6. 注意事項
正直なところ完全に趣味の領域である
- 実際のところ、劇的に読みやすくなるわけでもなく、さほど書きやすくなるわけでもない、更には利用する場面も限定的で、その上十分な説明を必要とする
- 何となくパイプライン風の見た目になって気分がいい程度の効果しかないのであまり活用できる場面は多くない
- 結論としては、あまり多用すべきテクニックではない
テンポラリ変数がベターな選択肢
- 関数の戻り値を次の関数の入力にしたい場合、やはりテンポラリ変数を使用するのが一番穏当な解決策ではある
- とはいえ、似たような変数が並ぶ関係上、変数を取り違えてバグを生む可能性も無いわけではない(曖昧な物言い)
def run(value):
result1 = f1(value)
result2 = f2(result1)
result3 = f3(result2)
return result3
参考資料
Appendix-1 素直にループで書いた例
def pipe(value, *fns):
buffer = value
for fn in fns:
buffer = fn(buffer)
return buffer
def main():
result = pipe([...], fn1, fn2, fn3)
print(result)
- 無理矢理
reduce
関数を使うより、こっちの方がシンプルで良いのではという感がある