きっかけ
ある理由で FizzBuzz を作成していたのですが、ふと「FizzBuzz ってできるだけ短く書こうとするとどうなるんだろう」と思いました。
巷にはありふれた記事かもしれませんが、自分の勉強のためにも記述していきます。
環境
Python 3.8.2
FizzBuzz のルール
- 1~100 までの数字を判定する
- 3 で割り切れる数字の場合、
Fizz
と表示する - 5 で割り切れる数字の場合、
Buzz
と表示する - 3 と 5 で割り切れる数字の場合、
FizzBuzz
と表示する - 上記以外の数字の場合、そのままの数字(String型)を表示する
- 3 で割り切れる数字の場合、
- ターミナル上で結果を確認し、それぞれの数字・文字列は改行して表示する
- PEP8 にできるだけ則る
※ 注意:本記事では上記のルールにのっとっているため、文字数が最小とならない部分がありますが、温かく見守ってください。
その 1 (for 文)
for 文を用いた FizzBuzz、一般的にはこれでしょう。
- 文字数:212 文字
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 文字
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 文のほう遅いようです。
while 文は FizzBuzz などの単純なソースではなく、別のところで活躍していただきましょう(丸投げ)。
その 3 (三項演算子)
三項演算子を用いた FizzBuzz、一文で書けそうでしたが長くなりすぎたのでコロンで改行しました。
- 文字数:126
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)
コメント
三項演算子を用いることで、式がだいぶすっきりしました。
この調子でどんどん短くしていきましょう。
補足:三項演算子について
三項演算子とは、if~else 文のような条件分岐を 1 文で記述したいときによく使用される演算子です。 以下の if~else 文は三項演算子を用いて次のように記述できます。補足なので折りたたみ。
(条件がTrueのときの値) if (条件) else (条件がFalseのときの値)
# if~else文
if number == 0:
print("number is 0")
else:
print("number is NOT 0")
# 三項演算子
print("number is 0" if number == 0 else "number is NOT 0")
その 4 (三項演算子、ジェネレータ式)
三項演算子とジェネレータ式を利用した FizzBuzz、無理やり 1 行表示することができます。
- 文字数:130 文字(80 文字を超えているので、実際は改行が必要ですね)
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 文ですっきり記述することができました。
次ではこの記述をさらに短くしてみましょう。
補足:ジェネレータ式
大まかな説明として、ジェネレータ式を説明するために、まずはリスト内包表記について説明します。 ※ ジェネレータ式について詳細を把握したい方はお手数ですが、 リスト内包表記は Python 独特の記述であり、以下のようになります。 リスト内包表記と、 以上を踏まえ、ジェネレータ式に進みましょう。 簡単に説明するとリスト内包表記の[]内のことで、括弧内で関数を作ってこれを利用し、処理を行おうというものです。 ジェネレータ式がどのような結果を返すのか確認してみます。 上記出力結果は「リストを返す関数みたいなもの」を返しているため、このままでは人間は理解できません。 ※ リスト内包表記ではジェネレータ式の返り値をリスト[]で受け取ることで、その中身を人間がわかるようにしていました。 しかし、ジェネレータ式もリスト内包表記のように 今回の FizzBuzz ではリスト内包表記よりもジェネレータ式を用いたほうが文字数が少なるなるため、こちらを採用して表記しました。補足なので折りたたみ。
リスト内包表記
、ジェネレータ
などでググってみてください。
リスト内包表記
print([i for i in range(0,10)]) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
区切り文字.join([リスト])
を使用することで、通常の for 文を利用したような結果を得ることができます。print("\n".join([i for i in range(0,10)]))
ジェネレータ式
print(i for i in range(0,10)) # <generator object <genexpr> at 0x000001C52B998BA0>
区切り文字.join([リスト])
を利用することで、通常の for 文を利用したような結果を得ることができます。
その 5 (ジェネレータ式、文字列操作、短絡評価)
ジェネレータ式と短絡評価(一部文字列操作)を利用した FizzBuzz、かなり短くなりました。
- 文字列:89 文字
print("\n".join("Fizz"*(i % 3 == 0)+"Buzz"*(i % 5 == 0)or str(i) for i in range(1, 101)))
コメント
とても短くなり、100 文字を下回りました。
自分がたどり着いた最短の FizzBuzz はこちらでした。
しかし、調べてみるとさらに短い FizzBuzz(世界最短)の記述がありましたので、次でそれを紹介します。
補足:文字列操作
今回の記述の場合、 Python において bool 型は int 型のサブクラスであり、bool 型の また、文字列を繰り返す方法として ここで つまり、判定する数字が 3 の倍数であれば boolean が真となり、補足なので折りたたみ。
"Fizz"*(i % 3 == 0)
や"Buzz"*(i % 5 == 0)
などにおいて、文字列 * 真偽値
の記述を使いました。True
とFalse
はそれぞれ0
と1
と等価です。文字列 * 数字(int)
があります。ABC * 3
とすれば、ABCABCABC
となり、ABC * 0
とすれば空文字""
となります。Fizz * 1 => Fizz
が表示され、15 の倍数であれば、"Fizz" * 1 + "Buzz" * 1 => "Fizz" + Buzz => "FizzBuzz"
となるわけです。
補足:短絡評価
短絡評価については次のサイトがとてもよくまとまっていました(自分も参考にさせていただきました)。 簡単に説明すると、以下のように and と or の返り値は決まったアルゴリズムに従うということです。 ここで、x(真)と記述しているのは、 ちなみに、x や y が偽である場合は次の場合です。 上記以外は、すべて真とみなされます。 つまり、今回の場合 ※ ""内が空でないため"Fizz"は真とみなされ、 ※ ""のため false とみなされ、補足なので折りたたみ。
x自身が真の場合
ということを記述しています。 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
("Fizz"*(i % 3 == 0)+"Buzz"*(i % 5 == 0)or i
の式において、以下のように計算されて FizzBuzz が成立する。
"Fizz"*(i % 3 == 0)+"Buzz"*(i % 5 == 0)or i
=> "Fizz" + "" or i
=> "Fizz" or i
=> "Fizz"
x(真) or y(真) => x
のアルゴリズムから、"Fizz"が返る
"Fizz"*(i % 3 == 0)+"Buzz"*(i % 5 == 0)or i
=> "" or i
=> i
x(偽) or y(真) => y
のアルゴリズムから、iが返る
その 6 :世界最短記述の FizzBuzz (for 文、短絡評価、ビット反転)
世界最短記述となる FizzBuzz、ここでは本ルールに則り少し記述を変えていますが、驚きの短さです!
- 文字数:64 文字
for i in range(100):print(i % 3//2*"Fizz"+i % 5//4*"Buzz"or -~i)
コメント
短いです。自分はこの記述に気づきませんでしたが、とてもシンプルです(見た目上、for 文と print 文のみ)。
使っている構文・技術としては、for 文、短絡評価、切り捨て除算、ビット反転でした。
補足:切り捨て除算
自分が初見時びびったので、一応補足です。 そのため、for 文は これと、短絡評価を用いて分全体を短くしているようです。補足なので折りたたみ。
i%3//2
とすることで、i が 0、1 の場合は 0 を返し、2 の場合は 1 を返します。range(100)
となっているのですね。
補足:ビット反転
Python の場合、単純にビットを反転した値ではなく、 そのため、補足なので折りたたみ。
~
演算子によるビット反転を使用していました。~x
は-(x+1)
となるそうです。
-~x
と記述すれば-(-(x+1))
、つまり(x+1)
ということになり、for 文の範囲がrange(100)
となっているようでした。
まとめ
今回でジェネレータ式、短絡評価、ビット反転とかを再確認するいい機会になりました。
FizzBuzz は奥が深いですね!
次回はFizzBuzzの文字数が少なくなったら実行速度が変わるのか調べてみたいですね。