Python での単一式プログラミングをする際のメモ

単一式プログラミングとは

単一式プログラミングとは、式として評価できる一行のみでひたすら書いていくもの。
ワンライナーの場合は、代入などの文を使うことができるが、単一式の場合はそもそも文が使えないなどの違いがある。

とりあえず勝手に考えただけなので、 Python においては eval 関数一発で評価できる範囲としておく。

参考
http://www.nishiohirokazu.org/blog/2006/08/python_12.html

基本

基本的にはhttp://www.nishiohirokazu.org/blog/2006/08/python_12.htmlで使っているテクニックは大体使える。
ただし、条件分岐に関しては Python2.5 から PEP 308 -- Conditional Expressions | Python.org が使えるので、多少わかりやすくなっているはず。

import

http://www.nishiohirokazu.org/blog/2006/08/python_12.html でも触れているが、モジュールのインポートには __import__ 関数を使う。

__import__ 関数は、モジュール名を文字列で渡すとモジュールオブジェクトを返す関数である。
ただし、 . を使って os.path のようなモジュール名を指定すると、それらのトップにあるモジュールが返ってくるため、注意が必要。

>>> __import__('os.path').join
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'module' object has no attribute 'join'
>>> __import__('os.path').path.join
<function join at 0xb74fbbfc>

逐次実行

単一式プログラミングの場合、式一つしか使わないため、複数行にわたる処理を書けない。
and と or を用いての逐次実行も可能だが、 lambda を用いることでも複数処理の実行が可能になる。
lambda を用いた場合、各処理の返値を気にする必要がないというのはちょっと良いところ。

>>> (lambda x:__import__('sys').stdout.write('aaaaa'))(__import__('sys').stdout.write('bbbb'))
aaaaabbbb

この場合は処理が右から左に流れるので、違和感があるかもしれない。

変数の導入

単一式でプログラムを組む場合、代入文が書けないために変数定義ができない。
そのため、変数を使うには lambda による関数定義とその呼び出しを利用する。

>>> (lambda x: (lambda y: x * y)(30))(20)
600

この例では、外側の lambda によってローカル変数 x が、内側の lambda によってローカル変数 y が導入されている。

このような無名関数を変数定義に利用するという方法は、 Lisp 系言語における let とほぼ同じである。(例は Scheme)

; let による局所変数定義を行う例
(let ((x 10)
      (y 20))
     (* x y))
; これはマクロにより以下のように展開される(処理系による)
((lambda (x y) (* x y)) 10 20)

再帰による繰り返しの表現

単一式によるプログラムにおいて、リスト内包表記のみで繰り返しを表現するというのは若干つらいものがある。
そんなときは再帰を用いることでさらに柔軟に繰り返し処理を表現することができる。
再帰を用いることにより、内包表記などの単純な繰り返しだけでは表現し得なかった木構造の解析などを簡単に行うことができる。

ただし、単純に lambda だけで関数を定義していたのでは、再帰呼び出しを行う先の関数名が確定できない。
そこで、 Y-Combinator(Y Combinator) を用いる。

Y-Combinator を使うことにより、無名関数のみで再帰の表現が可能になる。
以下は Y-Combinator を用いて lambda のみで再帰による階乗計算を行う例である。

>>> (lambda s: (lambda m:m(m)(s))(lambda proc: (lambda n: 1 if n == 0 else n * proc(proc)(n-1))))(5)
120

細かいところ

dict を合成したい

通常は

>>> d1 = dict(aaa=10, bbb=10)
>>> d2 = dict(bbb=30, ccc='aaa')
>>> d3 = d1.copy()
>>> d3.update(d2)
>>> d3
{'aaa': 10, 'bbb': 30, 'ccc': 'aaa'}

というように update メソッドを使って新しい辞書に追記していくことが多いが、これだと複数文実行することになる。
一応可能ではあるが、めんどくさいので以下のように dict 関数を使うことで一発で合成することができる。

>>> dict(d1.items()+d2.items())
{'aaa': 10, 'bbb': 30, 'ccc': 'aaa'}

サンプル

単一式でコマンドライン引数で渡されたディレクトリ以下のファイルをすべて出力する処理を書いてみた。

(lambda os=__import__('os'), sys=__import__('sys'):[(lambda p:(lambda m:m(m)(p))(lambda proc:(lambda n:[proc(proc)(os.path.join(n, x)) for x in os.listdir(n)] if os.path.isdir(n) else sys.stdout.write(n+'\n'))))(arg) for arg in sys.argv[1:]])()

これを展開すると以下のようになる。

(lambda os=__import__('os'), sys=__import__('sys'):
     [(lambda p:
           (lambda m:
                m(m)(p)
            )
       (lambda proc:
            (lambda n:
                 [proc(proc)(os.path.join(n, x))
                  for x in os.listdir(n)] if os.path.isdir(n) else sys.stdout.write(n+'\n')
             )
        )
       )(arg)
      for arg in sys.argv[1:]]
 )()

なんとなく Lisp っぽい感じ。

まとめ

以上のように、単一式であっても色々と処理が書ける。
このような書き方をする際の利点としては

といったことが挙げられる。

ただ、良いところばかりではなく以下のような問題もある。

  • lambda による関数呼び出しのネストが深いと再帰回数の制限に引っかかってしまう可能性がある(末尾再帰を最適化してくれれば…)
  • Lisp っぽい

とはいえ、意外とすんなりといけるもんだ。