はじめに
ワンライナー、それは果てなき荒野。
その1 市松模様ワンライナー
長年の宿題を解決する
内包表記だったかnumpy.where()
だったか忘れたが、そのあたりを学習中に「これを使えば市松模様をワンライナーで書けるのではないか」と思ったことがあった。別にそうせねばならない切実な理由があるわけではないが、その後もときどき思い出したように挑戦し、そして失敗していった。
先日とうとう成功し、うれしさのあまり裸で街を駆け回りおまわりさんの世話になったのだった。嘘だけど。
import numpy as np
import cv2
imgH, imgW = 240, 320
color1 = (135, 172, 79)
color2 = (34, 37, 41)
u = 50 # 単位となる四角の大きさ
# これ! Pythonの ^ はべき乗ではなくXOR。
img = np.array([[color2 if (x%(2*u) < u) ^ (y%(2*u) < u) else color1 \
for x in range(imgW)] for y in range(imgH)], np.uint8)
cv2.imshow("tanjiro", img)
cv2.waitKey(0)
cv2.destroyAllWindows()
結果 |
---|
内包表記の二重ループの中に三項演算子。うーん、これは美しくない。良い子は真似しないでね。
変数を使わずコード内に数値を埋め込み、cv2.destroyAllWindows()
が必要なcv2.imshow()
でなくcv2.imwrite()
で画像を保存する内容にすればimport
の2行以外は1行にできるが、そのような工夫は無意味だろう。
内包表記に関する別の質問をteratailでしたところ「意味のないコードをこねくりまわすより簡潔な書き方をしろ」と至極ごもっともな指摘をいただいたことがあるが、これも素直にループを回したほうがよさそうだ。
もっとも、本件に限っていえば「むしゃくしゃしてやった、後悔はしていない」だが。
for
ループを使わない方法
逆にfor
ループを使わない方法で市松模様を作ってみた。
ただの意地で、特筆すべきことはない。
最初は天邪鬼だな思ったが、作ってから見るとそう悪くないやり方に思えてきた。
import numpy as np
import cv2
import math
imgH, imgW = 240, 320
color1 = (135, 172, 79)
color2 = (34, 37, 41)
u = 50 # 単位となる四角の大きさ
# 2x2の単位行列を作る
imat = np.array([[color1,color2],[color2, color1]], np.uint8)
# 最近傍補間(というか単純整数倍)で拡大しユニットを作る
unit = cv2.resize(imat, (2*u,2*u), interpolation=cv2.INTER_NEAREST)
# ユニットで割り切れる大きさの市松模様を作る
rh = math.ceil(imgH/2/u)
rw = math.ceil(imgW/2/u)
tile = np.tile(unit, reps=(rh,rw,1)) # 3チャンネル画像の繰り返しはrepsも3チャンネルで指定する
# 必要なサイズにする
img = tile[:imgH, :imgW]
cv2.imshow("tanjiro", img)
cv2.waitKey(0)
cv2.destroyAllWindows()
その2 FizzBuzzワンライナー
もくもく会にて
オンラインもくもく会の中で、仲間の一人が皆で解きあうお題としてFizzBuzzを持ち掛けてきた。
教科書通りのFizzBuzzを書くことができた私はここではワンライナーで記述することを目指した。
よみがえる昭和のテクニック
8ビットホビーパソコンでゲームを作るとき、次のような表現をすることがあった。
テンキーで自キャラを上下左右に動かす処理を思い浮かべてほしい。
'教科書通りの方法
A$=INKEY$
IF A$="4" THEN X=X-1
IF A$="6" THEN X=X+1
IF A$="2" THEN Y=Y-1
IF A$="8" THEN Y=Y+1
'少しマニアックな方法
A$=INKEY$
X=X+(A$="4")-(A$="6")
Y=Y+(A$="2")-(A$="8")
A$="4"
などは代入ではなく比較演算子で、イマドキの言語なら==
を使うところ。
かつて私がさわっていたMSXでは真/偽がそれぞれ-1
/0
を返す。後者のプログラムではその結果を計算に使っているわけだ。
このテクニックがFizzBuzzワンライナーに使えるのではないかと思った。
で、エラーが出たらその都度直せばいいやと甘い考えでプログラムを組んでいったら、驚いたことに一発で満足に動くプログラムができてしまった。
def fizzbuzz(num):
return (num%3==0)*"Fizz" + (num%5==0)*"Buzz" + (num%3!=0)*(num%5!=0)*str(num)
この回答は驚きをもって迎えられた。なぜこれで正しく動くのか聞かれたが、私としてもテキトーに組んだら正しく動いてラッキーという程度だったので説明に戸惑った。頭の中を整理しながら解説する。
第一のポイントは、これまで書いてきたようにTrue
とFalse
が1
と0
に等しいということ。前述のMSXは-1
だがPythonでは1
だ。
そして第二のポイントは、文字列の乗算。整数に限られるが、数値の回数だけ繰り返された文字列が得られる。num=3
のときはFizz
が1回繰り返された文字列とBuzz
が0回繰り返された文字列が結合(+
)されたものが返されるというわけ。
整数と書いたが負の数のときの挙動は君の目で確かめてくれ!
ところで第二のテクニックはいつ身に付けたのだろう。あらためてWebMSXをさわってみたのだが、MSXではエラーになってしまった。Excel VBAでもなかった。どうやら私とこの手法のファーストコンタクトはプチコンなようだ。MSXもそうだがゲームプログラミングはホント役に立つものだ。Switch版のプチコン4は買ってないけど。
プチコン3号 |
---|
文字列×数値はOKだが数値×文字列はエラーになる。 |
ちなみにさまざまな言語で同等の演算ができるようだ。
なかなか学ぶ機会のない記述を学ぶ
値の振り分けにor
を使う
先ほど書いた
def fizzbuzz(num):
return (num%3==0)*"Fizz" + (num%5==0)*"Buzz" + (num%3!=0)*(num%5!=0)*str(num)
は、「3の倍数でも5の倍数でもない場合は普通にその数値を表示する」の部分がどうも美しくないように感じた。
そこでググってみたところ、FizzBuzzワンライナーはよくあるジャンルで中にはより短いコードを競う人たちもいることがわかった。
そんなプロフェッショナルたちが使っていたのがORで演算する手法だった。
公式な記述はこちらにあり、最初は直感的でないなとも思ったが、自分なりにまとめて腹落ちさせることができた。
x or y | x and y | |
---|---|---|
定義 | x が真ならばxの値を返す。 x が偽ならばyの値を返す。 |
x が偽ならばxの値を返す。 x が真ならばyの値を返す。 |
if文での 使われ方 |
↑の真理値 | ↑の真理値 |
考え方 | xが真ならばyを評価することなく真。一発合格。 xが偽でも敗者復活戦に進める。 yが真ならば最終的に真となる。良かったね。 yも偽ならば結局は偽となる。ダメダメですわ。 |
xが偽ならばyを評価することなく偽。一回戦落ち。 xが真ならば二回戦に進める。 yも真ならば最終的に真となる。お見事。 yが偽ならば結局は偽となる。うーん残念。 |
# x or y
print ("ABC" or "XYZ") # ABC
print ("" or "XYZ") # XYZ
print ("ABC" or "") # ABC
# None, 0, "", False, [] などが偽となる。
print (None or "XYZ") # XYZ
print (0 or 1) # 1
print (False or True) # True
print ([] or [1,2,3]) # [1,2,3]
print ("0" or "1") # "0"は偽ではないので"0"
# ビット演算とは違う。
print (3 or 5) # 3 (3が先にあるから)
print (5 or 3) # 5 (5が先にあるから)
print (5 & 3) # 1 (0b101 & 0b011 = 0b001)
print (5 | 3) # 7 (0b101 | 0b011 = 0b111)
print (5 ^ 3) # 6 (0b101 ^ 0b011 = 0b110)
# and は or の逆。
print ("ABC" and "XYZ") # XYZ
この技法をFizzBuzzに使うとこうなる。
x or y
はx
とy
のいずれかを選択しているだけなので、x
が文字列でy
が数値であっても問題ない。
def fizzbuzz(num):
return (num%3==0)*"Fizz" + (num%5==0)*"Buzz" or num
市松模様は完全なワンライナーにすることができなかったが、FizzBuzzは出力までをワンライナー化することができる。
コロンで改行しないとPEP8警察に逮捕されるかもしれないけど。
for i in range(1,100+1):print (i, (i%3==0)*"Fizz" + (i%5==0)*"Buzz" or i)
# 結果
1 1
2 2
3 Fizz
4 4
5 Buzz
6 Fizz
7 7
(中略)
14 14
15 FizzBuzz
16 16
(中略)
98 98
99 Fizz
100 Buzz
ループとbreak
とelse
ワンライナーとは関係ないが、上記or
と同様、よく見る命令なのに「なんでここにあるの? どういう処理なの?」と疑問に思った事柄があるのでついでに書いておく。
ループから抜けたか最後まで回り切ったかを判断するとき、フラグを使うことがある。
以下は九九の9の段を最後まで答えることができたりできなかったりする関数だ。
def qq(num):
exit = False
for j in range(1,10):
if j > num:
exit = True
break
else:
print(f"{9}x{j}={9*j}")
if exit:
print("途中まででした")
else:
print("完走したぞ")
>>> qq(5) # 9の段は5まで答えられる
9x1=9
9x2=18
9x3=27
9x4=36
9x5=45
途中まででした
>>> qq(9) # 9の段は9まで答えられる
9x1=9
9x2=18
9x3=27
9x4=36
9x5=45
9x6=54
9x7=63
9x8=72
9x9=81
完走したぞ
Pythonではこのループから抜けたフラグを使わずelse
節で記述することができる。else
以下が実行されるのはbreak
が実行されなかったとき。
def qq2(num):
for j in range(1,10):
if j > num:
print("途中まででした")
break
else:
print(f"{9}x{j}={9*j}")
else:
print("完走したぞ!")
………。
……。
…。
よし、わかった。これは「そういう使い方がある」というのを知ったうえで使わないようにしよう。
else
はループの途中でbreak
しなかったときの処理なので、break
したときの処理とbreak
しなかったときの処理をフラグを使って書く方法と比べるとわかりやすさが大きく劣ってしまう。上のコードでprint("途中まででした")
の位置を変えざるをえなかったのがそれだ。
また、for~else
で一纏まりなので、for
とelse
の間に共通の処理を差し込むことができない。これも大きな欠点の一つだ。
こちらについては下のコードを見ていただきたい。
def qq3(num):
for j in range(1,10):
if j > num:
print("="*20) # ここと
print("途中まででした")
break
else:
print(f"{9}x{j}={9*j}")
else:
print("="*20) # ここで同じ処理を2回書く必要がある
print("完走したぞ!")
(前略)
9x4=36
9x5=45
====================
途中まででした
(前略)
9x8=72
9x9=81
====================
完走したぞ
そもそも、if~else
では「条件を満たさなかったとき」にelse
以下が実行されるのに対してfor~else
だと「break
しなかったとき、すなわちfor
の条件を満たしてループを完走したとき」にelse
以下が実行されるというのは逆なんじゃないの?という思いもある。
実際、著名な参考書の中では使用を推奨しないと書かれている本もあるようだ。
終わりに
最近アウトプットがないのでちょっとした話をいくつか書き連ねようとしたところ、思いのほか重大な話になってしまった。