そうだ! EuroPython 2011へ行こう

#2 CPythonについてのハンズオン,講演

この記事を読むのに必要な時間:およそ 12 分

前回は基調講演とカンファレンスの全体像について紹介しました。今回と次回ではPythonそのものの講演について紹介したいと思います。

今回はCPythonというC言語で実装されたPythonの講演(一般的にPythonというときはCPythonを指します)について紹介します。

CPython

CPythonの講演で紹介するのは,すべてRaymond Hettinger氏が発表されたことについてです。Raymond氏の講演は,Pythonでコーディングする上でのテクニックや考え方など,どれも示唆に富む内容で筆者にとっては興味深かった内容でした。

Raymond Hettinger氏

Raymond Hettinger氏

特に注記がない場合,サンプルコードはPython 2.7.2で実行しています。

Advanced Python: 高度なPythonテクニック

「Advanced Python」というハンズオントレーニングでは,サンプルコードを見ながら,自分でコーディングしてみるといった,実際に手を動かすプログラムがありました。このハンズオンから,いくつかTIPSを紹介します。

なお,本講演のAdvanced Pythonには,事前説明のビデオはありますが,実際のハンズオンのビデオがまだアップロードされていないようです。

ハンズオン

ハンズオン

最適化

3つの最適化のテクニックについて紹介されていました。

  • グローバルな参照(組み込み関数,モジュール,グローバル変数)をローカルの参照に置き換える
  • 束縛メソッドを使う
  • 内部ループにおけるピュアPythonの関数呼び出しを最小化する

まずは,グローバル参照と束縛メソッドの2つをdisモジュールを使って,Pythonのバイトコードにディスアセンブルして検証してみます。

>>> from dis import dis
>>> class Foo(object):
...   def bar(self):
...     pass

>>> f = Foo()
>>> dis(lambda: f.bar())
  1  0 LOAD_GLOBAL    0 (f)
     3 LOAD_ATTR      1 (bar)
     6 CALL_FUNCTION  0
     9 RETURN_VALUE

>>> dis(lambda given_f_bar: given_f_bar())
  1  0 LOAD_FAST      0 (given_f_bar)
     3 CALL_FUNCTION  0
     6 RETURN_VALUE

無名関数でf.barを直接参照しようとした場合と,引数として渡すと仮定したときのバイトコード命令の比較です。後者の引数として渡すと仮定するとLOAD_GLOBALLOAD_ATTRの命令がなくなります。

Pythonは関数(メソッド)がファーストクラスオブジェクトなので,ローカル変数に割り当てることができます。この特性を利用して,例えば,ループ内でのグローバルな参照や属性参照をループの外へ切り出すことで高速化を図ることができます。Pythonの内包表記はなぜ速い?の記事がとても分かりやすいです。

3つ目の,内部ループにおけるピュアPythonの関数呼び出しを最小化するというのは,なるべく組み込み関数やC言語で実装された関数を使うということです。

例えば,2つの引数を受け取って乗倍して返す組み込み関数pow()と,無名関数で実装した場合を比較すると違いが明らかです。

$ python -m timeit "map(lambda x, y: x ** y, [1,2,3], [1,2,3])"
1000000 loops, best of 3: 1.85 usec per loop

$ python -m timeit "map(pow, [1,2,3], [1,2,3])"
1000000 loops, best of 3: 1.38 usec per loop

さらにベクトル化により,リスト内包表記よりもmap()の方が速いようなことも言及されていました。例えば,次のようなコードです。

>>> [ord(i) for i in string.letters]

>>> # こういった処理は map() の方が速い?
>>> map(ord, string.letters)

筆者の環境(Mac OS X 10.6.7)で検証してみたところ,組み込み関数を使う場合はmap()の方が速いのですが,ピュアPythonの関数だと,リスト内包表記の方が速かったりします。Python List Comprehension Vs. Mapでも議論されているので,そちらも参考にしてください。

時間計測

timeitモジュールを利用するサンプルの1つとして,任意の関数の実行時間を計測するサンプルプログラムです。

def use_listcomp(data):
  return [i ** 2 for i in data]

def use_itertools(data):
  from itertools import imap, repeat
  return list(imap(pow, data, repeat(2)))

if __name__=='__main__':
  from timeit import Timer
  from random import random

  n = 10000
  data = [random() for i in range(n)]

  setup = "from __main__ import use_listcomp, use_itertools, data"
  for func in use_listcomp, use_itertools:
    stmt = '{0.__name__}(data)'.format(func)
    print func.__name__, min(Timer(stmt, setup).repeat(7, 20))

このプログラムを実行すると,リスト内包表記を使った処理とitertoolsモジュールのツールを使った処理の時間を計測して,実行時間が最も小さかったものを表示します。

$ python get_timing.py
('use_listcomp', 0.022819042205810547)
('use_itertools', 0.028668880462646484)

筆者は,普段IPythonのtimeitの仕組みで実行時間を計測していますが,こういうやり方もあるんだなと参考になりました。

著者プロフィール

森本哲也(もりもとてつや)

一介のプログラマ。

自分で設計して,自分で開発して,自分で直せるような独立したプログラマを目指している。OSSコミュニティのゆるい人のつながりが性にあっていてPythonプログラミングが好き。共訳書に『エキスパート Pythonプログラミング』(アスキーメディアワークス)がある。

Twitter:@t2y

ブログ:http://d.hatena.ne.jp/t2y-1979/

コメント

コメントの記入