初めてのPython(17章)

lambda式、map関数、リスト内包表記、ジェネレータについて

lambda式

無名関数と呼ばれるように名前の無い関数を作成する。

実行されると関数が作成され、作成された関数が式の戻り値になる。
作成された関数が自動的に同名の変数に代入されるのがdefステートメントによる関数定義。

lambda式は次のように定義する。

lambda arg1, arg2, ... argN: 引数を使用した式(ボディ)

defステートメントとの違いは

  • lambda式は「式」
    • 式なので、リストの要素にしたり(データ構造に使える)、関数の引数にしたりできる
  • lambda式のボディ部分も「式」
    • return しなくても演算結果が返ってくる
    • ステートメントを書くことは出来ない(return, print, if~else~)が、工夫すれば同様のことは出来る
>>> def func(x, func):
...     print "address: %s" % func
...     print x, func()
... 
>>> number = 10
>>> string = "I'm sleepy"
>>> func(number, lambda: string)
address: <function <lambda> at 0x100489500>
10 I'm sleepy
>>> 
>>> D = {'already': (lambda: 2 + 2),
...      'now': (lambda: 2 ** 2),
...      'yet': (lambda: 2)}
>>> key='now'
>>> D[key]()
4
>>> 

以下については、defステートメント同様の動きをする

  • 引数の渡し方(位置マッチング、名前マッチング、デフォルト値、"*"、"**")
  • 変数のスコープはLEGB
>>> func = lambda a, b: a + b
>>> func("ham", "egg")
'hamegg'
>>> func(b="egg", a="ham")
'hamegg'
>>> 
>>> func = lambda a, b: a + b
>>> tupple=(1, 2)
>>> func(*tupple)
3
>>> dict = {'a':1, 'b':2}
>>> func(**dict)
3
>>> 
>>> func = lambda *a: a
>>> func(1,2,3,4)
(1, 2, 3, 4)
>>> 
>>> func = lambda **a: a
>>> func(a=1, b=2, c=3, d=4)
{'a': 1, 'c': 3, 'b': 2, 'd': 4}
>>> 
>>> def knights():
...     title = "Sir"
...     action = (lambda x: title + ' ' + x)
...     return action
... 
>>> act = knights()
>>> act("robin")
'Sir robin'
>>> 

また、lambda式の演算で条件分岐/繰り返しが必要な時は、

操作 置き換えの式
条件分岐(if〜else) if/else, and/or
繰り返し(for) リスト内包表記,map関数
>>> a = 10
>>> b = 1
>>> c = -1
>>> if a:
...     b
... else:
...     c
... 
1
>>> b if a else c
1
>>> ((a and b) or c)
1
>>> 
>>> for word in ['spam', 'ham', 'bacon']:
...     print word
... 
spam
ham
bacon
>>> import sys
>>> list = [sys.stdout.write(word) for word in ['spam\n', 'ham\n', 'bacon\n']]
spam
ham
bacon
>>> list = map(sys.stdout.write, ['spam\n', 'ham\n' 'bacon\n'])
spam
ham
bacon
>>> 

mapやリスト内包表記は復帰値としてリストを返すので、復帰値を受け取る変数を用意して代入している。
(そうしないと最後に[None, None, None]と表示されてしまう(インタプリタじゃなきゃ無視されるが))
また、sys.stdout.write()は渡された文字列をstdoutに出力するので、改行コードも必要。

map関数

シーケンスの個々の要素に対して同じ操作をするためのビルドイン関数。
map()の使い方は、以下で復帰値として演算結果からなるリストを返す。

map(function, sequece1[, sequence2, ...]) -> list

function部分には、defステートメントで定義された関数やlambda式が使用可能。
mapで出来ることは自作することが可能だそうな。
ただ、安定性やパフォーマンスはやっぱりビルドインが優れてる。
折角用意されているんだし、車輪の再開発?はしなくてもいいかな。

シーケンスの個々の要素に対して同じ操作をするビルドイン関数にはfilter/reduceもある

filter関数

以下のようにして使う。

filter(function or None, sequence) -> list, tupple or string

functionが与えられた時は、sequenceのうちfunctionがTrueを返す要素を返す。
Noneが与えられた時は、sequenceのうちTrueな要素を返す。
復帰値の形式はsequenceと同じ型(tuppleならtupple, stringならstring, それ以外ならlist)

>>> filter((lambda x: x > 0), range(-5, 5))
[1, 2, 3, 4]
>>> filter(None, range(-5,5))
[-5, -4, -3, -2, -1, 1, 2, 3, 4]
>>> 

reduce関数

以下のようにして使う

reduce(function, sequence[, initial]) -> value

