2020-11-21に更新

[Python]FizzBuzz問題のカッコイイ解答を読み解いてみる

はじめに

現在僕が受講しているPython基礎を習得する講習にて、

「FizzBuzz問題を2通り以上の書き方で書いてみよう!」

という課題がありました。

その模範解答の1つとして講師の方が挙げてくださった解答を目にして、基礎を学習中の身の僕はとても感動を覚えました。

こちらがその解答例。

FizzBuzzのカッコイイ解答

end = 100
for i in range(1, end + 1):
    print('FizzBuzz'[(4 if i % 3 else 0):(4 if i % 5 else 8)] or i)

う、うつくしい…
たった3行で書けるとは。(もっと言うと2行でいけてる)

けど ちょっと何を言っているのかわからない。

悔しいので、この記述を読み解いてみることにしました。
これまでどこかでは見たことのある色んなエッセンスが詰まっていて、読み解けた時には程よい満足感に包まれました。

この記事では、「このスクリプトが何を言っているのかわかる」というところを目指して、解説してみます。
ぜひ一緒に読み解いてみましょう!

※文法の正確な解説は参考リンクとして貼らせていただいて、まずは「意味がわかる」ところを目指します。
※自分も理解が浅い部分があるため、何か間違いあればご指摘お願いします!

FizzBuzz問題

数字を1から順に100までログ出力します。ただし、
・数字が3の倍数の時には数字の代わりに「Fizz」
・数字が5の倍数の時には数字の代わりに「Buzz」
・数字が3の倍数かつ5の倍数の時には代わりに「FizzBuzz」

と出力するようにしてください。

もしこの問題をやったことない方は、まずはこれをスクリプトで書いてみることに挑戦しましょう!
いろんな入門書、アルゴリズムの本で登場します。

今回の記事は、ひとまずこの問題に解答できる、という前提で書いているので、その上で読み進めてみてください。
for文やif文の基本的な使い方ができていれば良いかと思います)

オーソドックスな解答例

一応、FizzBuzz問題のオーソドックスな解答例です。
色々な書き方があるので、この限りではありません。

end = 100
for i in range(1, end + 1):
    if i % 3 == 0 and i % 5 == 0:
        print('FizzBuzz', end='\n')
    elif i % 3 == 0:
        print('Fizz', end=' ')
    elif i % 5 == 0:
        print('Buzz', end=' ')
    else:
        print(i, end=' ')

プチテクニック:

  • print() 関数に endオプションをつけて文字列を指定することで、その文字列(\n改行、\tタブなども可)を表示させて見やすくできます。
  • 「FizzBuzz」が出力される15の倍数ごとに改行することで見やすくなる、という学びがあったため使ってみました。

今回のカッコイイ解答について読み解く

冒頭に書いたカッコイイ解答ですが、3行目がちょっと何言ってるかわからない。

よくわからん3行目のスクリプト

print('FizzBuzz'[(4 if i % 3 else 0):(4 if i % 5 else 8)] or i)

調べながら読み解いていくと、この一文には、

  • (1)文字列のスライス 'hogehoge'[a:b]
  • (2)三項演算子x if 条件式 else y
  • (3)文字列・数値の論理演算x or y

このあたりがポイントとして含まれていそうです。

...
...と、その前に前提知識を。

(0)前提知識:数値・文字列の真偽値

必要な知識として 「数値・文字列の真偽値」 について整理しておきます。

数値を条件式として使った場合には、TrueかFalseの真偽値で返ります。そしてその真偽については、

  • 数値0, 空の文字列の時 => False
  • それ以外の時 => True

となります。if文の条件式などで使うことでコードがシンプルになるので、慣れておきたいですね。

(1)文字列のスライス

先ほどの一文をさらに分解して、わかりそうなところから見ていきましょう!

'FizzBuzz'[(4 if i % 3 else 0):(4 if i % 5 else 8)] 

この部分は、細かい部分は無視して構造だけ書くと、

'FizzBuzz'[a:b]

と言う形になっており、これは 「文字列のスライス」 を適用しています。

今回のFizzBuzzという文字列にスライスを適用することで、

print('FizzBuzz'[0:8]) #① => FizzBuzz
print('FizzBuzz'[0:4]) #② => Fizz 
print('FizzBuzz'[4:8]) #③ => Buzz
print('FizzBuzz'[0:0]) #④ => (何も表示なし)

と上手いこと表現できます。

まず、これが第一のポイント。

(2)三項演算子if-else

次に読解したいのは、'FizzBuzz'[a:b]と書いた時のa,bに当たる

(4 if i % 3 else 0)
(4 if i % 5 else 8)

この記法です。

なんとなく「後置if」という表現を聞いたことがあったのですが、これは 「三項演算子」 と呼ぶらしいです。

使う分には if〜elseを1行で書ける書き方 と抑えておけば良さそうでしょうか。

参考:
三項演算子(Python) - Qiita

これを当てはめると、上の2つの書き方は

(4 if i % 3 else 0)
=> 「i%3の値がTrue(iを3で割った余りの値が0以外、つまり3で割り切れない時)なら4、それ以外(3で割り切れる時)は0」

