エキスパートPythonプログラミング読書会 第二期 02に行ってきてました

もう1ヶ月ほど前になっちゃったけど、エキスパートPythonプログラミング読書会 第二期 02 - connpassに参加してました。Pythonの環境構築の自分なりのまとめ+エキスパートPythonプログラミング読書会 第二期 01に行ってきました。 - kanonjiの日記に続けて第2回目です。なんか忙しくてブログ書けなかったけど、重要な章を進めた回だったから、思い出しながら書いてみます。

下書きで少しずつ書いてたら、濃い回だったので、結局書き上がったのが3回目の1時間前という・・・

感想

前回の第1章が環境構築だったので、今回は2章からという事になります。エキPyは後半になるほど、テストとかコード管理とかPythonそのものより、Pythonを取り巻く周辺について書いた章が増えていきます。逆に2章からPython独特の構文の説明が始まり、2章3章が一番難しいんだとか。実際かなり質問が多く、進んだのは2章の途中まででした。
そんなとこを1ヶ月もメモせずにいたのは、失敗だったなぁ。

注意

読書会で解説を聞きながら書いたメモを元にこのエントリーを書いています。仕組みを理解せず聞いた事を書いてたり、それがもしかしたら聞き間違いや勘違いしてる可能性もあるとおもうので、このエントリーはそんなくらいの正確さだと思います。
あと、間違いの指摘や補足などあったらコメント頂けるとうれしいです。

リスト内包表記

import keyword
["%d_%s" % x for x in enumerate(keyword.kwlist)]
[func(i,k) % i,k for x in enumerate(keyword.kwlist)]

こんなやつ。

リスト内包表記は実行効率が高い。

なんかインタープリタ内部でごにょごにょするから速いらしいですが、現実問題として若干なので、読みやすさを犠牲に無理にリスト内包表記にするべきではない。

map(func, enumerate(keyword.kwlist))

こういう書き方もあるけど、リスト内包表記のほうが後から登場した構文で、やっぱりリスト内包表記が速いらしい。

短く書ける

普通にfor文を書くより短く書けて分かりやすくなります。ただ、ネストするような場合とか、複雑になると逆に読みにくくなるので、上記の通り無理して使うべきじゃない。

組み込み関数enumerate()
i = 0
seq = ["foo", "bar", "baz"]
for element in seq:
    seq[i] = '%d: %s' % (i, element)
    i += 1
seq = ["foo", "bar", "baz"]
for i, element in enumerate(seq):
    seq[i] = '%d: %s' % (i, element)

enumerate()を使うと、シークエンスのインデックスを簡単に取得できて、Pythonicに書けるんだとか。リスト内包表記でもfor文でも使えるなら使いたい。Pythonはインクリメントが無いので、そういった意味でもenumerate()を使うべきなのかな?

Pythonic
import this

Pythonらしい的な意味だと思う。これを実行すると、Pythonicとは何かを説明するThe Zen of Pythonという文章が英語で表示されるらしい。いわゆるイースターエッグhttp://www.python.jp/Zope/articles/misc/zenに日本語訳があります。

イテレータ

StopIteration

シークエンスの最後になると、StopIteration例外が発生します。for文はStopIterationでループを終わるようになっていて、例えばbreakなんかもStopIterationを発生させるんだとか。

プロトコル

StopIteration例外とか、イテレーターとして必要になるnext()__iter__()を適切に実装する事で、独自のイテレーターを実装できる。イテレーターとして必要なものが書かれてればイテレーターという、ダックタイピング的な考え。
さらに、イテレータプロトコルというものがあって、それに沿っているべきという約束となっているらしい。プログラムの世界のイテレーターというのを、Pythonではこうなるという約束がプロトコルで、それをプログラム的に保証したりするのがABCs*1
独自に、イテレーターは実装できるし、ダックタイピングとしてイテレーターになるけど、イテレータプロトコルに沿っていると、他の人から分かりやすいし使いやすくなる。

http://www.python.jp/doc/release/c-api/abstract.html

正直なところ、ちゃんとは理解しきれなかった。ドキュメントのここに、6個のプロトコルが書かれている。これだけなのか、他にもあるのかは不明。for文はイテレータプロトコルとシーケンス型プロトコルに対応しているとの事なので、プロトコルに沿う事でfor文やリスト内包表記で扱えるとも言えるのかも。

アダプタ

具体例から書くと、イテレータプロトコルに対して、next()がアダプタになる。プロトコルに対してアダプタを介して問い合わせるという考え。

勉強会の最中に書いたメモにはlen()はレングスプロトコルのアダプタというメモがあるんだけど、どうなんだろう。前述の6個のプロトコルには無いし、この辺はちゃんと理解しきれてる気がしない。

あと、ここで言うアダプタってのはアダプタパターンの事らしい。

ジェネレーター

関数をイテレータブルにするようなもの。本にはフィボナッチ数列を計算する例があります。

  1. フィボナッチ数列は無限に計算する事が可能なので、計算結果である数列を配列に入れてreturnできない。
  2. フィボナッチ数列は1個前の数値を計算に使うので、数列のn番目の数を求めるのに最初から計算する必要がある。

こういった事から、フィボナッチ数列の計算には、ジェネレーターが適しているようです。

def fibonacci():
    a, b = 0, 1 //タプル代入
    while True:
    yield b
    a, b = b, a + b

