Pythonらしいコードの書き方 - Kesin's diary

はてなブログに移行して最初の記事はやはりPythonネタにしました。 はてなブログいいですね。デザインの編集がやりやすくなったのと、Markdownで書けるのが素晴らしいです。

PyCon 2013の動画を見ていたら、素晴らしい"Transforming Code into Beautiful, Idiomatic Python"という発表を見つけたのでそのまとめです。
今どきのPythonコードのベターな書き方を紹介しています。


Transforming Code into Beautiful, Idiomatic Python ...

スライドはこちらにありました

結構長くなってしまったので、知ってる項目は読み飛ばしてもらえばと思います。

整数のループ

まずは基本のループ。 Cのfor int i=0; i<6; i++Pythonで単純に書くとこうなります。

for i in [0,1,2,3,4,5]:
    print i**2

リストの部分は通常range(6)に置き換えますが、iの値がとてつもなく大きい値までループで回した場合にはメモリを消費しすぎてしまうという問題があります。

多くの人がすでに知っているとは思いますが、xrangeを使うのがベターです

for i in xrange(6):
    print 1**2

xrangeはrangeと違って一気にメモリを確保しないので、メモリが節約できます。
動画中では、xrangeという名前は醜い!と言って笑いを取っていましたw
ちなみにPython3ではrangeがxrangeと同様の動きをするようになりましたので、rangeを使用してOKです。

コレクションのループ

リストなどの要素をループ中で取得したい場合、他の言語から移ったばかりの初心者はこのように書きがちです。

colors = ['red', 'green', 'blue', 'yellow']
for i in range(len(colors)):
    print colors[i]

Pythonのfor文はこのように他の言語のforEachと同様の働きもします。

colors = ['red', 'green', 'blue', 'yellow']
for color in colors:
    print color

for文では英語の単数形と複数形を使うことで、何をループさせているのかひと目で分かりやすくさせるのがお作法になってます。

逆順のループ

colors = ['red', 'green', 'blue', 'yellow']
for i in range(len(colors)-1, -1, -1):
    print colors[i]

reversedを使うことで何をしているのか分かりやすくなります。

colors = ['red', 'green', 'blue', 'yellow']
for color in reversed(colors):
    print colors

むしろ最初のrangeの例を使う人なんているのかな・・・

コレクションのループと同時に要素番号も取得したい

わりとよくあるパターンだと思います。

colors = ['red', 'green', 'blue', 'yellow']
for i in range(len(colors)):
    print i, '-->', colors[i]

これはenumerateを使うことでスッキリ書くことができます。

colors = ['red', 'green', 'blue', 'yellow']
for i, color in enumerate(colors):
    print i, '-->', color

同時に二つのコレクションをループさせたい

二つのリストから同じ要素番号の要素を取ってきて、要素数が少ない方に合わせてループを終了したいというパターンです。

names = ['raymond', 'rachel', 'mattew']
colors = ['red', 'green', 'blue', 'yellow']
n = min(len(names), len(colors))

for i in range(n):
    print names[i], '-->', colors[i]

複数のリストをまとめてくれるzipを使うとスッキリ書くことができます。
(実際にはイテラブルなものであれば、リストじゃなくても大丈夫です)

for name, color in zip(names, colors):
    print name, '-->', color

zipもrange同様、要素が大きすぎる場合にはメモリの問題があるので、そのようなときはzipをitertoolsモジュールのizipに置き換えるといいです。Python3ではrange同様、zipがizipと同じ動作をするように変更されているそうです。

ちなみにitertoolsにはizip以外にも便利なものが揃っているので、一度はドキュメントに目を通すといいと思います。combinationsとかproductを知った時には目からうろこが落ちました。

複数のループ終了条件

ここからは少し複雑なループの使い方になります。
ループ中で一つも条件に合わなかった時、例外的な値を返したいという処理はよくあると思います。ついフラグを使いたくなってしまうパターンですね。

def find(seq, target):
    found = False
    for i, value in enumerate(seq):
        if value == tgt:
            found = True
            break
    if not found:
        return -1
    return i

フラグ変数が入るとコードが見難くなり、何をやっているのか分かりにくくなりがちです。
このような場合はfor文の中でelseを使うことでフラグを使わなくてもエレガントに書けます。

def find(seq, target):
    for i, value in enumerate(seq):
        if value == tgt:
            break
    else:
        return -1
    return i

し、知らなかったーー!! elseはfor文がbreakされずに正しく終了したときに実行されるらしいです。

何かとお世話になる辞書(dict)ですが、知ってるとエレガントにコードが書けるテクニックがいくつかあるそうです。

キーでのループ

まずは基本。dictでループを回すとキーの値を取得できます。

d = {'matthew': 'blue', 'rachel': 'green', 'raymond': 'red'}
for k in d:
    print k

キーとバリューでのループ

キーが取得できるのだから、単純に思いつくのはこのような形です。

for k in d:
    print k, '-->', d[k]

辞書オブジェクトのitems()を使うとd[k]をする必要がなくなり、変数名を自由に付けられるのでコードの見通しがよくなります。

for k, v in d.items():
    print k, '-->', v

ちなみにrange, zip同様、辞書のサイズが大きいとメモリを消費してしまうので、そのような場合はiteritems()を使うとイテレーターで取得するのでメモリの節約になります。

for k, v in d.iteritems():
    print k, '-->', v

ペアから辞書の作成

