Python の FizzBuzz を少しずつ短くしてみた

## きっかけ

ある理由で FizzBuzz を作成していたのですが、ふと「FizzBuzz ってできるだけ短く書こうとするとどうなるんだろう」と思いました。

巷にはありふれた記事かもしれませんが、自分の勉強のためにも記述していきます。

## 環境

Python 3.8.2

## FizzBuzz のルール

- 1~100 までの数字を判定する
  - 3 で割り切れる数字の場合、`Fizz`と表示する
  - 5 で割り切れる数字の場合、`Buzz`と表示する
  - 3 と 5 で割り切れる数字の場合、`FizzBuzz`と表示する
  - 上記以外の数字の場合、そのままの数字(String型)を表示する
- ターミナル上で結果を確認し、それぞれの数字・文字列は改行して表示する
- PEP8 にできるだけ則る

※ 注意：本記事では上記のルールにのっとっているため、文字数が最小とならない部分がありますが、温かく見守ってください。

## その 1 (for 文)

for 文を用いた FizzBuzz、一般的にはこれでしょう。

- 文字数：212 文字

```python
for i in range(1, 101):
    if ((i % 3 == 0) & (i % 5 == 0)):
        print("FizzBuzz")
    elif (i % 5 == 0):
        print("Buzz")
    elif (i % 3 == 0):
        print("Fizz")
    else:
        print(i)
```

### コメント

Pythonらしく、とても見やすいですね。

## その 2 (while 文)

while 文を用いた FizzBuzz、for 文 ver.を書いたので while 文 ver.も記述してみました。

- 文字数：223 文字

```python
i = 1
while(i < 101):
    if ((i % 3 == 0) & (i % 5 == 0)):
        print("FizzBuzz")
    elif (i % 5 == 0):
        print("Buzz")
    elif (i % 3 == 0):
        print("Fizz")
    else:
        print(i)
    i += 1
```

### コメント

for 文 ver.よりも文字数が 11 文字多くなってしまいました。

蛇足ですが、上記のような単純な繰り返し操作の場合、for 文と while 文の実行速度を単純に比較すると while 文のほう遅いようです。

