Upgrade to Pro — share decks privately, control downloads, hide ads and more …

関数型Pythonアンチパターン

ukyo
August 28, 2020

 関数型Pythonアンチパターン

ukyo

August 28, 2020
Tweet

Transcript

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

    元哲学者 ◦ 型付きλ計算とか継続とか • Python好きだしよく使う • 関数型プログラミング好き ◦ Java8 StreamやC# LINQで触れる ◦ 圏論とかHaskellとかよく知らない ◦ 今Racket勉強中
  2. 関数型プログラミングって何 • 状態変化を排除 ◦ 状態によってコードの結果が変化することがない ▪ バグが少ない ▪ テストしやすい ◦

    状態変化を追う必要がない ▪ コードが読みやすい • 高階関数の多用 ◦ 処理を自由に入れ替え、抽象化されたコードが書ける
  3. 愚直にループ(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) • 状態変化使ってる
  4. 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(); • 状態変化がない • 高階関数を使っている
  5. 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)))))
  6. やべえやつ再掲 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)))))
  7. Python、長い式に向かない • パイプライン演算子(スレッディングマクロ)がない ◦ g(f(x))を、x > f > gと書けるやつ ◦

    メソッドチェーンがあればいけるが、関数では使えない • 「長い式」がそもそも書きづらい ◦ PEP8:1行79字以内 ◦ 改行に特別な記法が必要 ▪ 改行したところでバックスラッシュ ▪ 全体をカッコで囲う
  8. 式を区切り、名付ける 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) • 冗長だが、ぎこちなさはない(?) • それぞれの関数が「何をやったか」、注釈を付けることができる
  9. map/filter:関数型コレクション処理 heights = list(map(lambda x: x.height, students)) even = list(filter(lambda

    x: x % 2 == 0, range(40))) • 関数型コレクション処理の代表的関数 ◦ Python以外の言語で関数型っぽく書くときよく使う ◦ Python関係の文書でも関数型っぽい関数として紹介されることがある ▪ 『Python ハッカーガイドブック』(2020邦訳版)
  10. 内包表記に書き換え 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] • 状態変化はないため、依然関数型といっていい
  11. 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で消したがってた
  12. 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)))
  13. 内包、楽しくなってくる # 必修の教科書の値段を合計する # 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) ) • ちょっとやりすぎ
  14. ループを書いて、関数に状態変化を隠蔽 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章
  15. 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に並ぶ 関数型代表選手
  16. reduce、楽しくなってくる results = reduce(lambda x, f: [*x, f(x[-1])], fs, [students])

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

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

    forがPythonic、と考えた方が統一的なスタイルなんじゃない? ◦ map/filter < 内包 ◦ そもそも関数型じゃないPythonコードが世の中にたくさんある • 情報を圧縮するより、冗長でも明示化、がPythonicなんじゃない? ◦ 長い式 < 式を区切り、名付ける ◦ explicit is better than implicit ◦ 強制インデント
  19. 最初の例題再掲 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)))))
  20. 書き換え案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)
  21. 書き換え案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) • ループを関数に括り出した だけ • それでも関数型!
  22. いろんな内包 # 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)
  23. オレオレ内包 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)
  24. クロージャ+高階関数 def major_is(major): # 下がクロージャ def _ret(student): return student.major is

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

    __call__(self, student): return student.major is self.major • 以下のような等価な書き換えが可能 ◦ クロージャ→オブジェクト ◦ 高階関数→クラス • __call__を使えばオブジェクトを直接呼び出せる