fib = fibonacci()
next(fib) //1
next(fib) //1
next(fib) //2
[next(fib) for i in range(10)]
//[3,5,8,13,21,34,55,89,144,233]

ジェネレーターが作るのは特殊なイテレーターで、ジェネレーターオブジェクトと呼ばれる。この例の場合fibがジェネレーターオブジェクトで<generator object fibonacci at 0x100~~~の様に表現される。

使い道とか

フィボナッチ数列の計算は前回の数値を使って計算するので、メモリすごい使う事になるけど、ジェネレーターだとnext()都度計算して、古い値は捨ててるのでメモリ食わないんだとか。
使い道としては、大量にあるログの処理とかに便利らしい。あとは、10万行あるDBから読み出す際、全部読んでからカーソルを動かしてってのだとメモリを圧迫するので、ジェネレーターでラップして、必要ないレコードは捨てつつ読むとかで、使った事があると話してました。
本では、ループ処理を書く場合や、シーケンスを返す関数を実装する際には、ジェネレーターが使えないか考える事を推奨してました。

yield

ジェネレーターオブジェクトは、書かれたyieldの数だけの要素を持っている様なものになります。上記の例だと、無限にループするwhileの中にyieldがあるので、無限に要素を持っている様なものです。

def foo():
    bar = 1
    yield bar
    bar = 2
    yield bar
    bar = 3
    yield bar

gen = foo()

例えばこの場合、yieldが3回分しかないのでgenは3回next()したら、ジェネレーターは終わった事になる。

python 2.5からのsend
>>> def foo():
...     for b in range(3):
...         a = yield b
...         print a

>>> f = foo()
>>> f.next()
0       //bの出力
>>> f.send('bar')
bar    //aの出力
1       //bの出力
>>> f.next()
None //aの出力
2       //bの出力
>>> f.next()
None //aの出力
Traceback (most recent call last):
  File "<input>", line 1, in <module>
StopIteration
>>> f.next()
Traceback (most recent call last):
  File "<input>", line 1, in <module>
StopIteration
>>> 

Python 2.5からsendでジェネレーターに値を渡せるようになった。

  1. aは、sendでジェネレーターに渡した値を受け取るもの
  2. bは、yieldで返すもの
最低1回はnext()してからsend()が使える
ok = my_generator()
ok.next()
ok.send('foo')

ng = my_generator()
ng.send('foo')

ジェネレーターオブジェクトを作って、いきなりsend()は使えない。1回はnext()して、yieldのとこまで処理を進める必要がある。

yieldは文から式に

Python 2.5からsend()が登場したため、yieldは文から式になった。従来はreturnと同じく文という扱いだったが、send()で送られてきた値を受け取るという代入の様な動きをするので式になった。

if( foo = bar() ) //ダメ

Pythonでは文と式が明確に分かれているので、例えばif文の中で代入とかはできない。

コルーチン

Pythonにはスレッドがあるけど、ジェネレーターとsend, throw, closeを使う事で、コルーチンの様なものが実装出来るらしい。
コルーチンはマルチスレッドじゃないけど並列実行する仕組みで、組み込みとかでスレッドがそもそもないけど、マルチタスクにしたいという時に使うらしい。

例えば、ジェネレーター2つ用意して相互に値をやり取りするんだとか。

  1. 1個目のジェネレーターは実際のタスクを持って、重たい処理なら細かくyieldで返す、軽い処理はある程度まとめて実行してからyieldする。
  2. 2個目のジェネレーターは、次に実行する処理のidとかを返す。
ジェネレーター式

リスト内包表記と似た形でジェネレーターが使える。

[x for x in enumerate(kw)]
//全部が一気に出てしまう。
iter = (x for x in enumerate(kw))
//逐次実行できる
iter.next()
for z in [x for x in enumerate(kw)]: //リスト内包表記が先に処理されて全部メモりにのっちゃって無駄
     print z

for z in (x for x in enumerate(kw)): //メモリが無駄じゃない
     print z
def foo(seq):
     for x in seq:
          print x
//シークエンスを受ける関数foo()があって

foo([x.upper() for x in kw[:5]]) //リスト内包表記でもいいけど、
foo((x.upper() for x in kw[:5])) //ジェネレーターならメモリ消費が少ない
foo(x.upper() for x in kw[:5]) //さらにこの場合、Pythonのシンタックスとして()を省略出来る
xrange()の話
range(10)
//リストが生成される
xrange(10)
//イテレータブルなオブジェクトが生成される

リスト内包表記や普通のシークエンスと、ジェネレーターの比較と同じように、xrange()の方がメモリ効率がいい。ただしPython3ではrange()がxrange()の動きをするようになり、xrange()が消えるんだとか。加えて、よほど広いレンジで生成しない限り、メモリ消費の差は少ないのでrange()使っておいた方がPython3への移行に手間がかからないという話も。

リスト内包表記が出る前についての余談
filter(lambda x:x%2, range(10))//リスト内包表記が出来る前はこれでやってた
[1,3,5,7,9]
[x for x in range(10) if x % 2]
[1,3,5,7,9]

map() reduce() filter()はリスト内包表記で置き換えられつつあるらしい。

その他のリンクなど

  1. エキ Py 読書会02 2010/9/7
  2. 2.Adapter パターン 1 | TECHSCORE(テックスコア)
  3. import operator //四則演算などPythonのオペレーターが関数化されているモジュール

*1:abstract base classes