NTT データ数理システム の数理計画部でシニアリサーチャーをしている池田です.社内では Python 芸人をしています.
かわいさが足りない
まずは,とりあえず Fizz Buzz を Python で記述してみましょう.
for i in range(1, 101):
if i % 15 == 0:
print('FizzBuzz')
elif i % 3 == 0:
print('Fizz')
elif i % 5 == 0:
print('Buzz')
else:
print(i)
出力:
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
:
何の面白味もありませんね.そして何よりかわいくない…🤔
もっとかわいく!
そこで,かわいく書き換えたものが以下です.
from functools import reduce, partial
def apply(f1):
def _apply(f2):
def wrapper(*args):
f2a = f2(*args)
return f1(*f2a) if isinstance(f2a, tuple) else f1(f2a)
return wrapper
return _apply
@apply(dict.fromkeys)
def make_n2func(n, func): # {0: func, n, func, n*2: func, ..}
return range(0,15,n), func
def calln2func(f):
def wrapper(*args):
n2func = f(*args)
return lambda n: n2func[n%15](n)
return wrapper
@calln2func # @apply(lambda dct: lambda n: dct[n%15](n)) に同じ
@apply(partial(reduce, dict.__or__)) # Python 3.9 or more
@apply(map)
def make_fizzbuzz1(*args):
return args
funcs = str, lambda n: 'Fizz', lambda n: 'Buzz', lambda n: 'FizzBuzz'
fizzbuzz1 = make_fizzbuzz1(make_n2func, (1, 3, 5, 15), funcs)
@apply(map)
@apply(lambda x: (fizzbuzz1, x))
@apply(range)
@apply(lambda x: (1, x+1))
def fizzbuzz(*args):
return args
print(*fizzbuzz(100))
かわいい🥰!!!
Python には デコレータ という構文があり,関数(やクラス)をデコることができます.
今回はデコレータで関数をデコることで Fizz Buzz をかわいくしていきましょう.
デコレータの復習
まず,デコレータの復習をしておきましょう.
add1 = lambda x: x+1
mul2 = lambda x: x*2
def mul2deco(f):
def wrapper(a):
return mul2(f(a))
return wrapper
def add1deco(f):
def wrapper(a):
return add1(f(a))
return wrapper
@mul2deco
@add1deco
def spam(a):
return a
spam(5)
は,
def spam(a):
return a
mul2deco(add1deco(spam))(5)
と(ほぼ)等価です.下から順に適用されるので,この場合 (5+1)*2 = 12
となります.
また,デコレータは単なる関数なので,引数 → 加工する → 値を返す,といった程度のことしかできません.
方針
元のコードだとデコレータと相性が良くないので関数型っぽく書き換えます.
次のように 0 から 14 の数字と,それに適用する関数の辞書 n2func
を用意し,1 から 100 まで map することにします.
fizz = lambda n: 'Fizz'
buzz = lambda n: 'Buzz'
fizzbuzz = lambda n: 'FizzBuzz'
n2func = {0: fizzbuzz, 1: str, 2: str, 3: fizz, 4: str, 5: buzz, 6: fizz, 7: str, 8: str, 9: fizz, 10: buzz, 11: str, 12: fizz, 13: str, 14: str}
print(*map(lambda n: n2func[n%15](n), range(1,101)))
出力:
1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz 16 17 Fizz 19 Buzz Fizz 22 23 Fizz Buzz 26 Fizz 28 29 FizzBuzz 31 32 Fizz 34 Buzz Fizz 37 38 Fizz Buzz 41 Fizz 43 44 FizzBuzz 46 47 Fizz 49 Buzz Fizz 52 53 Fizz Buzz 56 Fizz 58 59 FizzBuzz 61 62 Fizz 64 Buzz Fizz 67 68 Fizz Buzz 71 Fizz 73 74 FizzBuzz 76 77 Fizz 79 Buzz Fizz 82 83 Fizz Buzz 86 Fizz 88 89 FizzBuzz 91 92 Fizz 94 Buzz Fizz 97 98 Fizz Buzz
n2func
をべた書きで用意するのは品がないので,それぞれの倍数ごとに辞書を作成して merge しましょう.
n2func1 = {0: 'str', 1: 'str', 2: 'str', 3: 'str', 4: 'str', 5: 'str', 6: 'str', 7: 'str', 8: 'str', 9: 'str', 10: 'str', 11: 'str', 12: 'str', 13: 'str', 14: 'str'}
n2func3 = {0: 'fizz', 3: 'fizz', 6: 'fizz', 9: 'fizz', 12: 'fizz'}
n2func5 = {0: 'buzz', 5: 'buzz', 10: 'buzz'}
n2func15 = {0: 'fizzbuzz'}
print(n2func1 | n2func3 | n2func5 | n2func15)
出力:
{0: 'fizzbuzz', 1: 'str', 2: 'str', 3: 'fizz', 4: 'str', 5: 'buzz', 6: 'fizz', 7: 'str', 8: 'str', 9: 'fizz', 10: 'buzz', 11: 'str', 12: 'fizz', 13: 'str', 14: 'str'}
これらの辞書を作る関数 make_n2func
を作ります.
def make_n2func(n, func): # {0: func, n: func, n*2: func, ..}
return dict.fromkeys(range(0,15,n), func)
print(make_n2func(1, 'str'))
print(make_n2func(3, 'fizz'))
print(make_n2func(5, 'buzz'))
print(make_n2func(15, 'fizzbuzz'))
出力:
{0: 'str', 1: 'str', 2: 'str', 3: 'str', 4: 'str', 5: 'str', 6: 'str', 7: 'str', 8: 'str', 9: 'str', 10: 'str', 11: 'str', 12: 'str', 13: 'str', 14: 'str'}
{0: 'fizz', 3: 'fizz', 6: 'fizz', 9: 'fizz', 12: 'fizz'}
{0: 'buzz', 5: 'buzz', 10: 'buzz'}
{0: 'fizzbuzz'}
map
関数を用いることで 4 つ一気に作ることができます.
print(*map(make_n2func, [1, 3, 5, 15], ['str', 'fizz', 'buzz', 'fizzbuzz']))
出力:
{0: 'str', 1: 'str', 2: 'str', 3: 'str', 4: 'str', 5: 'str', 6: 'str', 7: 'str', 8: 'str', 9: 'str', 10: 'str', 11: 'str', 12: 'str', 13: 'str', 14: 'str'} {0: 'fizz', 3: 'fizz', 6: 'fizz', 9: 'fizz', 12: 'fizz'} {0: 'buzz', 5: 'buzz', 10: 'buzz'} {0: 'fizzbuzz'}
merge は dict.__or__
で reduce
することでできます.
PEP 584 – Add Union Operators To dict
from functools import reduce
print(reduce(dict.__or__, map(make_n2func, [1, 3, 5, 15], ['str', 'fizz', 'buzz', 'fizzbuzz'])))
出力:
{0: 'fizzbuzz', 1: 'str', 2: 'str', 3: 'fizz', 4: 'str', 5: 'buzz', 6: 'fizz', 7: 'str', 8: 'str', 9: 'fizz', 10: 'buzz', 11: 'str', 12: 'fizz', 13: 'str', 14: 'str'}
ここで一旦,補助関数を用意します.
add1 = lambda x: x+1
mul2 = lambda x: x*2
mul2(add1(5)) # 12
これを 5
→ add1
→ mul2
という流れが分かりやすくなるように,関数を順に合成する関数 compose
を用意します.
def compose(*funcs):
return reduce(lambda f, g: g(*f) if isinstance(f, tuple) else g(f), funcs)
compose(5, add1, mul2) # 12
compose
関数を用いると,先ほどの n2func
作成は次のように表現できます.
from functools import partial
funcs = 'str', 'fizz', 'buzz', 'fizzbuzz'
n2func = compose((make_n2func, [1, 3, 5, 15], funcs),
map,
partial(reduce, dict.__or__)
)
n2func は 0 から 14 までしか対応していないので数値に対して 15 で割った余りを渡す部分と,これを 1 から 100 まで map する部分を書けば OK です.
funcs = str, lambda n: 'Fizz', lambda n: 'Buzz', lambda n: 'FizzBuzz'
fizzbuzz1 = compose((make_n2func, [1, 3, 5, 15], funcs),
map,
partial(reduce, dict.__or__),
lambda dct: lambda n: dct[n%15](n),
)
print(*compose(100,
lambda x: (1, x+1),
range,
lambda x: (fizzbuzz1, x),
map,
))
1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz 16 17 Fizz 19 Buzz Fizz 22 23 Fizz Buzz 26 Fizz 28 29 FizzBuzz 31 32 Fizz 34 Buzz Fizz 37 38 Fizz Buzz 41 Fizz 43 44 FizzBuzz 46 47 Fizz 49 Buzz Fizz 52 53 Fizz Buzz 56 Fizz 58 59 FizzBuzz 61 62 Fizz 64 Buzz Fizz 67 68 Fizz Buzz 71 Fizz 73 74 FizzBuzz 76 77 Fizz 79 Buzz Fizz 82 83 Fizz Buzz 86 Fizz 88 89 FizzBuzz 91 92 Fizz 94 Buzz Fizz 97 98 Fizz Buzz
あとはこれらをデコレータに書き直せば完成です.お疲れ様でした.
みんなも Python をかわいくしよう!