Python & ビット演算でドドスコしてみた

# 今更ドドスコ

こちらが話題になっていた2022年の夏、コロナに罹ってダウンしていたところ完全に乗り遅れてしまったので今更ながらやってみました。
リプで配列を使った例はよく見かけたので今回はPythonのビット演算で実装してみることにします。

https://twitter.com/Sheeeeepla/status/1554028833942441984?s=20&t=FwU9_mhj2y3M5KvKyD0rQw


# やってみた

コードはこんな感じ。

```python:ddsk.py
import random

buffer = 0

while True:
    buffer = buffer << 1 & 4095

    if random.getrandbits(1):
        buffer |= 1
        print("ドド", end="")
    else:
        print("スコ", end="")

    if buffer == 2184:
        print("ラブ注入♡")
        break
```

言わずもがなな内容ですが、以下を無限ループで処理してます。

- `buffer`をビットシフトし、12桁で固定するため論理演算を行う
- `random.getrandbits(1)`で`0`または`1`を乱数生成してそのまま条件分岐にかける
  - 判定結果が真（つまり`1`）だったら`buffer`の最下位ビットに`1`をセットして「ドド」を標準出力
  - 判定結果が偽（つまり`0`）だったら「スコ」を標準出力（`buffer`の最下位ビットは暗黙で`0`の扱い）
- `buffer`が`2184`（2進数で「100010001000」=「ドドスコスコスコ×3」）ならば「ラブ注入♡」を標準出力してループを抜ける


## 実行結果

根気強く何度も実行して短い結果を取得しました。

```
ドドドドスコドドスコスコスコスコスコドドドドスコスコスコドドスコドドスコスコスコドドドドスコスコスコドドドドドドドドドドスコドドドドスコスコスコスコスコドドスコドドド
ドドドドドスコドドドドスコドドドドスコスコスコスコドドスコドドドドスコスコドドドドドドスコスコスコスコスコスコスコドドドドドドスコスコスコスコスコドドドドドドスコドド
ドドドドドドスコドドドドドドドドスコスコドドドドスコスコドドスコスコスコスコスコスコドドスコドドドドスコドドドドドドドドドドドドスコスコスコドドドドスコスコスコドドス
コドドスコスコドドドドドドスコドドスコスコドドドドスコドドドドドドスコドドスコドドスコドドドドスコドドスコドドスコスコドドドドドドスコドドドドドドスコスコスコスコドド
スコスコスコドドドドスコドドスコドドスコスコドドドドドドスコドドドドスコスコドドドドスコドドスコドドスコスコスコスコドドスコドドスコスコドドスコドドスコドドドドスコス
コスコスコスコドドドドドドドドスコドドドドスコドドスコスコスコスコスコスコスコドドスコドドスコドドスコスコスコスコドドドドスコスコスコドドスコスコスコドドスコスコスコ
ラブ注入♡
```


# 気になるところ

ループの1週目の処理（`buffer`の値が`0`の状態でビットシフトして桁数を制御）が若干汚い感じで気になります。（技量の問題）

あとこれくらいの軽い処理で三項演算子が適用できなかったのも若干ストレスでした。（技量の問題）


# せっかくなのでコードを削ってみる

Pythonでこういう可読性低いコード書くのは好みませんが、折角なので削れるところを削ってみました。

```python:ddsk_short.py
import random
b=0
while 1:
 b=b<<1&4095
 if random.getrandbits(1):
  b|=1
  print("ドド")
 else:
  print("スコ")
 if b==2184:
  print("ラブ注入♡")
  break
```

なおファイルサイズはこんな感じ。

```bash
$ wc -c ddsk_short.py
164 ddsk_short.py
```

仮に`import`文で`as`節を使ったとしても文字数が変わらないところが歯痒いですね。

`print`時の行末の改行も削っているので実行結果は割愛します。（長くなるので）


# 感想

