読者です 読者をやめる 読者になる 読者になる

cocuh's note

type(あうとぷっと) -> 駄文

Python黒魔術でfizzbuzzを書こう

Python

なんとなく思い立って、
「今の自分がfizzbuzzをどこまで(技巧的に)かけるか書いてみよう」
のがこれです。


今回の縛りはこちら

  • 無限リスト
  • ↑必然的にジェネレータ(generator)
  • ifはいらない
  • もちろんワンライナー

できたのがこちら

gen = ((x%3<1and'fizz'or'')+(x%5<1and'buzz'or'')or str(x)for x in __import__('itertools').count(1))

動作環境はPython2.7.5です。



詳しい解説

ジェネレータを書こう

ジェネレータ式は下のように書けるためそれを使いました。

gen = (x for x in [1,2,3])
gen.next() # => 1
gen.next() # => 2
gen.next() # => 3
gen.next() # => 例外(StopIteration)

無限リストを作る方法としては、

  • 標準ライブラリ(itertools)を使う方法
  • 自分で作る方法

ここではitertoolsのcountを使います。Ref

import文の罠

Pythonでライブラリをimportするimport文は改行*1が必要です。
よってワンライナーでは使えません。
ここでは組み込み関数の__import__()を使います。Ref
ワンライナーと動的importするときぐらいしか使わないんじゃないかな…__import__()

以下のコードはだいたい*2同じ意味です。

import sys
sys = __import__('sys')

暗黙の型変換について

Pythonの暗黙の型変換は、intは0はFalseほかはTrueになり、strも空でなければTrueです。
コードでは明示してますが、andやor,notと一緒に使うと暗黙的に変換されます。

bool(1)   # => True
bool(0)   # => False

bool("a") # => True
bool("")  # => False

遅延評価使おう

if縛りなので遅延評価を使います

True or "a"  # => True
False or "a" # => "a"

orは片方がTrueであればTrueなので、
最初がTrueだと全体はTrueなので、後ろの部分が評価されません。

True and "a"  # => "a"
False and "a" # => False

andは片方がFalseであればFalseなので、
最初がFalseであれば後ろは評価されません

この遅延評価を利用します。

遅延評価ifの罠

if文は以下のように書けます

(条件) and (True時) or (False時)

暗黙の型変換から0はFalseなので、
「(x%3)が0でない時(True) 空文字列""、0(False)ならば"fizz"」とすればいいです。
よって次のように書けるように見えます

(x%3) and "" or "fizz"

ここで問題なのが、空文字列はFalseに変換されるので、
さっきのコードは以下のように解釈されます。
最初の括弧のなかの比較ですべてFalseになるので、すべて"fizz"になります。

 (x%3) and ""    or "fizz"
((x%3) and False)or True

最後を空文字列にするのは問題がないので、
条件にnotをつけてTrue時とFalse時を入れ替えないといけません。

(not x%3) and "fizz" or ""

notの後ろにスペースが必要で文字数がもったいない(?)ので比較演算子にします

(x%3<1) and "fizz" or ""

buzz側も同じようにします

ちなみに、if・三項演算子で書くと以下のようになりますが、
今回は遅延評価で書いたほうが1文字短いです。(ヤッタネ

x%3<1and"fizz"or""
""if x%3 else"fizz"

記号・数字の前後はスペースがなくても解釈されます。
ちょっと意外だったのが、x%3<1andの部分で1とandの間にスペースがなくてもなぜか解釈されました。""if x%3 else"fizz"の3とelseのスペースは必要でした。
いいことを知った・ω・

残りの条件str(x)

最後は、fizzとbuzzどちらにもならなかった場合だけです。
fizzの処理に引っかかると'fizz',buzz側では'buzz'が返ってきてます。
結合して空文字列(False)であるのが求める条件なので以下のように書けます。

(fizz処理) + (buzz処理) # => False

(fizz処理) + (buzz処理) or str(x)
# 'fizz' + ''     or str(x) => 'fizz'
# ''     + 'buzz' or str(x) => 'buzz'
# ''     + ''     or str(x) => str(x)

まとめ

これらをくっつけて1行にしたのがこれです。

gen = ((x%3<1and'fizz'or'')+(x%5<1and'buzz'or'')or str(x)for x in __import__('itertools').count(1))

ちなみに

今回やったことを綺麗に書いてみるとこんな感じになります。

def generator():
    n = 1
    while True:
        res = ""
        if n%3 == 0: res += "fizz"
        if n%5 == 0: res += "buzz"
        if res == "": res = str(n)
        yield res
        n += 1

Pythonはzen of pythonに基づいているからPythonだと思っているので、
ワンライナーや技巧プログラミングは黒魔術かなと思いタイトルのようにしました。

Python黒魔術でPythonの文法や挙動、知らないライブラリなどが勉強できるので、
興味があればやってみてもいいのかなと思います。
ハマると変な癖がつくのでおすすめはしません。(実話)

Pythonワンライナーに関しては西尾さんの記事が非常に非常に非常に参考になるので、
興味のある方は読んでみるといいと思います。

Pythonワンライナーを作成する際のノウハウ集
http://www.nishiohirokazu.org/blog/2006/08/python_12.html

*1:この改行はセミコロンで代用ができますが、それだと面白くないので…

*2:初期化タイミングやreload()の挙動が微妙に違った記憶が…