Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

自己紹介 ● 鈴木佑京(すずき うきょう) ● 株式会社ピコラボ所属プログラマ ○ 受託研究&開発 ■ 機械学習とか ● 元哲学者 ○ 型付きλ計算とか継続とか ● Python好きだしよく使う ● 関数型プログラミング好き ○ Java8 StreamやC# LINQで触れる ○ 圏論とかHaskellとかよく知らない ○ 今Racket勉強中

Slide 3

Slide 3 text

今日の構成 ● 目標説明 ○ 関数型だけどunPythonic にNO ○ 関数型だしPythonic にYES ● アンチパターンと対処法を3つ紹介 ○ 長い式を連ねる ○ map/filterを使う ○ 内包/reduceを濫用する ● まとめ

Slide 4

Slide 4 text

概要 ● 「関数型」と「Pythonic」は直交する ● 「関数型だがunPythonic」なアンチパターンを紹介していく 関数型 not関数型 Pyth onic un Pyth onic

Slide 5

Slide 5 text

関数型プログラミングって何 ● 状態変化を排除 ○ 状態によってコードの結果が変化することがない ■ バグが少ない ■ テストしやすい ○ 状態変化を追う必要がない ■ コードが読みやすい ● 高階関数の多用 ○ 処理を自由に入れ替え、抽象化されたコードが書ける

Slide 6

Slide 6 text

Pythonicって何 ● Pythonの言語機能にマッチする ● Pythonのコミュニティの価値観によりマッチする アンケート調査したわけでもなく、全く客観的なものではないので、根 本的にはお気持ちとして受け取ってください

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

愚直にループ(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) ● 状態変化使ってる

Slide 9

Slide 9 text

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(); ● 状態変化がない ● 高階関数を使っている

Slide 10

Slide 10 text

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)))))

Slide 11

Slide 11 text

関数型ではあるが ぎこちなく、読みづらい

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

やべえやつ再掲 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)))))

Slide 15

Slide 15 text

いくつか問題があるが、 とりあえず式が長すぎ

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

Python、長い式に向かない ● パイプライン演算子(スレッディングマクロ)がない ○ g(f(x))を、x > f > gと書けるやつ ○ メソッドチェーンがあればいけるが、関数では使えない ● 「長い式」がそもそも書きづらい ○ PEP8:1行79字以内 ○ 改行に特別な記法が必要 ■ 改行したところでバックスラッシュ ■ 全体をカッコで囲う

Slide 18

Slide 18 text

「長い式を書かせない」のがPython ……と考えたらどうか

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

式を区切り、名付ける 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) ● 冗長だが、ぎこちなさはない(?) ● それぞれの関数が「何をやったか」、注釈を付けることができる

Slide 21

Slide 21 text

アンチパターン その2: map/filterを使う

Slide 22

Slide 22 text

map/filter:関数型コレクション処理 heights = list(map(lambda x: x.height, students)) even = list(filter(lambda x: x % 2 == 0, range(40))) ● 関数型コレクション処理の代表的関数 ○ Python以外の言語で関数型っぽく書くときよく使う ○ Python関係の文書でも関数型っぽい関数として紹介されることがある ■ 『Python ハッカーガイドブック』(2020邦訳版)

Slide 23

Slide 23 text

Pythonicじゃない(断言)

Slide 24

Slide 24 text

対処法: 内包表記を使う

Slide 25

Slide 25 text

内包表記に書き換え 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] ● 状態変化はないため、依然関数型といっていい

Slide 26

Slide 26 text

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で消したがってた

Slide 27

Slide 27 text

郷に入ったら郷に従えってことで

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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)))

Slide 30

Slide 30 text

アンチパターン その3: 内包/reduceを乱用する

Slide 31

Slide 31 text

内包、楽しくなりがち

Slide 32

Slide 32 text

内包、楽しくなってくる # 必修の教科書の値段を合計する # 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) ) ● ちょっとやりすぎ

Slide 33

Slide 33 text

対処法: ループを書き、隠蔽する

Slide 34

Slide 34 text

ループを書いて、関数に状態変化を隠蔽 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章

Slide 35

Slide 35 text

reduceも楽しくなりがち

Slide 36

Slide 36 text

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に並ぶ 関数型代表選手

Slide 37

Slide 37 text

複数の関数を途中経過をロギングしつつ連続適用 fs = [math_filter, tall_filter, grade_map] x = students results = [x] for f in fs: x = f(x) results.append(x)

Slide 38

Slide 38 text

reduce、楽しくなってくる results = reduce(lambda x, f: [*x, f(x[-1])], fs, [students]) ● 短くはなったが、微妙…… ○ 慣れてないとかなり読みづらい

Slide 39

Slide 39 text

ループを書き、隠蔽する def logging_seq_apply(x, fs): results = [x] for f in fs: x = f(x) results.append(x) return results ● 慣れてなくても読める ● これでも実質的には関数型。

Slide 40

Slide 40 text

reduce vs ループ ● Guidoはreduceアンチ ○ https://www.artima.com/weblogs/viewpost.jsp?thread=98196 ○ 組み込み→標準ライブラリに格下げ ● forがPythonic、と考えた方が統一的なスタイルなんじゃない? ○ map/filter < 内包 ○ そもそも関数型じゃないPythonコードが世の中にたくさんある ● 情報を圧縮するより、冗長でも明示化、がPythonicなんじゃない? ○ 長い式 < 式を区切り、名付ける ○ explicit is better than implicit ○ 強制インデント

Slide 41

Slide 41 text

相当単純なreduce以外は、 ループの方がいいのでは

Slide 42

Slide 42 text

最初の例題再掲 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)))))

Slide 43

Slide 43 text

書き換え案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)

Slide 44

Slide 44 text

書き換え案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) ● ループを関数に括り出した だけ ● それでも関数型!

Slide 45

Slide 45 text

まとめ

Slide 46

Slide 46 text

紹介したアンチパターンまとめ ● 長い式を連ねる ○ →式を区切り、名付ける ● map/filterを使う ○ →内包表記を使う ● 内包/reduceを乱用する ○ →ループを書き、隠蔽する

Slide 47

Slide 47 text

根拠にした原則 ● Pythonicさについて ○ 長い式を書かせないのがPythonic ○ 圧縮するより、冗長でも明示化がPythonic ○ できるだけforで書くのがPythonic ● 関数型について ○ たとえ状態変化があっても、関数に隠蔽できれば実質関数型

Slide 48

Slide 48 text

絶対的なものではないので チーム内で統一が取れれば 別の方針でもいいと思います

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

いろんな内包 # 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)

Slide 52

Slide 52 text

オレオレ内包 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)

Slide 53

Slide 53 text

アンチパターン その4: クロージャを濫用する & 対処法: オブジェクトと使い分ける

Slide 54

Slide 54 text

クロージャ+高階関数 def major_is(major): # 下がクロージャ def _ret(student): return student.major is major return _ret ● ランタイムに処理を生成することができる

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

クロージャとオブジェクトの使い分け ● クロージャの方がずっとすっきり書ける ● オブジェクトに慣れてる人の方がチームに多ければオブジェクトを使う理由 になる ○ Pythonic、とまでは言えないか…… ● 状態変化を含む場合はオブジェクトを使うべき ○ 関数が状態変化を起こすのは予想しづらい

Slide 57

Slide 57 text

極めて単純なケース以外は オブジェクトの方が いいのでは?