seqenceの要素を2つ取り出してfunctionにかけて、その結果とseqenceの次の要素とfunctionにかけて、
というようにsequneceの要素を累積的にfunctionにかけて、最終的に1つの値を返す。
initialが指定されると、initialとsequence[0]の要素からfunctionの処理が始まる。
初期値を指定するようなもので、initialを指定しないとsequence[0]とsequence[1]で処理が始まる。

>>> reduce((lambda x, y: x + y), [1, 2, 3, 4])
10
>>> reduce((lambda x, y: x + y), [1, 2, 3, 4], 10)
20
>>> 

3.0でのmap/filter/reduceがどうなるか未定って書いてある。
map/filterはリストではなくイテレータを返し(リストが欲しければlist(map...))、
reduceはビルドインから外れたらしい(使うにはfunctoolsのインポートが必要)*1

リスト内包表記

forループをネストするこも可能

>>> [(x,y) for x in range(5) if x % 2 == 0 for y in range(5) if y % 2 == 1]
[(0, 1), (0, 3), (2, 1), (2, 3), (4, 1), (4, 3)]
>>> for x in range(5):
...     for y in range(5):
...         if (x % 2) == 0 and (y % 2 ==1):
...             res.append((x, y))
... 
>>> res
[(0, 1), (0, 3), (2, 1), (2, 3), (4, 1), (4, 3)]
>>> 

1ライナーだから、あまりネストしすぎると判りにくい。
実行速度が速いとか、文法上forが駄目なところもOK(式だから)といった利点もある。
ただ、読みやすさが重要だと思うので、ネストは2つぐらいかな。

ジェネレータ

反復処理に使われるイテレータプロトコルに対応したジェネレータオブジェクトを作成する。
ジェネレータの定義は、関数と同様にdefステートメントで定義するが、returnではなくyeildステートメントを使う
復帰値には、反復毎に返したい値を生成する式やオブジェクトを指定する。

def generater_name(arg1[, arg2, ...]):
    <statement>
    yeild 復帰値

イテレータプロトコルに対応したオブジェクト(イテレータ)は、iter()にシーケンスを渡すことで作成される。
このオブジェクトは、必ずnext()を持っていて、
このメソッドは、オブジェクトの要素に1つずつアクセスし、要素がなくなったところでStopIteration例外を上げる

