PythonでリテラルなしFizzBuzzワンライナー
ツイッターでこういう話が出ていた(@nishio さん、面白いお題をありがとうございます!):
リテラル禁止FizzBuzz大会、開幕!(待て https://t.co/zpIirtNpmO
— nishio hirokazu (@nishio) April 16, 2021
class Fizz:
— zehnpaard (@zehnpaard) April 16, 2021
def Buzz(*a): return len(a)
b=https://t.co/H1SEH2bvbd
x,y,z,fs,bs=b(b),b(b,b,b),b(b,b,b,b,b),Fizz.__name__,b.__name__
while b:
print(x if x%y and x%z else bs if x%y else fs if x%z else fs+bs)
x+=b(b) https://t.co/KnMqQjZf77
数字は「*args
で可変長引数をとって、その長さを返す」関数で表現できる。
b = Fizz.Buzz one = b(b) two = b(b,b) three = b(b,b,b)
といった使い方ができる(ちなみに引数の値自体はなんでもいい)。
文字列はオブジェクトの__name__
を取れば大丈夫。
ガハハ、勝ったな、と思ったら:
というわけで「Pythonで、リテラル禁止、改行禁止、1ツイートに収まるFizzBuzz」という課題は手慣れたPythonistaなら30分で作れることが実証されました https://t.co/qOrxvhMyhJ
— nishio hirokazu (@nishio) April 16, 2021
この縛りだとクラスや関数をclass/def
で定義できなくなってしまう。
ならば・・・
Pythonワンライナー
— zehnpaard (@zehnpaard) April 17, 2021
(g:=lambda x: print(x if (fs:=list(f.__kwdefaults__)[f()]) and (bs:=list(f.__kwdefaults__)[f(f)]) and x%(y:=f(f,f,f)) * x%(z:=f(f,f,f,f,f)) else fs if x%z else bs if x%y else fs + bs) or g(x+f(f))) and (f:=lambda *a,Fizz=g,Buzz=g:len(a)) and g(f(f)) https://t.co/KnMqQjHDIx
私が勝手に「Guidoの置き土産」と呼んでいるWalrus Operator:=
を使えば一つの式の中で変数束縛し放題。「改行なし」をさらに制限して「式一つ」で出来てしまう。
X and Y and Z
はX
とY
がTruthy
な値なら順次に評価していって最終的な値はZ
になる。
lambda
を使った無名関数二つにWalrus Operatorでg
とf
という名前を束縛している。
順番が前後するが、まずf
関数を見てみる。これは必要な数字と文字列を得るための関数で、「*args
で可変長引数をとって、その長さを返す」アイディアはそのまま、そしてlambda *a, Fizz=id, Buzz=id:...
とFizz
、Buzz
をデフォルト値のあるkwargsとして追加している。これでf.__kwdefaults__
で{ "Fizz": ..., "Buzz": ... }
といったdictionaryが入手できて、キーから文字列が得られる。
g
関数はFizzBuzzロジックと再帰によるループを、X if Y else Z
やand/or
、walrus operatorを使いまくって一つの式に詰め込んでいる。
個人的に一番のポイントだったのはx%(y:=f(f,f,f)) * x%(z:=f(f,f,f,f,f))
とx%3が0だった時に右側がショートサーキットで飛ばされないよう、and
ではなく*
にしてあるところ。
なんでf
がg
のあとに出てくるかというと、もともとf:=lambda *a,Fizz=id,Buzz=id:...
と定義していたのだが(組み込み関数の中でid
が一番短い)、g
と定義順を逆にすれば文字数が削れるのと関係ないid
が出てこなくて済む、ということに気づいて順番を入れ替えたから。Pythonでは関数のデフォルト引数は「件数呼び出し時」ではなく「関数定義時」に評価されるので、すでに定義済みの変数しか使えない(例えば再帰的にデフォルト引数をf
にすることもできない)。
これでg(f(f))
すると1からmax recursion depth
に到達する995までの数字のFizzBuzzが表示される。
その後少し修正を加えて、今現在の最終形はこちら:
束縛する名前を減らしてみた
— zehnpaard (@zehnpaard) April 17, 2021
(f:=lambda *a,A=id,Fizz=id,Buzz=id: len(a) if a else print(A if (kw:=sorted(f.__kwdefaults__)) and A%f(f,f,f) and A%f(f,f,f,f,f) else kw[f(f,f)] if A%f(f,f,f,f,f) else kw[f(f)] if A%f(f,f,f) else kw[f(f,f)]+kw[f(f)]) or f(A=A+f(f))) and f(A=f(f))
変更点は以下の通り:
- 単一の無名関数に詰め込んだ(そのため新しい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、問題としてなかなか楽しかったのでもっと流行ってもよかったのになー、と少し寂しい気持ちがある。