関数型Pythonアンチパターン

1dde44a6055fe7b3511d5cb543d88cbe?s=47 ukyo
August 28, 2020

 関数型Pythonアンチパターン

1dde44a6055fe7b3511d5cb543d88cbe?s=128

ukyo

August 28, 2020
Tweet

Transcript

  1. 関数型Pythonアンチパターン 鈴木佑京

  2. 自己紹介 • 鈴木佑京(すずき うきょう) • 株式会社ピコラボ所属プログラマ ◦ 受託研究&開発 ▪ 機械学習とか •

    元哲学者 ◦ 型付きλ計算とか継続とか • Python好きだしよく使う • 関数型プログラミング好き ◦ Java8 StreamやC# LINQで触れる ◦ 圏論とかHaskellとかよく知らない ◦ 今Racket勉強中
  3. 今日の構成 • 目標説明 ◦ 関数型だけどunPythonic にNO ◦ 関数型だしPythonic にYES • アンチパターンと対処法を3つ紹介 ◦

    長い式を連ねる ◦ map/filterを使う ◦ 内包/reduceを濫用する • まとめ
  4. 概要 • 「関数型」と「Pythonic」は直交する • 「関数型だがunPythonic」なアンチパターンを紹介していく 関数型 not関数型 Pyth onic un

    Pyth onic
  5. 関数型プログラミングって何 • 状態変化を排除 ◦ 状態によってコードの結果が変化することがない ▪ バグが少ない ▪ テストしやすい ◦

    状態変化を追う必要がない ▪ コードが読みやすい • 高階関数の多用 ◦ 処理を自由に入れ替え、抽象化されたコードが書ける
  6. Pythonicって何 • Pythonの言語機能にマッチする • Pythonのコミュニティの価値観によりマッチする アンケート調査したわけでもなく、全く客観的なものではないので、根 本的にはお気持ちとして受け取ってください

  7. 例題: 「数学専攻で、20代の生徒の 身長の平均を計算する」

  8. 愚直にループ(Python) heights = [] for s in students: if s.major

    is not Major.Math: continue if s.age < 20: continue if s.age >= 30: continue heights.append(s.height) average_height = mean(heights) • 状態変化使ってる
  9. Python以外の言語における関数型(C#) var average_height = students.Where(x => x.Major == Major.Math) .Where(x

    => x.Age >= 20) .Where(x => x.Age < 30) .Select(x => x.Height) .Average(); • 状態変化がない • 高階関数を使っている
  10. Pythonに「直訳」 average_height = mean( map(lambda x: x.height, filter(lambda x: x.age

    < 30, filter(lambda x: x.age >= 20, filter(lambda x: x.major is Major.MATH, students)))))
  11. 関数型ではあるが ぎこちなく、読みづらい

  12. こういうのを書かないための アンチパターン

  13. アンチパターン その1: 長い式を連ねる

  14. やべえやつ再掲 average_height = mean( map(lambda x: x.height, filter(lambda x: x.age

    < 30, filter(lambda x: x.age >= 20, filter(lambda x: x.major is Major.MATH, students)))))
  15. いくつか問題があるが、 とりあえず式が長すぎ

  16. 関数型、式が長くなりがち • 「返り値」で全てを表現するので、式を連ねやすい • λ式によって式が複雑化しやすい 場合によっては一行でいろいろやりすぎて読みづらいことも ……

  17. Python、長い式に向かない • パイプライン演算子(スレッディングマクロ)がない ◦ g(f(x))を、x > f > gと書けるやつ ◦

    メソッドチェーンがあればいけるが、関数では使えない • 「長い式」がそもそも書きづらい ◦ PEP8:1行79字以内 ◦ 改行に特別な記法が必要 ▪ 改行したところでバックスラッシュ ▪ 全体をカッコで囲う
  18. 「長い式を書かせない」のがPython ……と考えたらどうか

  19. 対処法: 式を区切り、名付ける

  20. 式を区切り、名付ける def is_20s(x): return 20 <= x.age < 30 math

    = filter(lambda x: x.major is Major.MATH, students) math_20s = filter(is_20s, math_students) math_20s_heights = map(lambda x: x.height, math_students_20s) average_height = mean(math_20s_heights) • 冗長だが、ぎこちなさはない(?) • それぞれの関数が「何をやったか」、注釈を付けることができる
  21. アンチパターン その2: map/filterを使う

  22. map/filter:関数型コレクション処理 heights = list(map(lambda x: x.height, students)) even = list(filter(lambda

    x: x % 2 == 0, range(40))) • 関数型コレクション処理の代表的関数 ◦ Python以外の言語で関数型っぽく書くときよく使う ◦ Python関係の文書でも関数型っぽい関数として紹介されることがある ▪ 『Python ハッカーガイドブック』(2020邦訳版)
  23. Pythonicじゃない(断言)

  24. 対処法: 内包表記を使う

  25. 内包表記に書き換え heights = list(map(lambda x: x.height, students)) even = list(filter(lambda

    x: x % 2 == 0, range(40))) ↓ heights = [x.height for x in students] even = [x for x in range(40) if x % 2 == 0] • 状態変化はないため、依然関数型といっていい
  26. map/filter vs 内包表記 • 「下の方がPythonic」とよく言われる ◦ lambdaなしで済む ◦ 集合の内包表記に近い ◦

    for文のアナロジーで理解しやすい • 下の方がちょっと速い(らしい) ◦ https://stackoverflow.com/questions/1247486/list-comprehension-vs-map • Guidoもmap/filter嫌ってた ◦ https://www.artima.com/weblogs/viewpost.jsp?thread=98196 ◦ Python3で消したがってた
  27. 郷に入ったら郷に従えってことで

  28. それでもmap/filterを使うとき

  29. partialと組み合わせられるのはmap/filterだけ math_filter = partial(filter, lambda x: x.major is Major.Math) tall_filter

    = partial(filter, lambda x: x.height > 100) grade_map = partial(map, lambda x: x.grade) height_map = partial(map, lambda x: x.height) add2_map = partial(map, lambda x: x + 2) result_0 = add2_map(height_map(tall_filter(students)) result_1 = grade_map(math_filter(tall_filter(students)))
  30. アンチパターン その3: 内包/reduceを乱用する

  31. 内包、楽しくなりがち

  32. 内包、楽しくなってくる # 必修の教科書の値段を合計する # 1000円以上の場合は大学から補助が出て 1000円になる total_text_price = sum( book.price

    if book.price < 1000 else 1000 for course in courses if course.mandatory for book in course.textbooks if not i.have(book) ) • ちょっとやりすぎ
  33. 対処法: ループを書き、隠蔽する

  34. ループを書いて、関数に状態変化を隠蔽 def total_price(courses, i): result = 0 for course in

    courses: if not course.mandatory: continue for book in course.textbooks: if i.have(book): continue if book.price < 1000: result += book.price else: result += 1000 return result • ループを書いた方が読みやすい • 状態変化は関数の中に隠蔽する ◦ 関数の外から見れば状態変化してな いので「外からは」関数型 ▪ 状態変化を考えなくて良い ▪ 状態を外から触れない ◦ 「中は」関数型じゃないが、短く端的な らまあ別によいのでは ◦ 参考:On Lisp 第3章
  35. reduceも楽しくなりがち

  36. functools.reduceとは result = reduce(f, [a, b, c] , x) x

    a b c f f f acc = x for e in [a, b, c]: acc = f(acc, e) result = acc map/filterに並ぶ 関数型代表選手
  37. 複数の関数を途中経過をロギングしつつ連続適用 fs = [math_filter, tall_filter, grade_map] x = students results

    = [x] for f in fs: x = f(x) results.append(x)
  38. reduce、楽しくなってくる results = reduce(lambda x, f: [*x, f(x[-1])], fs, [students])

    • 短くはなったが、微妙…… ◦ 慣れてないとかなり読みづらい
  39. ループを書き、隠蔽する def logging_seq_apply(x, fs): results = [x] for f in

    fs: x = f(x) results.append(x) return results • 慣れてなくても読める • これでも実質的には関数型。
  40. reduce vs ループ • Guidoはreduceアンチ ◦ https://www.artima.com/weblogs/viewpost.jsp?thread=98196 ◦ 組み込み→標準ライブラリに格下げ •

    forがPythonic、と考えた方が統一的なスタイルなんじゃない? ◦ map/filter < 内包 ◦ そもそも関数型じゃないPythonコードが世の中にたくさんある • 情報を圧縮するより、冗長でも明示化、がPythonicなんじゃない? ◦ 長い式 < 式を区切り、名付ける ◦ explicit is better than implicit ◦ 強制インデント
  41. 相当単純なreduce以外は、 ループの方がいいのでは

  42. 最初の例題再掲 average_height = mean( map(lambda x: x.height, filter(lambda x: x.age

    < 30, filter(lambda x: x.age >= 20, filter(lambda x: x.major is Major.MATH, students)))))
  43. 書き換え案1:区切り、名付ける+内包を使う def is_20s(x): return 20 <= x.age < 30 math

    = (s for s in students if s.major is Major.MATH) math_20s = (s for s in students if is_20s(s)) average_height = mean(s.heights for s in math_20s)
  44. 書き換え案2:ループを書き、隠蔽する def average_height(students): heights = [] for s in students:

    if s.major is not Major.Math: continue if not is_20s(s): continue heights.append(s.height) return mean(heights) • ループを関数に括り出した だけ • それでも関数型!
  45. まとめ

  46. 紹介したアンチパターンまとめ • 長い式を連ねる ◦ →式を区切り、名付ける • map/filterを使う ◦ →内包表記を使う •

    内包/reduceを乱用する ◦ →ループを書き、隠蔽する
  47. 根拠にした原則 • Pythonicさについて ◦ 長い式を書かせないのがPythonic ◦ 圧縮するより、冗長でも明示化がPythonic ◦ できるだけforで書くのがPythonic •

    関数型について ◦ たとえ状態変化があっても、関数に隠蔽できれば実質関数型
  48. 絶対的なものではないので チーム内で統一が取れれば 別の方針でもいいと思います

  49. 良いコードとは何か 考えるきっかけになれば よかったです

  50. ちょっと脱線して 内包TIPSを一つ

  51. いろんな内包 # list even = [i for i in range(30)

    if i % 2 == 0] # dict str_int = {str(i): i for i in range(30)} # set friends = {f for c in children for f in c.friends} # gen even_gen = (i for i in range(30) if i % 2 == 0)
  52. オレオレ内包 class MyCollection(Sequence): def __init__(self, _iter: Iterable): self._list = list(_iter)

    # ... my_math = MyCollection(x for x in students if x.major is Major.MATH)
  53. アンチパターン その4: クロージャを濫用する & 対処法: オブジェクトと使い分ける

  54. クロージャ+高階関数 def major_is(major): # 下がクロージャ def _ret(student): return student.major is

    major return _ret • ランタイムに処理を生成することができる
  55. クロージャ≒オブジェクト class MajorCheck: def __init__(self, major): self.major == major def

    __call__(self, student): return student.major is self.major • 以下のような等価な書き換えが可能 ◦ クロージャ→オブジェクト ◦ 高階関数→クラス • __call__を使えばオブジェクトを直接呼び出せる
  56. クロージャとオブジェクトの使い分け • クロージャの方がずっとすっきり書ける • オブジェクトに慣れてる人の方がチームに多ければオブジェクトを使う理由 になる ◦ Pythonic、とまでは言えないか…… • 状態変化を含む場合はオブジェクトを使うべき

    ◦ 関数が状態変化を起こすのは予想しづらい
  57. 極めて単純なケース以外は オブジェクトの方が いいのでは?