gae アプリ 開発メモ

Google App Engine アプリの開発メモ / 言語: python, javascript / ビギナー

ダイアモンド継承とsuper()

python 2.7 の話。

まずはダイアモンド継承を書く

class A(object):
    def talk(self):
        print 'This is A.'

class B(A):
    def talk(self):
        print 'This is B'

class C(A):
    pass

class D(B, C):
    pass

def main():
    d = D()
    d.talk()

if __name__ == '__main__':
    main()

結果は

This is B

継承順序を入れ替えてみる

class D(C, B): # 順序を入れ替えてみる
    pass

def main():
    d = D()
    d.talk()

結果は

This is B

C.talk() = A.talk()
ではないらしい。

super()で C.talk() が呼び出されるように指定してみる

class D(B, C):
    def talk(self):
        super(C, self).talk() # C を明示する

def main():
    d = D()
    d.talk()

結果は

This is A.

C.talk() = A.talk()
となったみたい。

プロパティの作り方

下の書き方、どちらでもOK。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

class A(object):
    def __init__(self):
        self._x = 0

    def get_val(self):
        return self._x

    def set_val(self, value):
        self._x = value

    value = property(get_val, set_val)

class B(object):
    def __init__(self):
        self._x = 0

    @property
    def value(self):
        return self._x

    @value.setter
    def value(self, value):
        self._x = value

def main():
    a = A()
    print a.value
    a.value = 1
    print a.value

    b = B()
    print b.value
    b.value = 1
    print b.value

if __name__ == '__main__':
    main()

デコレータ

なんの気なしに @classmethod とか @staticmethod とか使っていたけど、なかなかの仕組みらしい。

デコレータとは

Pythonの用語集(decorator)より。

(デコレータ) 関数を返す関数。
通常、 @wrapper という文法によって関数を変換するのに利用されます。
デコレータの一般的な利用として、 classmethod() と staticmethod() があります。

デコレータ = ただの関数だという柔軟性にビックリ。
要は必要があれば、自分で定義できちゃうってことだ。

デコレータの制約は

  • 関数を引数とする
  • 関数を返す

というシンプルな制約のみ。

だから、デコレータには

  • __call__() を持つオブジェクト
  • __call__() を持つクラス

を指定することもできる。

追記
関数は返さなくてもよい。property()がそれ。

特に、クラスを指定した場合、

  1. 指定した関数はコンストラクタの引数にしてオブジェクトが生成される
  2. 指定した関数を呼び出すと、生成したオブジェクトの __call__() が呼び出される

といった振る舞いになる。

使い道

PythonDecoratorLibrary

  • State Machine Implementaion
  • Singleton
  • Synchronization

など実用的な使い道が公開されていた。

デコレータを実行するタイミング

@wrapper の表記で書いたデコレータが実行されるのは、@wrapper が読み込まれた時。
一般的には、関数の【実行時】ではなく、関数の【定義時】にデコレートする。
以下、サンプルコード。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

def deco_fn(fn):
    print 'deco_fn'
    return fn

class deco_class(object):
    def __init__(self, fn):
        print 'deco_class.__init__()'
        self._fn = fn

    def __call__(self, *args):
        print 'deco_class.__call__()'
        self._fn(*args)

class A(object):
    @deco_fn
    def foo(self):
        print 'foo'

@deco_class
def boo():
    print 'boo'

def main():
    print '---'
    print 'execute start.'
    a = A()
    a.foo()
    boo()
    print 'execute end.'

if __name__ == '__main__':
    main()

で、結果。

deco_fn
deco_class.__init__()
---
execute start.
foo
deco_class.__call__()
boo
execute end.

もー、複雑すぎてワクワクする仕組みだ。

ファイルの読み込みと for~in

当たり前といえば、当たり前なんだけど。

ファイルの読み込みで for~in を途中で break して、また同じファイルに for~in すると続きを読むことができる。

with open(filename, 'r') as source:
    for line in source:
        out.write(line)  # 1 行目を出力
        break;

    for line in source:
        out.write(line)  # 2 行目以降を出力

ジェネレータ関数に対する for~in でも同じ挙動。

def num():
    for n in range(10):
        yield n

