Python をデコってかわいくしよう！

[NTT データ数理システム](http://www.msi.co.jp/) の数理計画部でシニアリサーチャーをしている池田です．社内では Python 芸人をしています．

# かわいさが足りない
まずは，とりあえず [Fizz Buzz](https://ja.wikipedia.org/wiki/Fizz_Buzz) を Python で記述してみましょう．

```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
    ：
    
何の面白味もありませんね．そして何よりかわいくない…🤔

# もっとかわいく！
そこで，かわいく書き換えたものが以下です．

```python
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 には [デコレータ](https://docs.python.org/ja/3/reference/compound_stmts.html#function) という構文があり，関数(やクラス)をデコることができます．
今回はデコレータで関数をデコることで Fizz Buzz をかわいくしていきましょう．

# デコレータの復習
まず，デコレータの復習をしておきましょう．

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

は，

```python
def spam(a):
    return a

mul2deco(add1deco(spam))(5)
```

と(ほぼ)等価です．下から順に適用されるので，この場合 `(5+1)*2 = 12` となります．

また，デコレータは単なる関数なので，引数 → 加工する → 値を返す，といった程度のことしかできません．

# 方針
元のコードだとデコレータと相性が良くないので関数型っぽく書き換えます．

次のように 0 から 14 の数字と，それに適用する関数の辞書 `n2func` を用意し，1 から 100 まで map することにします．

```python
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 しましょう．

```python
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` を作ります．

```python
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 つ一気に作ることができます．

```python
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](https://peps.python.org/pep-0584/)

```python
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'}

ここで一旦，補助関数を用意します．

```python
add1 = lambda x: x+1
mul2 = lambda x: x*2

mul2(add1(5))  # 12
```

これを `5` → `add1` → `mul2` という流れが分かりやすくなるように，関数を順に合成する関数 `compose` を用意します．

```python
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` 作成は次のように表現できます．

```python
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 です．

```python
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 をかわいくしよう！