外部変数のキャプチャ

この記事は Python Tips Advent Calendar 2012 6日目の記事です。


Python では、ラムダ式やローカルでの def 関数は、それらを定義した時に、外にあるローカル変数の参照を、ラムダ式や関数のオブジェクトの内部に保存します。

>>> x = 10
>>> f = lambda: x     # x をラムダ式オブジェクトに保存する
>>> f()     # f は x への参照を保持している
10

ローカル変数のコピーではなく "参照" であるという点に注意です。

>>> x = 10
>>> idx = id(x)     # ここで使っている x と
>>> f = lambda: idx == id(x)     # ここで使っている x は同じオブジェクトなので、同じ ID になる
>>> f()
True

これは便利なこともあるのですが、例えば以下の様な挙動になることもあるので注意です。

>>> xs = [1,2,3]
>>> fs = [(lambda: x) for x in xs]
>>> for f in fs:
...     f()
3
3
3

普通に見ると 1 2 3 と出力されそうですが、for x in xs で作った x は、ループ中は同じオブジェクト(ループ中常に id(x) が同じ)になるので、全てのラムダ式で同じオブジェクトを参照することになり、最後の値である 3 が出力されることになります。


これを解決するには、例えば以下のようにラムダ式で引数を取るようにして、partial でその引数をバインドしてやるという手が考えられます。

>>> from functools import partial
>>> fs = [partial(lambda x: x, x) for x in xs]
>>> for f in fs:
...     f()
1
2
3


あるいは、引数のデフォルト値は定義時にバインドされるという特性を利用して、

>>> fs = [(lambda x=x: x) for x in xs]
>>> for f in fs:
...     f()
1
2
3

のように書くこともできます。


.

print で標準出力以外に出力する

この記事は Python Tips Advent Calendar 2012 5日目の記事です。


※以下の説明は Python 2 用です。Python 3 では print(..., file=sys.stderr) などになります。


print 関数はデフォルトでは標準出力に出力しますが、明示的に指定すればそれ以外の場所、標準エラーなどにも出力できます。

>>> import sys
>>> print>>sys.stderr, 10,20 # 標準出力に '10 20\n' が出力される

これはファイルオブジェクトのようなインターフェースさえ持ってるものなら何でもいいので、当然ファイルにも出力できます。

>>> with open('hoge', 'w') as f:
...     print>>f, 10,20 # 'hoge' ファイルに '10 20\n' が出力される


また、StringIO を使うと、print の出力を文字列として受け取る事ができます。

>>> import StringIO
>>> fd = StringIO.StringIO()
>>> print>>fd, 10,20
>>> fd.getvalue()
'10 20\n'

これは例えば、出力する文字列に対するテストなどに使えます。

>>> import sys
>>> def print_rot13(data, fd = sys.stdout):
...     print>>fd, data.encode('rot13')
>>> 
>>> import StringIO
>>> fd = StringIO.StringIO()
>>> print_rot13('Hello', fd)
>>> assert fd.getvalue() == 'Uryyb\n'


.

暗黙bool値変換

この記事は Python Tips Advent Calendar 2012 4日目の記事です。


Python はいろんな値を、暗黙に bool 値として扱います。
False として扱われるデータは以下の通りです。

>>> false = False
>>> none = None
>>> int_zero = 0
>>> long_zero = 0L
>>> float_zero = 0.0
>>> complex_zero = 0 + 0j
>>> empty_tuple = ()
>>> empty_list = []
>>> empty_dict = {}
>>> empty_set = set()
>>> empty_string = ''

>>> assert not int_zero
>>> assert not long_zero
>>> assert not float_zero
>>> assert not complex_zero
>>> assert not false
>>> assert not none
>>> assert not empty_tuple
>>> assert not empty_list
>>> assert not empty_dict
>>> assert not empty_set
>>> assert not empty_string

更に、ユーザ定義のクラスであっても、__nonzero__ で False を返すか、__len__ で 0 を返した場合も False として扱われます。(__nonzero__ の方が優先される)

>>> class UserDefined1(object):
...     def __nonzero__(self): return False
>>> ud1 = UserDefined1()
>>> class UserDefined2(object):
...     def __len__(self): return 0
>>> ud2 = UserDefined2()

>>> assert not ud1
>>> assert not ud2

特に1つの if 文で複数の型を判定する可能性があるようなケースでは注意しましょう。


.

スライス操作あれこれ

この記事は Python Tips Advent Calendar 2012 3日目の記事です。


今日は Python のスライス操作についてです。

基本的なスライス操作

>>> xs = range(10)
>>> xs[2:5]    # 2以上5未満までのインデックス
[2, 3, 4]
>>> xs[:2]     # 前を省略した場合は0と同じ
[0, 1]
>>> xs[7:]     # 後ろを省略した場合はlen(xs)、つまり10と同じ
[7, 8, 9]
>>> xs[-3:]    # -3 は後ろから 3 番目(つまり len(xs)-3 と同じ)
[7, 8, 9]
>>> xs[-3:-1]  # 当然 2 個目にもマイナスを入れられる
[7, 8]
>>> xs[::4]    # 3 個目にはスキップ数が入れられる
[0, 4, 8]
>>> xs[2:-2:3] # 組み合わせ
[2, 5]
>>> xs[::-4]   # マイナス値の場合は後ろからスキップする
[9, 5, 1]
>>> xs[8:1:-2] # 組み合わせ
[8, 6, 4, 2]

スライスでこんな風にリストを取得できます。

スライス操作で書き換える

