PythonでリテラルなしIfなしFizzBuzzワンライナー
前回の記事のFizzBuzzコードをちょこっといじってこんな感じで満足していた:
リテラルを使わず簡潔な高可読性FizzBuzzが書けるプログラマです🤥
— zehnpaard (@zehnpaard) April 23, 2021
(f:=lambda *a,A=id,Fizz=id,Buzz=id:len(a) or (k:=sorted(f.__kwdefaults__)) and print(A if (x:=A%f(f,f,f))*(y:=A%f(f,f,f,f,f)) else k[f(f)] if x else k[f(f,f)] if y else k[f(f,f)]+k[f(f)]) or f(A=A+f(f)))(A=f(f)) https://t.co/W5Wph2zgwh
そうしたらこんなツイートが:
ifやswitch/matchを使わないfizzbuzz
— がくぞ (@gakuzzzz) April 25, 2021
え、IfもなしでFizzBuzzを・・・?
出来らあ!
と啖呵を切って考えてみる。まあ前回のコードでも利用していたand/orの特性を使えばifは当然省けるはず。
Pythonだと真偽値以外もTruthinessを持ち、特に空文字列は偽なので("Fizz" if x%3==0 else "")+("Buzz" if x%5==0 else "") or x
のようなロジックを使うのが一番スマート。
もう一つハック的なポイントとして、PythonでTrue
とFalse
はほぼ1
と0
と同等なので("Fizz" * (x%3==0))+("Buzz" * (x%5==0)) or x
と書ける。
これでコア部分のロジックはif
なしでいけそう。
前回の記事でも紹介した通り、"Fizz", "Buzz", 3, 5はリテラルなしで簡単に作れる。しかし0は(無名関数の定義の仕方のせいで)作れなくなっている。f(f)-f(f)
のようにして作ってもいいのだが、長くなるのでイマイチ。
まず考えたのは関数内の式の順番を入れ替えるやり方:
(f:=lambda *a,A=id,Fizz=id,Buzz=id:(A!=id and ((k:=sorted(f.__kwdefaults__)) and print(k[f(f,f)]*(A%f(f,f,f)==f())+k[f(f)]*(A%f(f,f,f,f,f)==f()) or A) or f(A=A+f(f)))) or len(a))(A=f(f)) https://t.co/wAZB2sVvdY
— zehnpaard (@zehnpaard) April 25, 2021
FizzBuzzのプリント・ループ処理に入る条件をA!=id
にして、len(a)
は関数の最後に持ってきた。これでf()
が0になる。
しかし、よく考えるとf()
で0を表現するまでもなく、プリント・ループ処理に入った時点でlen(a)
という0の値をすでに評価していることに気づいた。この値を変数束縛して使ってしまえばいい。
さらに二つ修正点がある:
X if Y else Z
という構文を使っていた時はX
やZ
の部分は評価されないケースがあった。それに対してX + Y or Z
の場合、X
の部分は必ず評価される。なのでX
に(k:=sorted(f.__kwdefaults__))
という変数束縛を持ってこれる- 0が表現できるので仮引数をソートして"Buzz"が先頭に来ても問題ない。ので仮引数に
A
を使う必要がなくなり、"Fizz"と"Buzz"をアクセスするインデックスに1と0を使える(文字数を減らせる)。
これらを踏まえて、とりあえず現在の最終形:
(f:=lambda *a,x=id,Fizz=id,Buzz=id:(z:=len(a)) or print((k:=sorted(f.__kwdefaults__))[f(f)]*(x%f(f,f,f)==z)+k[z]*(x%f(f,f,f,f,f)==z) or x) or f(x=x+f(f)))(x=f(f))
— zehnpaard (@zehnpaard) April 25, 2021
if
なしという制約がつく前に比べて文字数が減ってる・・・ (この記事の冒頭のコードが211文字、最終形のコードが162文字)
制約がついて工夫しないといけなくなるといいアイデアが出る、というケース。
追記:前回の記事に書き忘れたが、walrus operatorことAssignment Expressionsを多用しているのでPython3.8以降が必要。