2008年9月24日水曜日

Python で map 関数の第 2 引数を操作の対象ではなく手段として使う

操作対象としてのリスト

map 関数は、2.1 組み込み関数 によると、

map(function, list, ...)

functionlist の全ての要素に適用し、返された値からなるリストを返します。

操作対象のリストがあり、それを操作する関数を定義するというイメージ。

 

例えば、リストの各要素を 2 倍したいなら、

print map(lambda x: x*2, [1,2,3,4,5])

map 関数の第 2 引数のリストに対して、第 1 引数の関数を適用する。

リスト内包表記を使うなら、(cf. Python のリスト内包表記)

print [x*2 for x in [1,2,3,4,5]]

対象のリストがあって、そこから一つずつ取り出して関数を適用する。

 

リストは「料理される側」であって、関数はそれに対する「包丁」というイメージが自分の頭に固定された。 (+_+)

080923-001

 

手段としてのリスト

そういう固定観念があったので、前回「Python のジェネレータ (3)」で参考にした「数字にコンマを振る」の記事のコメントにあった map 関数の使い方を見て、なるほどと思った。なぜかと言うと、 map の第 2 引数が「関数全体の目的」に対して「手段」として使われていたから。具体的には、他のリストを参照するための手段としてのリスト。

080923-002

例えば、

a = "hogepiyofuga"
print map(lambda x: a[x], [1,4,7,8])

結果は、

['o', 'p', 'o', 'f']

map 関数の第 2 引数が、他の変数を参照するための手段として使われている。 map 関数の外にある変数を、無名関数 lambda の中から参照。

リスト内包表記で書くなら、

print [a[x] for x in [1,4,7,8]]

頭が固いので、こういうイメージ、 map に対して全然わかなかった。 (+_+)

 

普通に書くとすると、対象を中心に考え、

print [e for i,e in enumerate("hogepiyofuga") if i in [1,4,7,8]]

map と filter 関数を使うならば、

print map(lambda (i,e): e,
          filter(lambda (i,e): i in [1,4,7,8],
                 enumerate("hogepiyofuga")))

 

Ruby では?

Ruby で書くなら、

str = "hogepiyofuga".split(//s)
p [1,4,7,8].map{|e| str[e]}

あ~、そうだ。これ書いていて、以前から違和感を感じていたコードの理由がなんとなくわかった。脱線してしまうが、 Ruby では 繰り返しの for が内部で呼出しているというイテレータ。例えば、ブロックを 5 回実行するには、

(0...5).each{|i| p i}

for 文の書き方に比べて感覚的にしっくり来なかった。 ( 10.times do … という書き方はしっくり来るんだけど ^^; )

繰り返しの処理を行うとき、意識の中ではブロックの中に記述されているコードが処理・関心の中心にある。繰り返す回数は、あくまでも回数をこなすためにカウントする手段に過ぎない。問題に対して脇役というイメージ。だから、その手段となっている Range オブジェクト が最初に来て、主語のように居座り、それに対して処理をお願いするという形がピンと来なかった。上記の map の使い方もイメージとしては同様に感じていたために、何か微妙な感じがしたのかな。

ついでなので、対象の方を「主」にして書くと、

result = ""
"hogepiyofuga".split(//s).each_with_index do |e,i|
  result << e if [1,4,7,8].any?{|elem| elem == i}
end
p result

あれ?思ったよりも長くなった… (@_@;) 書き方違ってるのかな?

 

lambda

ちなみに、Python の 5.11 ラムダ (lambda) によると、

ラムダ形式で作成された関数は、実行文 (statement) を含むことができないので注意してください。

代入は、「代入文 (assignment statement)」、つまり statement の一種なので (cf. 6. 単純文 (simple statement))、 lambda の中に含むことはできない。 Ruby の proc{}, lambda{} とは異なる。(cf. Ruby のブロックと Proc)

だから、次のコードを実行しようとすると、「exceptions.SyntaxError: lambda cannot contain assignment」 というエラーが表示されてしまう。

a = list("hogepiyofuga")
map(lambda x: a[x] = "X", [1,4,7,8])

まぁ、しかし上記のような目的にそんな風には書かないか ^^; 普通は、

result = ""
for i,e in enumerate("hogepiyofuga"):
    result += "X" if i in [1,4,7,8] else e
print result

 

ジェネレータを使うなら、

def g():
    for i,e in enumerate("hogepiyofuga"):
        yield "X" if i in [1,4,7,8] else e

print "".join(x for x in g())

上記のジェネレータを一般化すると、

def g2(str, L, x):
    for i,e in enumerate(str):
        yield x if i in L else e

print "".join(x for x in g2("hogepiyofuga", [1,4,7,8], "X"))

 

どうしても、 lambda の中で外の変数を変更したいなら、オブジェクトのメソッド呼出しにすればいいか~。

class StringWrapper:
    def __init__(self, str):
        self.str = str
    def __getitem__(self, key):
        return self.str[key]
    def __setitem__(self, key, value):
        self.str = self.str[:key] + value + self.str[key+len(value):]
    def set(self, key, value):
        self.__setitem__(key, value)
    def __str__(self):
        return self.str

s = StringWrapper("hogepiyofuga")

# 参照
print map(lambda x: s[x], [1,4,7,8])

# 変更 
map(lambda x: s.set(x,"X"), [1,4,7,8]) 
print s

(cf. Python で要素を添字で参照する - 特殊メソッドを使って)

しかし、もうこれは lambda を使う意味がなくなってる  … ^^; これなら StringWrapper に、先ほどのジェネレータ g2 に相当するメソッドを作って、オブジェクトに対して普通にメソッド呼出しをするよね…。

 

余談

map 関数のドキュメントを読みなおしてはじめて気がついたけれど、これって複数のリストに対して適用できるのかぁ~ (@_@)

map(function, list, ...)

… 追加の list 引数を与えた場合、 function はそれらを引数として取らなければならず、関数はそのリストの全ての要素について個別に適用されます; 他のリストより短いリストがある場合、要素 None で延長されます。

(2.1 組み込み関数 の map より)

例えば、

print map(lambda x,y: x+y, [1,2,3],[10,20,30])
print map(lambda x,y,z: x+y+z,
          [1,2,3],[10,20,30],[100,200,300])
print map(lambda *x: [a*2 for a in x],
          [1,2,3],[10,20,30],[100,200,300])

結果は、

[11, 22, 33]
[111, 222, 333]
[[2, 20, 200], [4, 40, 400], [6, 60, 600]]

functionNone の場合、恒等関数であると仮定されます (同上より)

恒等関数 – Wikipedia とは、

変数を全く変えずにそのままの値で返す関数のこと。

zip 関数の説明には次のようにある。

zip() は初期値引数が Nonemap() と似ています。

例えば、

print map(None, [1,2,3],[10,20,30])

結果は、

[(1, 10), (2, 20), (3, 30)]

追記(2008.9.25) : 違いについては、Python の zip と map の違い参照。