スライスで取得したリストに代入をすると、その要素が代入したリストに置き換わります。

>>> xs = range(10)
>>> xs[:5] = [42] # [0, 1, 2, 3, 4] が [42] に置き換わる
>>> print xs
[42, 5, 6, 7, 8, 9]
>>> xs[:1] = range(5) # [42] が [0, 1, 2, 3, 4] に置き換わる
>>> print xs
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

要素の削除についても同様で、スライスで取得した要素が全て削除されます。

>>> del xs[::2] # [0, 2, 4, 6, 8] が削除される
>>> print xs
[1, 3, 5, 7, 9]

スキップ付きのスライスも同様です。スライスで取得した要素が代入したリストに置き換わります。

>>> xs[::2] = xs[::-2]   # [1, 5, 9] が [9, 5, 1] に置き換わる
>>> print xs
[9, 3, 5, 7, 1]

ただしスキップ付きは注意点があって、これは要素数が同じでない場合は例外を投げます。

>>> xs[::2] = xs    #doctest: +ELLIPSIS
Traceback (most recent call last):
   ...
ValueError: attempt to assign sequence of size 5 to extended slice of size 3

リストの反転

リストの反転は、スライスを使って簡単に書けます。

>>> xs = [1,2,3]
>>> list(reversed(xs))
[3, 2, 1]
>>> xs[::-1] # 短く書けるよ!
[3, 2, 1]

リストのコピー

リストの(浅い)コピーもスライスで書けます。

>>> xs = [1,2,3]
>>> list(xs)
[1, 2, 3]
>>> xs[:] # 短く書けるよ!
[1, 2, 3]

スライスの正体

スライスというのは単なる略記法で、実際はこれは以下の様な slice クラスのオブジェクトになっています。

>>> xs = [1,2,3,4]
>>> xs[1::2]
[2, 4]
>>> xs[slice(1, None, 2)]
[2, 4]

slice クラスは start, stop, step の3つのプロパティを持っているので、以下のように自分でスライス処理を書いたりもできます。

>>> class X(object):
...     def get(self, x):
...         if isinstance(x, slice):
...             return 'sliced object'
...         else:
...             return 'normal object'
>>> x = X()
>>> x.get(slice(1, 2, 3))
'sliced object'
>>> x.get(3)
'normal object'

ちなみに x.get(1:2:3) という書き方はできません。スライスの略記は [] の中だけで使えます。

Ellipsis

[] の中では、スライス以外にも、もう一つ、Ellipsis というクラスの略記法が使えます。

>>> class X(object):
...     def __getitem__(self, x):
...         if x is Ellipsis:
...             return 'ellipsis object'
...         else:
...             return 'normal object'
>>> x = X()
>>> x[...] # Ellipsis を渡す
'ellipsis object'
>>> x[Ellipsis] # x[...] はこれと同じ意味
'ellipsis object'
>>> x[42]
'normal object'

これは標準のリストでは使われていないですが、例えば行列とかを計算してくれる numpy パッケージでは、こんな風に使えます。

>>> import numpy
>>> x = numpy.array([[[1],[2],[3]], [[4],[5],[6]]])
>>> x[..., 0]
array([[1, 2, 3],
       [4, 5, 6]])
>>> x[:, :, 0] # x[..., 0] はこれと同じ意味
array([[1, 2, 3],
       [4, 5, 6]])


.

文字列を複数行に書く

この記事は Python Tips Advent Calendar 2012 2日目の記事です。

>>> "aaa\
... bbb"
'aaabbb'

\ で終わると単純に結合してくれるので、このようになります。

>>> '''aaa
... bbb'''
'aaa\nbbb'

""" や ''' で囲まれた文字列は、改行コードもそのまま入った状態で出力されます。
もし """ や ''' で改行コードを入れたくない場合、

>>> '''aaa\
... bbb'''
'aaabbb'

と書けばいいです。


あるいは、C や C++ と同様、文字列リテラルが連続している場合は連結されるので、

>>> ('aaa'
...  'bbb')
'aaabbb'

のように書くことができます。カッコがあるのは単に文の終わりと判断されないためなので、

>>> 'aaa' \
... 'bbb'
'aaabbb'

と書いても構いません。


.

比較の連鎖

この記事は Python Tips Advent Calendar 2012 1日目の記事です。

Python では比較を連鎖させていくことができます。

>>> x = 5
>>> 0 <= x <= 10
True

これは、ほぼ 0 <= x and x <= 10 と同じで、x が 2 回評価されるという点のみが違います。


.

os.pipe を使って subprocess.Popen する時の注意

ファイルディスクリプタは subprocess.Popen (というか fork)時にコピーされます。
なので、親プロセスと子プロセスの両方で、必要のないディスクリプタを閉じる必要があります。


しかしこれ、子プロセスが既存のコマンド(例えば tail)だったらファイルディスクリプタ閉じるのって無理じゃね?とか思ったのですが、fork して exec するまでの間に呼んでくれる、preexec_fn 関数というのを指定できるようなので、それを使います。

import os
from subprocess import Popen

r,w = os.pipe()
p = Popen(["tail", "-f", "hoge"], stdout=w,
          preexec_fn = lambda: os.close(r)) # 子プロセスの読み込み側を閉じる
os.close(w) # 親プロセスの書き込み側を閉じる

# あとは好きなだけ読む
rfd = os.fdopen(r)
for line in rfd:
  print line[:-1]

ただ、このコードだと、例外が起こった時にファイルディスクリプタ漏れる気がするので、もうちょいマジメにコード書かないといけないですね。