Arantium Maestum

プログラミング、囲碁、読書の話題

PythonでリテラルなしFizzBuzzワンライナー

ツイッターでこういう話が出ていた(@nishio さん、面白いお題をありがとうございます!):

リテラルなしでFizzBuzz?出来らぁ!

数字は「*argsで可変長引数をとって、その長さを返す」関数で表現できる。

b = Fizz.Buzz
one = b(b)
two = b(b,b)
three = b(b,b,b)

といった使い方ができる(ちなみに引数の値自体はなんでもいい)。

文字列はオブジェクトの__name__を取れば大丈夫。

ガハハ、勝ったな、と思ったら:

え、リテラルも改行もなしでFizzBuzzを・・・?

この縛りだとクラスや関数をclass/defで定義できなくなってしまう。

ならば・・・

私が勝手に「Guidoの置き土産」と呼んでいるWalrus Operator:=を使えば一つの式の中で変数束縛し放題。「改行なし」をさらに制限して「式一つ」で出来てしまう。

X and Y and ZXYTruthyな値なら順次に評価していって最終的な値はZになる。

lambdaを使った無名関数二つにWalrus Operatorでgfという名前を束縛している。

順番が前後するが、まずf関数を見てみる。これは必要な数字と文字列を得るための関数で、「*argsで可変長引数をとって、その長さを返す」アイディアはそのまま、そしてlambda *a, Fizz=id, Buzz=id:...FizzBuzzをデフォルト値のあるkwargsとして追加している。これでf.__kwdefaults__{ "Fizz": ..., "Buzz": ... }といったdictionaryが入手できて、キーから文字列が得られる。

g関数はFizzBuzzロジックと再帰によるループを、X if Y else Zand/or、walrus operatorを使いまくって一つの式に詰め込んでいる。

個人的に一番のポイントだったのはx%(y:=f(f,f,f)) * x%(z:=f(f,f,f,f,f))とx%3が0だった時に右側がショートサーキットで飛ばされないよう、andではなく*にしてあるところ。

なんでfgのあとに出てくるかというと、もともとf:=lambda *a,Fizz=id,Buzz=id:...と定義していたのだが(組み込み関数の中でidが一番短い)、gと定義順を逆にすれば文字数が削れるのと関係ないidが出てこなくて済む、ということに気づいて順番を入れ替えたから。Pythonでは関数のデフォルト引数は「件数呼び出し時」ではなく「関数定義時」に評価されるので、すでに定義済みの変数しか使えない(例えば再帰的にデフォルト引数をfにすることもできない)。

これでg(f(f))すると1からmax recursion depthに到達する995までの数字のFizzBuzzが表示される。

その後少し修正を加えて、今現在の最終形はこちら:

変更点は以下の通り:

  • 単一の無名関数に詰め込んだ(そのため新しいkwargsであるAを定義している。f(f,f,f)のように引数を普通に入れる場合は整数を返し、f(A=1)のようにすれば先ほどのg関数のようにFizzBuzzループが始まる)。ちなみに可変長引数aの長さを条件にどっちの処理を行うかの分岐をするという実装の関係でf()で0が表現できなくなっている。そのせいで新しいkwargsはFizzとBuzzよりもlexographicに小さい文字列である必要が出てくるので大文字のAにしてある。また無名関数が一つになった関係でプレースホルダーとしてidを使う必要が出てきたのは無念。
  • list(f.__kwdefaults__)だとどの順番で文字列が得られるかが微妙に実装依存な気もする(最近のPythonでは違うとは思うが)のでsorted(f.__kwdefaults__)にして文字列の順番が一意に定まるようにした。
  • walrus operatorによる変数束縛を減らした。束縛されるのはf関数自体とkw:=sorted(f.__kwdefaults___)の二つだけ。前者は(特に再帰のために)絶対必要(いや、やろうと思えばFix operator的なことができるのか?)で、後者はツイートに入れるための文字数制限の関係で必要。

リテラル禁止FizzBuzz、問題としてなかなか楽しかったのでもっと流行ってもよかったのになー、と少し寂しい気持ちがある。