- [参考： Python を速くしたいときにやったこと＞ 1.while 文はやめておけ！！！](https://qiita.com/shaka/items/f180ae4dc945dc7b9066#1while%E6%96%87%E3%81%AF%E3%82%84%E3%82%81%E3%81%A6%E3%81%8A%E3%81%91)

while 文は FizzBuzz などの単純なソースではなく、別のところで活躍していただきましょう(丸投げ)。

## その 3 (三項演算子)

三項演算子を用いた FizzBuzz、一文で書けそうでしたが長くなりすぎたのでコロンで改行しました。

- 文字数：126

```python
for i in range(1, 101):
    print("FizzBuzz" if (i % 15 == 0) else "Buzz" if (i % 5 == 0) else "Fizz" if (i % 3 == 0) else i)
```

### コメント

三項演算子を用いることで、式がだいぶすっきりしました。

この調子でどんどん短くしていきましょう。

### 補足：三項演算子について

<details><summary>補足なので折りたたみ。</summary><div>

三項演算子とは、if~else 文のような条件分岐を 1 文で記述したいときによく使用される演算子です。

```text
(条件がTrueのときの値) if (条件) else (条件がFalseのときの値)
```

以下の if~else 文は三項演算子を用いて次のように記述できます。

```python
# if~else文
if number == 0:
    print("number is 0")
else:
    print("number is NOT 0")
```

```python
# 三項演算子
print("number is 0" if number == 0 else "number is NOT 0")
```

</div></details>

## その 4 (三項演算子、ジェネレータ式)

三項演算子とジェネレータ式を利用した FizzBuzz、無理やり 1 行表示することができます。

- 文字数：130 文字(80 文字を超えているので、実際は改行が必要ですね)

```python
print("\n".join("FizzBuzz" if i % 15 == 0 else "Buzz" if i % 5 == 0 else "Fizz" if i % 3 == 0 else str(i) for i in range(1, 101)))
```

### コメント

先ほどの三項演算子と組み合わせて利用することで、1 文ですっきり記述することができました。

次ではこの記述をさらに短くしてみましょう。

### 補足：ジェネレータ式

<details><summary>補足なので折りたたみ。</summary><div>

大まかな説明として、ジェネレータ式を説明するために、まずはリスト内包表記について説明します。

※ ジェネレータ式について詳細を把握したい方はお手数ですが、`リスト内包表記`、`ジェネレータ`などでググってみてください。

#### リスト内包表記

リスト内包表記は Python 独特の記述であり、以下のようになります。

```python
print([i for i in range(0,10)])  # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
```

リスト内包表記と、`区切り文字.join([リスト])`を使用することで、通常の for 文を利用したような結果を得ることができます。

```python
print("\n".join([i for i in range(0,10)]))
```

以上を踏まえ、ジェネレータ式に進みましょう。

#### ジェネレータ式

簡単に説明するとリスト内包表記の[]内のことで、括弧内で関数を作ってこれを利用し、処理を行おうというものです。

ジェネレータ式がどのような結果を返すのか確認してみます。

```python
print(i for i in range(0,10))  # <generator object <genexpr> at 0x000001C52B998BA0>
```

上記出力結果は「リストを返す関数みたいなもの」を返しているため、このままでは人間は理解できません。

※ リスト内包表記ではジェネレータ式の返り値をリスト[]で受け取ることで、その中身を人間がわかるようにしていました。

しかし、ジェネレータ式もリスト内包表記のように`区切り文字.join([リスト])`を利用することで、通常の for 文を利用したような結果を得ることができます。

今回の FizzBuzz ではリスト内包表記よりもジェネレータ式を用いたほうが文字数が少なるなるため、こちらを採用して表記しました。

</div></details>

## その 5 (ジェネレータ式、文字列操作、短絡評価)

ジェネレータ式と短絡評価(一部文字列操作)を利用した FizzBuzz、かなり短くなりました。

- 文字列：89 文字

```python
print("\n".join("Fizz"*(i % 3 == 0)+"Buzz"*(i % 5 == 0)or str(i) for i in range(1, 101)))
```

### コメント

とても短くなり、100 文字を下回りました。

自分がたどり着いた最短の FizzBuzz はこちらでした。

しかし、調べてみるとさらに短い FizzBuzz(世界最短)の記述がありましたので、次でそれを紹介します。

### 補足：文字列操作

<details><summary>補足なので折りたたみ。</summary><div>

今回の記述の場合、`"Fizz"*(i % 3 == 0)`や`"Buzz"*(i % 5 == 0)`などにおいて、`文字列 * 真偽値`の記述を使いました。

Python において bool 型は int 型のサブクラスであり、bool 型の`True`と`False`はそれぞれ`0`と`1`と等価です。

- [参考：issubclass(bool, int)](https://github.com/nkmk/python-snippets/blob/7a8eda36e5701c408b4399f669d7e49e89351e38/notebook/bool_example.py#L7)
- [参考：print(True == 1)](https://github.com/nkmk/python-snippets/blob/7a8eda36e5701c408b4399f669d7e49e89351e38/notebook/bool_example.py#L10)
- [参考：print(False == 0)](https://github.com/nkmk/python-snippets/blob/7a8eda36e5701c408b4399f669d7e49e89351e38/notebook/bool_example.py#L13)

また、文字列を繰り返す方法として`文字列 * 数字(int)`があります。

ここで`ABC * 3`とすれば、`ABCABCABC`となり、`ABC * 0`とすれば空文字`""`となります。

つまり、判定する数字が 3 の倍数であれば boolean が真となり、`Fizz * 1 => Fizz`が表示され、15 の倍数であれば、`"Fizz" * 1 + "Buzz" * 1 => "Fizz" + Buzz => "FizzBuzz"`となるわけです。

</div></details>

### 補足：短絡評価

<details><summary>補足なので折りたたみ。</summary><div>

短絡評価については次のサイトがとてもよくまとまっていました(自分も参考にさせていただきました)。

> [参考：Python の論理演算子 and, or, not（論理積、論理和、否定）](https://note.nkmk.me/python-boolean-operation/)

簡単に説明すると、以下のように and と or の返り値は決まったアルゴリズムに従うということです。

ここで、x(真)と記述しているのは、`x自身が真の場合`ということを記述しています。

```python
    x(真) and y(偽) => y
    x(偽) and y(真) => x
    x(真) or y(偽) => x
    x(偽) or y(真) => y
    x(真) and y(真) => y
    x(偽) and y(偽) => x
    x(真) or y(真) => x
    x(偽) or y(偽) => y
```

ちなみに、x や y が偽である場合は次の場合です。

- bool 型の False
- None
- 数値（int 型や float 型）の 0, 0.0
- 空の文字列''
- 空のコンテナ（リスト、タプル、辞書など）[], (), {},

上記以外は、すべて真とみなされます。

つまり、今回の場合`("Fizz"*(i % 3 == 0)+"Buzz"*(i % 5 == 0)or i`の式において、以下のように計算されて FizzBuzz が成立する。

- 判定する数字が 3 の倍数の場合

  `"Fizz"*(i % 3 == 0)+"Buzz"*(i % 5 == 0)or i` => `"Fizz" + "" or i` => `"Fizz" or i` => `"Fizz"`

  ※ ""内が空でないため"Fizz"は真とみなされ、`x(真) or y(真) => x`のアルゴリズムから、"Fizz"が返る

- 判定する数字が 3、5、15 の倍数以外の場合

  `"Fizz"*(i % 3 == 0)+"Buzz"*(i % 5 == 0)or i` => `"" or i` => `i`

  ※ ""のため false とみなされ、`x(偽) or y(真) => y`のアルゴリズムから、iが返る

</div></details>

## その 6 ：世界最短記述の FizzBuzz (for 文、短絡評価、ビット反転)

世界最短記述となる FizzBuzz、ここでは本ルールに則り少し記述を変えていますが、驚きの短さです！

> [参考：FizzBuzz を 1byte で実装する > Python3 (59 bytes)](https://qiita.com/ymg_aq/items/b8e5d26035180bc8797e#python3-59-bytes)

- 文字数：64 文字

```python
for i in range(100):print(i % 3//2*"Fizz"+i % 5//4*"Buzz"or -~i)
```

###　コメント

短いです。自分はこの記述に気づきませんでしたが、とてもシンプルです(見た目上、for 文と print 文のみ)。

使っている構文・技術としては、for 文、短絡評価、切り捨て除算、ビット反転でした。

</div></details>

### 補足：切り捨て除算

<details><summary>補足なので折りたたみ。</summary><div>

自分が初見時びびったので、一応補足です。

`i%3//2`とすることで、i が 0、1 の場合は 0 を返し、2 の場合は 1 を返します。

そのため、for 文は`range(100)`となっているのですね。

これと、短絡評価を用いて分全体を短くしているようです。

</div></details>

### 補足：ビット反転

<details><summary>補足なので折りたたみ。</summary><div>

`~`演算子によるビット反転を使用していました。

Python の場合、単純にビットを反転した値ではなく、`~x`は`-(x+1)`となるそうです。

> [参考：単項算術演算とビット単位演算](https://docs.python.org/ja/3/reference/expressions.html#unary-arithmetic-and-bitwise-operations)

そのため、`-~x`と記述すれば`-(-(x+1))`、つまり`(x+1)`ということになり、for 文の範囲が`range(100)`となっているようでした。

</div></details>

## まとめ

今回でジェネレータ式、短絡評価、ビット反転とかを再確認するいい機会になりました。

FizzBuzz は奥が深いですね！

次回はFizzBuzzの文字数が少なくなったら実行速度が変わるのか調べてみたいですね。