LoginSignup
5
2

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-04-29

きっかけ

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

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

環境

Python 3.8.2

FizzBuzz のルール

  • 1~100 までの数字を判定する
    • 3 で割り切れる数字の場合、Fizzと表示する
    • 5 で割り切れる数字の場合、Buzzと表示する
    • 3 と 5 で割り切れる数字の場合、FizzBuzzと表示する
    • 上記以外の数字の場合、そのままの数字(String型)を表示する
  • ターミナル上で結果を確認し、それぞれの数字・文字列は改行して表示する
  • 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 文で記述したいときによく使用される演算子です。

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

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

# 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 独特の記述であり、以下のようになります。

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 文を利用したような結果を得ることができます。

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

その 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(世界最短)の記述がありましたので、次でそれを紹介します。

補足:文字列操作

補足なので折りたたみ。

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

Python において bool 型は int 型のサブクラスであり、bool 型のTrueFalseはそれぞれ01と等価です。

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

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

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

補足:短絡評価

補足なので折りたたみ。

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

参考:Python の論理演算子 and, or, not(論理積、論理和、否定)

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

ここで、x(真)と記述しているのは、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

ちなみに、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が返る

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

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

参考:FizzBuzz を 1byte で実装する > Python3 (59 bytes)

  • 文字数:64 文字
for i in range(100):print(i % 3//2*"Fizz"+i % 5//4*"Buzz"or -~i)

 コメント

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

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

補足:切り捨て除算

補足なので折りたたみ。

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

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

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

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

補足:ビット反転

補足なので折りたたみ。

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

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

参考:単項算術演算とビット単位演算

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

まとめ

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

FizzBuzz は奥が深いですね!

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

5
2
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2