>>> obj = iter("string")
>>> obj
<iterator object at 0x10049b750>
>>> dir(obj)
['__class__', '__delattr__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__iter__', '__length_hint__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'next']
>>> obj.next()
's'
>>> obj.next()
't'
>>> obj.next()
'r'
>>> obj.next()
'i'
>>> obj.next()
'n'
>>> obj.next()
'g'
>>> obj.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>> 

ジェネレータもイテレータ同様にnext()を持つ。

>>> obj = generate("string")
>>> obj
<generator object generate at 0x1004a3140>
>>> dir(obj)
['__class__', '__delattr__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__iter__', '__name__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'gi_code', 'gi_frame', 'gi_running', 'next', 'send', 'throw']
>>> obj.next()
's'
>>> obj.next()
't'
>>> obj.next()
'r'
>>> obj.next()
'i'
>>> obj.next()
'n'
>>> obj.next()
'g'
>>> obj.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>> 

ジェネレータを使えば、イテレータと同様のことが出来る。
一番の違いは、ジェネレータがnext()が呼ばれる毎にオブジェクトを生成して返すこと。
一度全ての要素を作成した上で、ひとつずつ返すのではなく、毎回毎回生成して返す。
ジェネレータは動作の特性上、省メモリ/演算負荷が小さい。
イテレータの場合は、作成にはシーケンスが必要なため、あらかじめ要素分のメモリが必要

この動作を実現するために、ジェネレータでは値を返した時点のステート情報(ローカルスコープ全体)を保持しておき、
next()が呼ばれたら、ステート情報を再度読み込んで処理を再開し、値を生成し、ステート情報を退避し、復帰値を返す。

ジェネレータ式

ジェネレータオブジェクトを関数を通して作成するのではなく、式として作る。
作り方は、リスト内包表記とほぼ同様だが、角カッコのかわりに丸カッコを使う。
ジェネレータ関数同様に、ジェネレータ式も必要な時に必要な値を返す。
c.f リスト内包表記は一度リストを作成する。

>>> obj = (x + 1 for x in range(5))
>>> obj
<generator object <genexpr> at 0x1010ac280>
>>> dir(obj)
['__class__', '__delattr__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__iter__', '__name__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'gi_code', 'gi_frame', 'gi_running', 'next', 'send', 'throw']
>>> obj.next()
1
>>> obj.next()
2
>>> obj.next()
3
>>> obj.next()
4
>>> obj.next()
5
>>> obj.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>> 
>>> obj = [x + 1 for x in range(5)]
>>> obj
[1, 2, 3, 4, 5]

関数のつまづくところ

ローカルスコープかどうかは代入処理があるかどうかで決まる

処理の順番は関係なく、代入処理がどこかであればそれはローカルスコープ

>>> X = 10
>>> def test():
...     print X
...     X = 20
... 
>>> test()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in test
UnboundLocalError: local variable 'X' referenced before assignment
>>> 

順番が関係あるなら、上はエラーを吐かずに↓みたいになるはず。

>>> test()
10
>>> 

代入しなければ、グローバルスコープの変数を参照するだけになってエラーを吐かない。

>>> X = 20
>>> def test_global():
...     print X
... 
>>> test_global()
20
>>> 

globalはステートメントだから、print global X なんてことは許してもらえない。

>>> def test():
...     print global X
  File "<stdin>", line 2
    print global X
               ^
SyntaxError: invalid syntax

関数の中で、最初はグローバルスコープを使ってて、その後ローカルスコープなんてことは出来ないのか。
って、こんなことをやらないとだめなシチュエーションがないってことか。。。

>>> def test():
...     global X
...     print X
...     X = 10
... 
>>> X
20
>>> test()
20
>>> X
10
>>> 
関数の引数のデフォルト値

とりあえず、下みたいなことが書いてあるが、いまいち判んない。

引数のデフォルト値の評価は、関数オブジェクトを作った時に1回行われるだけ。
関数の呼び出し毎に行われるわけではない。

>>> def saver(x=[]):
...     x.append(1)
...     print x
... 
>>> saver([2])
[2, 1]
>>> saver(['a'])
['a', 1]
>>> saver()
[1]
>>> saver()
[1, 1]
>>> saver([5])
[5, 1]
>>> saver()
[1, 1, 1]
>>> 

saver([2]), saver(['a'])までは想像してた通りの動きをするが、saver()でわけが判らなくなった。
自分の思ってた動きでは、

>>> saver()
['a', 1, 1]
>>> 

さらにいうと、もう一回デフォルト値を[5]で与えた後の、saver()の動きもよくわからん。

>>> saver()
[5, 1, 1]
>>> 

だと思ったのになぁ。

デフォルト値を使う場合と、そうじゃない時でsaver()呼び出し時のローカルスコープxの値が違う!?
デフォルト値を使う場合は、ローカルスコープxの値がどっかに確保されてて、毎回毎回更新されるみたいに見える。

同じように悩んでた方がいた。*2
やっぱり、どうもデフォルト値を使った場合の引数の値は特別な領域に保存される。

>>> dir(saver)
['__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__doc__', '__format__', '__get__', '__getattribute__', '__globals__', '__hash__', '__init__', '__module__', '__name__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'func_closure', 'func_code', 'func_defaults', 'func_dict', 'func_doc', 'func_globals', 'func_name']
>>> saver.func_defaults
([1, 1, 1],)
>>> 

こうやって関数名を使って属性の一覧を表示すると、

  • 関数ってオブジェクト
  • 関数オブジェクトが関数名の変数に代入されている

みたいなことが目に見えてよくわかる気がする。

で、func_defaultsって属性がデフォルト値を使った場合の引数の値を持ってて、これが毎回毎回更新されていく。

>>> def test(x=[]):
...     x.append(1)
...     print x
... 
>>> test.func_defaults
([],)
>>> test()
[1]
>>> test.func_defaults
([1],)
>>> test()
[1, 1]
>>> test.func_defaults
([1, 1],)
>>> test([10])
[10, 1]
>>> test.func_defaults
([1, 1],)
>>> test([1])
[1, 1]
>>> test.func_defaults
([1, 1],)
>>> test()
[1, 1, 1]
>>> test.func_defaults
([1, 1, 1],)
>>> 

でも、こういう状況になるのが嫌なら使ってねってコードだと、

>>> def saver(x=None):
...     if x is None:
...         x = []
...     x.append(1)
...     print x
... 
>>> saver.func_defaults
(None,)
>>> saver([2])
[2, 1]
>>> saver.func_defaults
(None,)
>>> saver()
[1]
>>> saver.func_defaults
(None,)
>>> saver()
[1]
>>> saver.func_defaults
(None,)
>>> 

あら!? デフォルト値がNoneのままだ。
同じようにappendしていってるのに、if文がかましてあるとデフォルト値が累積されない、なんで?
同じように(None, ) -> ([1], ) -> ([1, 1], ) ってなるのを期待してたのに。。。なんで??

その他

applyという関数もあるが、3.0でなくなるかも?みたい。
実際にPEP3100に、To be remobed, use f(*arg, **kw)って書いてあった。

また、実行速度の計測にはtime/timeitモジュールを使う。
timeitの方が簡単にコードが書ける。