« 日記 |Main| 日記 »

« Pythonでイテレータの復習 | Python | Pythonの構文木を見る »

Pythonでワンライナーを作成する際のノウハウ集

これはLL Ringというイベントの「じゃんけん2.0」に出場する際に「多くの構文に改行が必須であるPythonで書かれたじゃんけんエージェントをワンライナーにしていたらウケるかな」と思ってワンライナー化しているときに書いたメモです。自分用のメモのつもりだったので書き殴ってありますが、意外と人気のようなので近いうちに加筆します。 実は後から書いた英語版(How to make oneliner in Python?)の方が整理されているのかも。

完成したワンライナー

jankenoneliner.png

def文を式にする

defは改行を要求するのでlambdaに置き換える必要がある。

def foo(x): return x + 1(ここに改行)
foo = lambda x: x + 1
globals().__setitem__("foo", lambda x: x + 1)

lambdaは式しか含むことが出来ないので、代入などの文は全部式に置き換える必要がある。

if文を式にする

if condition:
    p("True")
else:
    p("False")
condition and p("True") or p("False")

andやorが遅延評価することを利用している。このandとorを使った評価順の制御が、式しか使えないlambdaの中ではすべての基礎になる。

p("True")が0, NoneなどのFalseと判定される値になりうる場合は例えばこうする。

(condition and [p("True")] or [p("False")])[0]

空でないリストやタプルはTrueと判定されることを利用している。

柴田さん(TRIVIAL TECHNOLOGIES 2.0)情報によればbool値をintに変換した際に0と1になることを利用して以下のように書くこともできる。これは上の例での関数pが副作用を持たない場合にのみ使うことができる。

["False", "True"][condition]

for文を式にする

リスト閉包内包を使う。

>>> for i in range(5):
	print i

	
0
1
2
3
4
>>> import sys;[sys.stdout.write(str(i) + "\n") for i in range(5)]
0
1
2
3
4
[None, None, None, None, None]

最後の[None, None, None, None, None]がこの式の値である。

代入文を式にする

リストのappendや辞書の__setitem__を使用する。 グローバルな名前空間globals()、ローカルの名前空間locals()、そしてオブジェクトxの名前空間x.__dict__がよく使われるだろう。

while文を式にする

ループ回数が十分少なければ再帰呼び出しでも構わないが、多い場合は再帰回数の上限を超えてしまうので使えない。 そこで、itertoolsのcountとifilterfalseを使う。countはいわゆる無限リスト。ifilterfalseによって生成されたイテレータのnextメソッドを呼ぶと、最初にfalseになる所まで実行して中断することを利用する。

>>> i = 1
>>> while i < 100:
	i *= 2

	
>>> i
128

break文とelse節

breakを実現するためには「式を評価した結果が特定の値になったら続きを実行しない」という必要がある。 「特定の値」をわかりやすくするために"BREAKED"という文字列にすると、これはTrueと判断される値なので、orで繋いでおき、Trueと判断される値になったらループを終了する。itertools.ifilterfalseではなくitertools.ifilterを使う。ループがブレイクされた場合には値が"BREAKED"になり、されなかった場合にはTrueになることを利用してelse節を実現できる。

>>> primes = []
>>> for i in range(2, 100):
	for p in primes:
		if i % p == 0:
			break
	else:
		primes.append(i)

>>> primes
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]

この素数を求めるコードをまず下のように書き換え、forを消す。

>>> primes = []
>>> for i in range(2, 100):
	j = 0
	while j < len(primes):
		if i % primes[j] == 0:
			break
		j += 1
	else:
		primes.append(i)

		
>>> primes
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]

>>> globals().__setitem__("primes", []) or [
  globals().__setitem__("j", 0) or
  ifilter(bool,
    (
      not(j < len(primes)) or
      (i % primes[j] == 0 and "BREAKED") or
      globals().__setitem__("j", j + 1)
      for c in count()
    )
  ).next() == True and primes.append(i)
  for i in range(2, 100)
] and None
>>> primes
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]

改行の除去と適切なimportはお任せします。

2つの式を順次実行する

一つめの式がTrueと判定されるならandで結ぶ。Falsと判定されるならorで結ぶ。わからないなら一つめの式を[]で囲ってTrueと判定させる。

