1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ワンライナー、二題

Posted at

はじめに

ワンライナー、それは果てなき荒野。

その1 市松模様ワンライナー

長年の宿題を解決する

内包表記だったかnumpy.where()だったか忘れたが、そのあたりを学習中に「これを使えば市松模様をワンライナーで書けるのではないか」と思ったことがあった。別にそうせねばならない切実な理由があるわけではないが、その後もときどき思い出したように挑戦し、そして失敗していった。
先日とうとう成功し、うれしさのあまり裸で街を駆け回りおまわりさんの世話になったのだった。嘘だけど。

tanjiro1.py
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()
結果
tanjiro.png

内包表記の二重ループの中に三項演算子。うーん、これは美しくない。良い子は真似しないでね。

変数を使わずコード内に数値を埋め込み、cv2.destroyAllWindows()が必要なcv2.imshow()でなくcv2.imwrite()で画像を保存する内容にすればimportの2行以外は1行にできるが、そのような工夫は無意味だろう。

内包表記に関する別の質問をteratailでしたところ「意味のないコードをこねくりまわすより簡潔な書き方をしろ」と至極ごもっともな指摘をいただいたことがあるが、これも素直にループを回したほうがよさそうだ。
もっとも、本件に限っていえば「むしゃくしゃしてやった、後悔はしていない」だが。

forループを使わない方法

逆にforループを使わない方法で市松模様を作ってみた。
ただの意地で、特筆すべきことはない。
最初は天邪鬼だな思ったが、作ってから見るとそう悪くないやり方に思えてきた。

tanjiro2.py
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ビットホビーパソコンでゲームを作るとき、次のような表現をすることがあった。
テンキーで自キャラを上下左右に動かす処理を思い浮かべてほしい。

BASIC
'教科書通りの方法
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ワンライナーに使えるのではないかと思った。
で、エラーが出たらその都度直せばいいやと甘い考えでプログラムを組んでいったら、驚いたことに一発で満足に動くプログラムができてしまった。

FizzBuzz関数
def fizzbuzz(num):
    return (num%3==0)*"Fizz" + (num%5==0)*"Buzz" + (num%3!=0)*(num%5!=0)*str(num)

この回答は驚きをもって迎えられた。なぜこれで正しく動くのか聞かれたが、私としてもテキトーに組んだら正しく動いてラッキーという程度だったので説明に戸惑った。頭の中を整理しながら解説する。

第一のポイントは、これまで書いてきたようにTrueFalse10に等しいということ。前述のMSXは-1だがPythonでは1だ。
そして第二のポイントは、文字列の乗算。整数に限られるが、数値の回数だけ繰り返された文字列が得られる。num=3のときはFizz1回繰り返された文字列とBuzz0回繰り返された文字列が結合(+)されたものが返されるというわけ。
整数と書いたが負の数のときの挙動は君の目で確かめてくれ!

ところで第二のテクニックはいつ身に付けたのだろう。あらためてWebMSXをさわってみたのだが、MSXではエラーになってしまった。Excel VBAでもなかった。どうやら私とこの手法のファーストコンタクトはプチコンなようだ。MSXもそうだがゲームプログラミングはホント役に立つものだ。Switch版のプチコン4は買ってないけど。

プチコン3号
20210716092959.png
文字列×数値はOKだが数値×文字列はエラーになる。

ちなみにさまざまな言語で同等の演算ができるようだ。

なかなか学ぶ機会のない記述を学ぶ

値の振り分けにorを使う

先ほど書いた

FizzBuzz関数
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 yxyのいずれかを選択しているだけなので、xが文字列でyが数値であっても問題ない。

FizzBuzz関数その2
def fizzbuzz(num):
    return (num%3==0)*"Fizz" + (num%5==0)*"Buzz" or num

市松模様は完全なワンライナーにすることができなかったが、FizzBuzzは出力までをワンライナー化することができる。
コロンで改行しないとPEP8警察に逮捕されるかもしれないけど。

fizzbuzz_oneliner.py
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

ループとbreakelse

ワンライナーとは関係ないが、上記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で一纏まりなので、forelseの間に共通の処理を差し込むことができない。これも大きな欠点の一つだ。
こちらについては下のコードを見ていただきたい。

正しく動くが、美しくない
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以下が実行されるというのは逆なんじゃないの?という思いもある。
実際、著名な参考書の中では使用を推奨しないと書かれている本もあるようだ。

終わりに

最近アウトプットがないのでちょっとした話をいくつか書き連ねようとしたところ、思いのほか重大な話になってしまった。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?