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

関数型Pythonアンチパターン

Sponsored · SiteGround - Reliable hosting with speed, security, and support you can count on.
Avatar for ukyo ukyo
August 28, 2020

 関数型Pythonアンチパターン

Avatar for ukyo

ukyo

August 28, 2020
Tweet

Other Decks in Programming

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__を使えばオブジェクトを直接呼び出せる