NoneはFalseと判定される。__setitem__やsys.stdout.writeなどのメソッドはNoneを返すのでa or b or c or ...と続けていくのが一番自然だが、Trueと判定される物が混ざっている場合は(a and False)などとやって無理矢理Falseにする必要がある。もちろん、文字数を減らしたい場合は(a and 0)でOK。数値であることがわかっているならa * 0がよいだろう。

逆にandで続けていくのなら、Falseと判定されうる物は最初に書いたように[]で囲うことでTrueと判定させる必要がある。Falseと判定される物が連続しているなら[sys.stdout.write("1"), sys.stdout.write("2"), sys.stdout.write("3")]というようにリストにしてしまうのも字数を減らす上では一つの手である。

class文を式にする

read-onlyでないクラスをインポートする。そのクラスのインスタンスを作ってメンバを動的につっこむ。もし複数のインスタンスを作る必要があるならlambdaで囲ってbuilder関数にする。

>>> class Counter:
	def __init__(self):
		self.count = 0
	def __call__(self):
		self.count += 1
		return self.count

	
>>> c = Counter()
>>> c()
1
>>> c()
2

クラスを定義するのは多くの場合トップレベルなのでこの例ではimport文や代入文を使っている。完全に式としてクラスの生成をしたければimportを式にし、生成したインスタンスを引数として無名関数を呼び出して、その中で各種メンバの挿入を行って返せばよい。この関数をクラス名をキーとして名前空間に入れれば、普通のクラス定義と同じように使うことが出来る。

例外のハンドリング

例外を投げうるコードをパイプとして開き、例外が投げられたときにPythonが標準で出力するエラーメッセージをパースすればtry catchに相当することが出来る。以下はじゃんけんエージェントのプロトタイプからの引用。まず最初にコマンドライン引数に"SAFETY_NET"を渡す。渡された場合には引数を"GO"に変えて自分自身を起動する。

    if sys.argv[3] == "SAFETY_NET":

        ifilterfalse(bool,
            (
                re.search("Software caused connection abort",
                    os.popen3(
                        r"python %s %s %s GO" % (
                            sys.argv[0],
                            sys.argv[1],
                            sys.argv[2]
                        )
                    )[2].read()
                )
                for x in count()
            )
        ).next()


    elif sys.argv[3] == "GO":
        #  例外を投げ得る処理

これで

while True:
    try:
        #  例外を投げ得る処理
    except ....: # Software caused connection abortだけキャッチして無視
        pass

に相当する処理が出来る。

この場合は特定の例外だけをキャッチして無視するコードであるが、実際には例外が発生したときの状況が必要だったりするケースもあるかと思う。それは、必要になるデータをすべてまとめてcPickle.dumpsで文字列にダンプし、切り出しやすいように適当な印をつけて例外が投げられそうな部分の手前で出力しておき、キャッチ側ではそれをcPickle.loadsする。

import文を式にする

組み込み関数__import__を使う。でも、lambdaの中でimportせずに冒頭でimportするほうが関数にする必要がないので楽。

字数の削減

最もよく使うメソッドを__call__にする。 __setitem__は頻出するので、x.__dict__.__setitem__("__call__", x.__dict__.__setitem__)がよいかもしれない。

[N]でN文字の文字列を意味することにする。from [N] import [M]をfrom [N] import [M] as [1]に置き換えると、インポート部分は5文字増え、呼び出し部分はM - 1文字減る。Mが7文字以上なら使うのが1回でも置き換えるメリットがあり、使うのが2回あればMが4文字でもメリットがある。

長い名前を1~2文字の短い名前にするのは字数を減らす上で有効だが、うっかり衝突してしまうと目で見て直すのは大変なのでバージョン管理とこまめなテストが重要。あとどの文字をどのスコープで何に使ったかを記録するといいかもしれない。

トラックバック(Trackback)

Trackback URL: http://www.nishiohirokazu.org/mt/mt-tb.cgi/240

ご意見・ご感想をお送りください(フィードバック)

(フィードバックはメールで送信され、基本的に表示されませんが、内容によっては公開させていただくこともございます。ご了承ください。Your comment doesn't appear the page immediately. If the comment has value to other people, it will be put on the page or subsequent entries. Thank you.)

上の情報は、いずれも未記入でかまいません。 All of above questions are optional.