キーとバリューの候補が既にリスト等で存在する場合は、for文などを使わなくても一発で辞書に変換できます。

names = ['raymond', 'rachel', 'mattew']
colors = ['red', 'green', 'blue']
d = dict(izip(names, colors))

>>> d
{'rachel': 'green', 'mattew': 'blue', 'raymond': 'red'}

辞書によるカウント

自然言語処理などでは、どの単語が文書に何回登場したかを数えたいことがよくあります。
辞書はお手軽なのでよく使いますが、新しい単語が登場したときに新たなキーとして登録しないとエラーになるため、少し工夫をする必要があります。

colors = ['red', 'green', 'red', 'blue', 'green', 'red']
d = {}
for color in colors:
    if color not in d:
        d[color] = 0
    d[color] += 1

>>> d
{'blue': 1, 'green': 2, 'red': 3}

辞書オブジェクトのgetを使うと、キーが存在しなかった場合は引数で渡した値をバリューとしてセットしてくれるので、上のコードのfor文内は1行で済みます。

d = {}
for color in colors:
    d[color] = d.get(color, 0) + 1

さらに便利なのがcollectionsモジュールのdefaultdictです。defaultdictはキーが存在しなかったときに、自動的に作成するオブジェクトを指定できます。
し、知らなか(ry

d = defaultdict(int)
for color in colors:
    d[color] += 1

defauldict(int)とはどのような意味でしょうか?手元でインタプリタを起動して確認してもらえばと思いますが、int()は0を返します。

>>> int()
0

つまり、defultdict(int)は登録されていないキーが呼び出された時に、自動的に{キー: 0}を登録してくれます。

動画では紹介されていませんが、個人的にはカウンタとして使用するなら同じくcollectionsモジュールのCounterの方がより直感的でオススメです。

辞書によるグルーピング

辞書のバリューをリストにするという、よくあるケースです。
例えば、文字列の長さごとにグルーピングすると、こんな感じになると思います。

names = ['raymond', 'rachel', 'matthew', 'ronger', 'betty', 'melissa', 'judith', 'charlie']
d = {}
for name in names:
    key = len(name)
    if key not in d:
        d[key] = []
    d[key].append(name)

>>> d
{5: ['betty'], 6: ['rachel', 'ronger', 'judith'], 7: ['raymond', 'matthew', 'melissa', 'charlie']}

カウントの初期値を0にしたのと同様に、キーが存在しなかったときに空のリストをバリューとして登録する必要があります。
これは辞書オブジェクトのsetdefaultで置き換えることができます。

d = {}
for name in names:
    key = len(name)
    d.setdefault(key, []).append(name)

if文の処理を圧縮することができましたが、発表中ではこれですら美しくないということでdefaultdictを使うことをオススメしていました。

d = defaultdict(list)
for name in names:
    key = len(name)
    d[key].append(name)

これもカウントのときにint()が0を返すのと同様に、

>>> list()
[]

list()が空のリストを返すことを活用しています。
辞書へのアクセスが通常と同じd[key]なので、setdefaultより直感的に理解しやすいでしょう。

キーワード引数

Pythonは関数への引数に '引数名=値' という書き方が可能です。タイプ量は増えますが、引数の名前が分かると関数の振る舞いに予想がつくので可読性が向上します。

twitter_search('@obama', False, 20, True)

twitter_search('@obama', retweets=False, numtweets=20, popular=True)

キーワード引数を使うことで、関数の定義を確認しなくてもFalseやTrueが何を設定しているのかひと目で分かります。

名前付きタプル

動画中のサンプルだと分かりづらいので、Python 2.4 Advent Calendar 2012 (6) namedtuple がないが分かりやすくて参考になります。

こちらでも簡単なサンプルを示します。defaultdict同様、collectionsモジュールをインポートするのを忘れないでください。

Point = namedtuple('Point3d', 'x y z')
point = Point(10,20,30)

>>> point
Point3d(x=10, y=20, z=30)
>>> print point.x, point.y, point.z
10 20 30
>>> print point[0], point[1], point[2]
10 20 30

このように通常のタプルの振る舞いに加えて、オブジェクトのように参照することが可能になります。ちょっとした構造体のようなものが欲しいときに便利です。

し、知らなかったーー!!というか公式ドキュメント見た時にイマイチ利点が分からなかったので忘れてた。

展開して代入

有名なので解説は必要ないでしょう。0x30がユーモアですねw

p = 'Raymond', 'Hettinger', 0x30, 'python@example.com'
fname = p[0]
lname = p[1]
age = p[2]
email = p[3]

fname, lname, age, email = p

実は応用すると代入が簡単に行える以外にも便利な場面があります。
フィボナッチ数列を求める関数を例とすると、

def fibonacci(n):
    x = 0
    y = 1
    for i in range(n):
        print x
        t = y
        y = x + y
        x = t

tを一時変数として利用するプログラマーには非常にありふれた方法ですが、直感的に分かりづらいこともあります。
このように書きなおすことで、一時変数を使用する必要はなくなります。

def fibonacci(n):
    x, y = 0, 1
    for i in range(n):
        print x
        x, y = y, x+y

x, yが同時のタイミングで代入されるため、このような書き換えが可能になっています。

その他

時間の都合上駆け足になっていましたが、その他にもjoin, deque, デコレータ、with文、リスト内包表記、ジェネレーターなどが紹介されてました。 デコレーターは未だに自分で実装して使いこなすことができないです・・・。