Python難読化のススメ

この記事はFUN advent calendar 2019 の8日目の記事です。
昨日の記事はこたくんによる[macOSでもスープが飲みたい！](https://qiita.com/kotambourine/items/4503718f2690af136892)です。

## おわび
深層学習の記事を書く予定でしたが、エラーを取れませんでした。学習が成功したらもう一本記事書きます。

## はじめに

Pythonはなるべく可読性が保証されるように設計された言語と言われています。例えば、他の多くの言語がコードを中括弧で区切るのに対して、Pythonはインデントで区切ります。これによって絶対にインデントが保たれたコードが出来上がります。人によって中括弧を置く場所の流儀が違う、なんてことは起こりません。また、標準ライブラリ、サードパーティライブラリがともに豊富で、様々な役割をこなせます。深層学習、スクレイピング、サーバーサイド、Webアプリなど、他にも色々なことができます。しかし、Pythonを使ってもコードに個人差は出てしまいます。今回はFizzBuzzを例にあげて、実際に見ていきます。


## FizzBuzz

ある正整数$n$が与えられ、1から$n$までの整数$x$を1ずつ増分し、$x$が3で割り切れたら`Fizz`、5で割り切れたら`Buzz`、それ以外の場合は$x$を出力するという問題。$x$が15で割り切れるときは`FizzBuzz`と出力する。

## 素直に書くと

```Python
n = int(input())
for x in range(1,n+1):
    if x%15==0:
        print('FizzBuzz')
    elif x%3==0:
        print('Fizz')
    elif x%5==0:
        print('Buzz')
    else:
        print(str(x))
```
入出力の結果はおそらくこうなります。

```
30 #input
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
```
## 三項演算子とジェネレータ式
シンプルなif文で書くことができるなら三項演算子でまとめることができます。

```Python
print('\n'.join('FizzBuzz' if i%15==0 else 'Fizz'
    if i% 3==0 else 'Buzz' if i% 5==0 else str(i) for i in range(1,int(input())+1)))
```

一行で書くことができました。(説明のため改行)
まずは条件式の部分に着目します。

```Python
'FizzBuzz' if i%15==0 else 'Fizz' if i%3==0 else 'Buzz' if i%5==0 else str(i)
```
Pythonの三項演算子は`<Trueのときの値> if 条件式 else <Falseのときの値>`という構文です。`<Falseのときの値>`にもう一度三項演算子を置くことで、三項演算子をネストすることができます。(上の例では3重ネスト) 条件式を何にすればいいかわかったところで、ジェネレータ式の説明です。ジェネレータ式の構文は`(<条件式> for アイテム in イテラブル)`で、ジェネレータオブジェクトを返します。Pythonを触ったことがある人だとリスト内包表記とよく似ていることがわかります。配列や集合などの要素を表現する方法に、外延表記と内包表記があります。外延表記は要素を全列挙する方法で、内包表記は条件を満たす要素を選択する方法です。リスト内包表記の構文は`[<条件式> for アイテム in イテラブル]`です。例は以下の通り。

```Python
#外延表記
ls = [0,2,4,6,8]
#内包表記
ls = [2*i for i in range(5)]
ls = [i for i in range(0,10,2)]
ls = [i for i in range(10) if i%2==0]
```

ジェネレータ式とリスト内包表記の構文上の違いは`()`を使うか`[]`を使うかの違いです。しかし、中身はかなり違います。リスト内包表記はあくまでリストですから、要素をすべて持っています。だから、添字を指定して要素を呼ぶことができます。しかし、ジェネレータ式は必要になったときに必要な分だけ中身が作られます。これは遅延評価の一種です。ジェネレータ式はリストとは違い、常に要素を持つことはありません。そのため、メモリをほとんど専有しません。Pythonの公式の見解は、ジェネレータ式が使える状況ならなるべくそちらを使いましょう。ということなので、それに従いました。`.join()`は`str.join(イテラブル)`という形で、イテラブル中の文字列を`str`で結合した文字列を返します。これで先程のFizzBuzzを読めるようになりました。

```Python
print('\n'.join('FizzBuzz' if i%15==0 else 'Fizz'
    if i% 3==0 else 'Buzz' if i% 5==0 else str(i) for i in range(1,int(input())+1)))
```

## 最短コード(だったもの)
FizzBuzzはこうも書けます。(僕が知る中で最短のコード→コメント欄参照)　最初に比べるとだいぶ短くなっていますが、すぐに読めるようになります。

```Python 
print(*(n%3//2*'Fizz'+n%5//4*'Buzz'or-~n for n in range(int(input()))),sep='\n')
```

もちろん入出力の結果は同じです。ぜひ試してみてください。
では、どうなっているかを解説していきます。全体の構造としては、nが3の倍数ならFizz、5の倍数ならBuzz、それ以外ならn+1を要素に持つジェネレータオブジェクトを生成し、要素を展開して、改行で区切って出力。ということをしています。何言ってるか意味不明ですね。1つずつ解説していきます。`n%3//2`はnが3の倍数－1のときに、1になります。(下図参照) Pythonのrangeオブジェクトはデフォルトでは0から始まるので、0,1,2,…とループが回っていきます。(ちなみにrangeオブジェクトも遅延評価)

|n|n%3|n%3//2|n%3//2*'Fizz'|
|:-:|:-:|:-:|:-:|
|0|0|0| |
|1|1|0| |
|2|2|1|Fizz|
|3|0|0| |
|4|1|0| |
|5|2|1|Fizz|
中身としては上の表の様になります。Pythonでは、int型\*str型はstr型変数がint型個並んだものが返ってきます。
n%5//4も同様にして、5の倍数－1のときに1になり、`Buzz`が返ってきます。また、15の倍数－1のときにそれぞれの文字列が＋で結合され、`FizzBuzz`が返ってきます。この文脈での`or`は感覚的にはUNIX/Linuxシェルの`||`に近いです。Pythonでは`<条件式1> or <条件式2>` とある場合、条件式1がTrueであれば、条件式2は実行されず、条件式1の値のみが返ってきます。条件式1がTrueになったことで、`True or <条件式2>` となり、orは成立するからです。同様に、条件式1がFlaseの場合は、False or 条件式2となり、条件式2を実行するまで、結果がTrueになるかわからないため、条件式2を実行します。これは論理演算における短絡評価の一種とみなしていいと思います。Pythonにおいて、`bool(空文字列)` は `False` ですから、nが3の倍数－1でも5の倍数－1でもないときは、$-\verb#~#n$が実行されます。$-\verb#~#n$は簡単に言うとnをビット反転させて、符号を反転させています。

|$n_{(10)}$|$n_{(2)}$|$\verb#~#~n_{(2)}$|$\verb#~#n_{(10)}$|$-\verb#~#n_{(10)}$|
|:-:|:-:|:-:|:-:|:-:|
|0|0|1|-1|1|
|1|01|10|-2|2|
|2|010|101|-3|3|
|3|011|100|-4|4|
|4|0100|1011|-5|5|
|5|0101|1010|-6|6|
\~演算は論理否定演算子で、いわゆる符号付き整数に対する1の補数表現です。これを符号反転するとn+1と同じ結果が得られます。これで条件式は完成です。ジェネレータ式の前についている`*`はイテラブルの前につけてそのイテラブルをアンパック、つまり分解します。言葉を変えれば要素の全展開です。`sep=str`はprint関数のオプショナル引数で、print関数に複数の値が渡されたとき、間を何で埋めるかを表しています。デフォルトでは`sep=''`が渡されています。先程使った`.join()`と何が違うかというと、`.join()`は要素の中身が文字列に限定されているのに対し、こちらは文字列と数字が混在していても問題ありません。(なんで？) 最後のFizzBuzzもこれで終わりです。呆気なかったですね。

## おわりに
今回は拙筆を読んでいただきありがとうございます。今回はFizzBuzzで遊んでみました。難読化されたPythonを読むという名のPythonの言語仕様の紹介でした。記事内の誤謬、まだ短くできるぞなどの指摘、自分の言語ではこう書くぞ。などなど何でもいいのでコメントお待ちしております。

明日は`kiefer7126`さんによるなんか書くです。