def boo():
    b = num()
    for n in b:
        print n  # 0 を出力
        break

    for n in b:
        print n  # 1 以降を出力

listやdict.iter*()などは常に先頭から返しなおす。

def foo():
    a = [1, 2, 3]
    for n in a:
        print n  # 1 を出力
        break

    for n in a:
        print n  # 1 から出力しなおす
def foo():
    a = {'abc': 0, 'def': 1, 'xyz': 3}
    for n in a.iterkeys():
        print n  # 'abc' を出力
        break

    for n in a.iterkeys():
        print n  # 'abc' から出力しなおす

Pythonで継承元のメソッドを呼び出すには

伝統的な書き方

どんなメソッドを読んでいるのかが明示的。

class C(B):
    def method(self, arg):
        B.method(self, arg)

super() を使用する

なんか便利らしい。
要勉強。

class C(B):
    def method(self, arg):
        super(C, self).method(arg)

一時的なクラスオブジェクトを使用する

super() の代替方法。
メリットは継承元の指定を一定に書けるけど、それがよい事かはまた別の話。

base = B
class C(base):
    def method(self, arg):
        base.method(self, arg)
base = None

Python は関数を呼ぶコストがハンパない

テキストファイルを1バイトずつ走査するテストコードを書いてみた。
テストするテキストファイルのサイズは 6,811,956 バイト。

まずは元のソース。

def main():
    buffer = ''
    filei = open('sample.txt', 'r')
    while True:
        read_buffer = filei.read(0x2000)
        if read_buffer == '':
            break

        index = 0
        length = len(read_buffer)
        while index < length:
            buffer += read_buffer[index]
            index += 1
    filei.close()

    fileo = open('out.txt', 'w')
    fileo.write(buffer)
    fileo.close()

if __name__ == '__main__':
    main()

profile した結果、

         1622 function calls in 5.103 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        2    0.000    0.000    0.000    0.000 :0(close)
      806    0.001    0.000    0.001    0.000 :0(len)
        2    0.001    0.000    0.001    0.000 :0(open)
      807    0.041    0.000    0.041    0.000 :0(read)
        1    0.007    0.007    0.007    0.007 :0(setprofile)
        1    0.024    0.024    0.024    0.024 :0(write)
        1    0.000    0.000    5.096    5.096 RLE-SA~1.PY:7(<module>)
        1    5.029    5.029    5.096    5.096 RLE-SA~1.PY:7(main)
        1    0.000    0.000    5.103    5.103 profile:0()
        0    0.000             0.000          profile:0(profiler)

ここに空関数を足してみる。

# ↓ コレを追加
def foo():
    pass

def main():
    buffer = ''
    filei = open('sample.txt', 'r')
    while True:
        read_buffer = filei.read(0x2000)
        if read_buffer == '':
            break

        index = 0
        length = len(read_buffer)
        while index < length:
            buffer += read_buffer[index]
            index += 1
            foo()  # ← コレを追加
    filei.close()

    fileo = open('out.txt', 'w')
    fileo.write(buffer)
    fileo.close()

if __name__ == '__main__':
    main()

profile した結果はこちら。

         6598988 function calls in 14.462 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        2    0.000    0.000    0.000    0.000 :0(close)
      806    0.001    0.000    0.001    0.000 :0(len)
        2    0.001    0.000    0.001    0.000 :0(open)
      807    0.042    0.000    0.042    0.000 :0(read)
        1    0.007    0.007    0.007    0.007 :0(setprofile)
        1    0.023    0.023    0.023    0.023 :0(write)
        1   10.526   10.526   14.454   14.454 RLE-SA~1.PY:10(main)
        1    0.000    0.000   14.455   14.455 RLE-SA~1.PY:7(<module>)
  6597366    3.861    0.000    3.861    0.000 RLE-SA~1.PY:7(foo)
        1    0.000    0.000   14.462   14.462 profile:0()
        0    0.000             0.000          profile:0(profiler)

5.103 seconds → 14.462 seconds って…。
プロファイルの結果によると、ざっくり考えても

  • 空関数の foo の実行時間は 3.861 seconds
  • 関数の呼び出しコストは 5.498 seconds

ってこと。

速度の最適化を考える場合、『ループの中の関数呼び出しを減らす』のは Python には効果てきめんのようだ。