(4 if i % 5 else 8)
=>「iを5の値がTrue(iを5で割った余りの値が0以外、つまり35で割り切れない時)なら4、それ以外(3で割り切れる時)は8」

と、条件によって変わる数値を表す表現になります。
(0は条件式の中で扱うと、真偽値はFalseになるんでしたね(前提条件))

なるほどわからん。

...
...具体的に今回の問題のFizzBuzzで考えてみると、これまでの話がなんとなく繋がって見えてきます。

iが3の倍数か、5の倍数か、3の倍数かつ5の倍数(15の倍数)かで場合分けができて、

print('FizzBuzz'[0:8]) #①=> FizzBuzz(iが3の倍数かつ5の倍数の時)
print('FizzBuzz'[0:4]) #② => Fizz(iが3の倍数で、5の倍数ではない時)
print('FizzBuzz'[4:8]) #③ => buzz(iが5の倍数で、3の倍数ではない時)
print('FizzBuzz'[0:0]) #④ => (何も表示なし)(iが3の倍数でも5の倍数でもない時)

この4パターンの結果を

'FizzBuzz'[(4 if i % 3 else 0):(4 if i % 5 else 8)]

この1行で表現することができるのです。

ただこれだけだとまだ、Fizz, Buzz, FizzBuzzの文字列は出力できても、数字を出力することができません。

そこで、次のポイントです。

(3)数値・文字列の論理演算

ここまできたらもう一息です。もう一つ見ていない部分がありました。

よくわからん3行目のスクリプト

print('FizzBuzz'[(4 if i % 3 else 0):(4 if i % 5 else 8)] or i)

この最後のor iという表現に謎が残っています。
そこでまた細かい部分を省いて構造だけ書いてみると、

print('FizzBuzz'[a:b] or i)

という構造になっています。

このor「論理演算子」 というやつですが、どうもなんかこの表現の中では直感的にわかるorの使い方と様子が違いそうです。

一般的な論理演算子orの使い方は

条件式 or 条件式 => TrueFalseを返す
例) 1 < 10 or 2 > 20 => True

このように、orで並ぶのは条件式で、1つでも合っていればTrue、どれも間違っていたらFalseが返る、と言う真偽値を返す演算となっています。
これは直感的にわかりやすいですね。

ところが今回の表現は、

文字列・数値 or 文字列・数値 => どちらかの値(文字列か数値)を返す
例) 'FizzBuzz' or 3 => FizzBuzz
例) '' or 3 => 3

という風にorで並んでいる値のどれかを返す演算になっています。上の例では
orの前の値が真・後ろの値が真のとき、全体としては前の値を返す」
orの前の値が偽・後ろの値が真のとき、全体としては後ろの値を返す」
というルールとなっています。

参考:
Python♪論理演算子(and, or, not)の使い方と数値や文字列の論理演算 | Snow Tree in June

なるほどわからん。

...
...細かい文法は今後学んでいくとして、今回は読み解くことを優先して「このようなルールがあるんだなー」と流すことにします。
(「短絡評価(ショートサーキット)」という評価法?が使われています)

具体的に今回の問題に当てはめてみると、(2)で考えた4パターンをさらに以下のように考えることができます。

print('FizzBuzz'[0:8] or i) #① => FizzBuzz(orの前の値が真・後ろの値が真の時、前の値を返す)
print('FizzBuzz'[0:4] or i) #② => Fizz(上と同様)
print('FizzBuzz'[4:8] or i) #③ => buzz(上と同様)
print('FizzBuzz'[0:0] or i) #④ => i(orの前の値が偽・後ろの値が真のとき、後ろの値を返す)

①〜③の出力結果は(2)の時と同じですが、④だけ出力結果が変わっています。

'FizzBuzz'[(4 if i % 3 else 0):(4 if i % 5 else 8)] or i

orを使うだけで、1行で数値を出力することが可能になったのです。

おわりに

楽しかった。以上です。
ここまで読んでみて、改めて冒頭のカッコイイFizzBuzz解答をのスクリプトを実行してみると、少し見え方が違うのではないでしょうか。

なかなか直感的に説明できないのがもどかしく感じたので、精進せねばと思いました。
今回見てきた文法は一つ一つは結構目にする書き方かと思うので、こういうのを使いこなして、自分もpythonでシンプルなスクリプトを書けるようになりたいものです。

FizzBuzz面白いですね。
また新しい概念を学んだら、あえてそれを生かしたFizzBuzzを作ってみる、と言うのも練習として面白そうだなあと思いました。

Originally published at qiita.com
ツイッターでシェア
みんなに共有、忘れないようにメモ

Massa

北海道でアプリ制作に取り組んでるノンプログラマな農夫。仕事や日常生活で感じる小さな不便を解消すべく趣味と実益を兼ねて遊んでます ■Python・GAS + LINE bot

Crieitは誰でも投稿できるサービスです。 是非記事の投稿をお願いします。どんな軽い内容でも投稿できます。

また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!

有料記事を販売できるようになりました!

こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください。
ボードとは?

コメント