もっと前に同じようなお題でズンドコキヨシがありましたが、3セット繰り返す分ズンドコキヨシよりも実装幅が広かったのでプログラマーの個性が出るお題だなと感じました。

配列との実行速度の比較とかしてみると面白そうです。（**結果が乱数に左右される手前、同条件での比較ができないですが**）

あと「そもそもPythonのビット演算って早いのか？」という疑問が沸いたので今度cpythonのソースコード見て調べてみようと思います。~~たぶん~~。~~やれたらやる~~。


# 追記

## 追記その１：ちゃんと2進数に直した

コードを見返してみたところビット演算なのに数値を10進数で扱ってるのは何か解せなかったので2進数に直しました。

```python:ddsk.py
import random

buffer = 0b0

while True:
    buffer = buffer << 1 & 0b111111111111

    if random.getrandbits(1):
        buffer |= 0b1
        print("ドド", end="")
    else:
        print("スコ", end="")

    if buffer == 0b100010001000:
        print("ラブ注入♡")
        break
```

うん、この方がビット演算してるプログラムっぽくて好きだ。


## 追記その２：無限ループと`break`がまだ絞れる

よく考えたら無限ループ使わない方が短く書けることに気づきました。

```python:ddsk_short.py
import random
b=0
while b!=2184:
 b=b<<1&4095
 if random.getrandbits(1):
  b|=1
  print("ドド")
 else:
  print("スコ")
print("ラブ注入♡")
```

これで20バイト近く削減です。

```bash
$ wc -c ddsk_short.py
147 ddsk_short.py
```


## 追記その３：バイト数は一旦忘れて行数にコミットしてみる

自分の技量的にバイト数削るのは頭打ちな気がしたので別の切り口で遊んでみます。


```python:ddsk.py
import random
buffer=0b0
while buffer!=0b100010001000:
   bit = random.getrandbits(1)
   buffer = buffer << 1 & 0b111111111111 | bit
   print("ドド" if bit else "スコ", end="")
print("ラブ注入♡")
```

`print`を三項演算子使って書いたらすっきりしました。


## 追記その４：そもそもちゃんと元ツイの通りに組めてなかった件

> 【問題】**配列{"ドド","スコ"}から**ランダムに要素を標準出力し続け、『その並びが「ドドスコスコスコ」を3回繰り返したもの』に一致したときに「ラブ注入♡」と標準出力して終了するプログラムを作成せよ(配点:5点)

やっちまった。

配列から取得するというのが完全に抜け落ちていたので修正したのがこちら。

```python:ddsk.py
import random
ddsk = ["ドド", "スコ"]
buffer=0b0
while buffer!=0b011101110111:
   bit = random.getrandbits(1)
   print(ddsk[0 if not bit else 1], end="")
   buffer = buffer << 1 & 0b111111111111 | bit
print("ラブ注入♡")
```

「まず標準出力したうえで並びを検知する」というお題の流れに沿って、バッファの更新を`print`の後ろに動かしました。（~~ここに来てなぜかちょっとだけ可読性を意識~~）


## 追記その５：追記その４がバグってたので修正

`while buffer!=0b011101110111:`は先頭が`0`のため実質2進11桁の判定条件となり、11ループ目の時点で「スコスコスコドドスコスコスコドドスコスコスコドド」が成立するとループを抜けてしまいます。
コメントで指摘くださった @shiracamus さん、ありがとうございました！

こちら先頭のビットを`1`で指定するよう条件のビット列を反転し、`print`時の配列要素の指定で帳尻を合わせました。

そしてコメントで教えていただいたコードを参考に、`buffer`の最下位ビット取得を論理積で取得する形に変更しておきます。

```python:ddsk.py
import random
buffer=0b0
while buffer!=0b100010001000:
    buffer = buffer << 1 & 0b111111111111 | random.getrandbits(1)
    print(["ドド", "スコ"][~buffer & 0b1], end="")
print("ラブ注入♡")
```