初めてのPython(24章中編)

演算子オーバーロード

__getitem__ と __iter__

どちらも反復処理を実現させるのに使う。

反復処理には次のようなものが該当する。

  • forループ
  • in演算子によるメンバの存在確認
  • タプル/リストのアンパック代入

どっちの演算子オーバーロードでも反復処理を実現できるけど、反復処理を行う時に検索される順番は、

  1. __iter__
  2. __getitem__

__getitem__の場合

[kobakoba0723@fedora13-intel64 ~]$ cat loop_index.py
#!/usr/bin/env python

class C1(object):
  def __init__(self, string):
    self.data = string
  def __getitem__(self, index):
    return self.data[index]
  
if __name__ == '__main__':
  x = C1("spam")
  
  for i in range(4):
    print 'x[%d]: %s' % (i, x[i])
  for data in x:
    print data, 

[kobakoba0723@fedora13-intel64 ~]$ python loop_index.py
x[0]: s
x[1]: p
x[2]: a
x[3]: m
s p a m
[kobakoba0723@fedora13-intel64 ~]$

__iter__の場合
イテレータプロトコルの場合、反復処理は次の要領で行われる。

  1. ビルドイン関数iterが呼び出される
  2. __iter__ メソッドが実行され、イテレータオブジェクトが返される
  3. イテレータオブジェクトのnextメソッドが呼ばれる(StopIteration例外が発生するまで繰り返し)

なので、__iter__メソッドをオーバーロードする場合、nextメソッドも定義しておく必要がある。

[kobakoba0723@fedora13-intel64 ~]$ cat loop_iter.py
#!/usr/bin/env python

class C1(object):
  def __init__(self, string):
    self.data = string
    self.index = 0
  def __iter__(self):
    return self
  def next(self):
    if self.index == len(self.data):
      raise StopIteration
    result = self.data[self.index]
    self.index += 1
    return result

if __name__ == '__main__':
  x = C1('spam')
  
  for data in x:
    print data,
  
[kobakoba0723@fedora13-intel64 ~]$ python loop_iter.py
s p a m
[kobakoba0723@fedora13-intel64 ~]$ 

__getitem__ と __iter__ の違い
同じようなことができるけど、__iter__は1回反復処理を実施してしまうと2回目以降には対応できない。
新しくイテレータオブジェクトを作らないといけない。
イテレータプロトコルの場合は、イテレータオブジェクトの要素を消費しながらループを回しているけど、
インデクシングの場合は、単純に要素を指定してその値を返しているだけだから何度でもループを回すことが出来る。

[kobakoba0723@fedora13-intel64 ~]$ python
Python 2.6.4 (r264:75706, Apr  1 2010, 02:55:51) 
[GCC 4.4.3 20100226 (Red Hat 4.4.3-8)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import loop_index
>>> import loop_iter
>>> x = loop_index.C1("spam")
>>> [n for n in x]
['s', 'p', 'a', 'm']
>>> [n for n in x]
['s', 'p', 'a', 'm']
>>> y = loop_iter.C1("spam")
>>> [n for n in y]
['s', 'p', 'a', 'm']
>>> [n for n in y]
[]
>>> 

ただ、次のように__iter__で新しいオブジェクトを毎回返すようにしておけば対応できる

  def __iter__(self):
    return C1(self.data)
__getattr__ と __setattr__

属性へのアクセス/値の代入に対応するフックメソッド

通常のアクセス、値の代入とは少し違う動きをするので注意が必要

  • __getattr__が呼ばれるのは、属性の検索が実施されてヒットしなかった時だけ
  • __setattr__の中でインスタンス属性への値を代入すると無限ループになる*2ので、self.__dict__['属性名'] = 値 を使う
[kobakoba0723@fedora13-intel64 ~]$ cat getattr.py
#!/usr/bin/env python

class C1(object):
  def __init__(self, value):
    self.name = value
  def __getattr__(self, attrname):
    if attrname == 'age':
      print '(in __getattr__ method)',
      return 28
    else:
      raise AttributeError, attrname

if __name__ == '__main__':
  x = C1("taro")
  
  print 'name:', x.name
  print 'age:', x.age
  print 'address: ', x.address

[kobakoba0723@fedora13-intel64 ~]$ python getattr.py
name: taro
age: (in __getattr__ method) 28
address: 
Traceback (most recent call last):
  File "getattr.py", line 18, in <module>
    print 'address: ', x.address
  File "getattr.py", line 11, in __getattr__
    raise AttributeError, attrname
AttributeError: address
[kobakoba0723@fedora13-intel64 ~]$ 

__setattr__は、インスタンス.属性名 = 値 が実行された時に、インスタンス.__setattr__('属性名', '値')の形で呼び出される

[kobakoba0723@fedora13-intel64 ~]$ cat setattr.py
#!/usr/bin/env python

class C1(object):
  def __init__(self):
    pass
  def __setattr__(self, attrname, value):
    if attrname == 'name':
      self.__dict__['name'] = value
    elif attrname == 'age':
      self.__dict__['age'] = value
    else:
      raise AttributeError, attrname + ' is not allowed to define'

if __name__ == '__main__':
  x = C1()
  x.name = 'taro'
  x.age = '28'
  print 'name:', x.name
  print 'age:', x.age
  x.tel = '012-3456-7890'

[kobakoba0723@fedora13-intel64 ~]$ python setattr.py
name: taro
age: 28
Traceback (most recent call last):
  File "setattr.py", line 20, in <module>
    x.tel = '012-3456-7890'
  File "setattr.py", line 12, in __setattr__
    raise AttributeError, attrname + ' is not allowed to define'
AttributeError: tel is not allowed to define
[kobakoba0723@fedora13-intel64 ~]$ 

__setattr__を使うことで、クラスの外から値を代入できる変数を制限することが出来るようになる。
ただ、上みたいに1個1個if文で書いてると簡単に制限を緩くしたり出来ないので、そういう時は、in演算子を使ってやるとよい

[kobakoba0723@fedora13-intel64 ~]$ cat setattr_new.py
#!/usr/bin/env python

class C1(object):
  def __init__(self):
    pass
  def __setattr__(self, attrname, value):
    if attrname in self.private_attr:
      self.__dict__[attrname] = value
    else:
      raise AttributeError, attrname + ' is not allowed to define'

class C2(C1):
  private_attr = ['name', 'age']

if __name__ == '__main__':
  x = C2()
  x.name = 'taro'
  x.age = '28'
  print 'name:', x.name
  print 'age:', x.age
  x.tel = '012-3456-7890'

[kobakoba0723@fedora13-intel64 ~]$ python setattr_new.py
name: taro
age: 28
Traceback (most recent call last):
  File "setattr_new.py", line 21, in <module>
    x.tel = '012-3456-7890'
  File "setattr_new.py", line 10, in __setattr__
    raise AttributeError, attrname + ' is not allowed to define'
AttributeError: tel is not allowed to define
[kobakoba0723@fedora13-intel64 ~]$ 

今回は、許可するもののリストを作って、そこに対してin演算子でチェックしたが、許可しないもののリストで作るのもいいかも。

あと、__getattr__の場合、何かを制限したりするんじゃないし、どういう用途で使うんだろうか。

__repr__ と __str__

文字列の表現処理に関するフックメソッド

  • __repr__ は repr関数や対話型コマンドラインの自動出力機能に対応。また、__str__が無かった時にも対応する
  • __str__ は str関数やprintステートメントに対応(人にとって判りやすい形で出力)

reprだけ定義しておけば最低限対応は可能だけど、両方定義すれば呼び出し方に応じて出力内容を変えられる

[kobakoba0723@fedora13-intel64 ~]$ cat output_str.py
#!/usr/bin/env python

class C1(object):
  def __init__(self, value):
    self.data = value
  def __repr__(self):
    return 'C1(%s)' % self.data
  def __str__(self):
    return 'data: %s' % self.data

if __name__ == '__main__':
  x = C1(10)
  print '(repr(x), str(x)) = (%s, %s)' % (repr(x), str(x))
  print x

[kobakoba0723@fedora13-intel64 ~]$ python output_str.py
(repr(x), str(x)) = (C1(10), data: 10)
data: 10
[kobakoba0723@fedora13-intel64 ~]$ 

__call__

インスタンスが呼び出された時のフックメソッド
以下のようなコードを書いた時に、関数だけでなくインスタンスを渡せるようになる

[kobakoba0723@fedora13-intel64 ~]$ cat call.py
#!/usr/bin/env python

def call_func(funcname, value):
  funcname(value)

def func1(value):
  print value

class C1(object):
  def __init__(self, value):
    self.data = value
  def __call__(self, value):
    print self.data * value

if __name__ == '__main__':
  x = C1(10)
  
  call_func(func1, 10)
  call_func(x, 10)

[kobakoba0723@fedora13-intel64 ~]$ python call.py
10
100
[kobakoba0723@fedora13-intel64 ~]$ 

__del__

デストラクタ的なことをしたいときのフックメソッド*3

[kobakoba0723@fedora13-intel64 ~]$ cat del.py 
#!/usr/bin/env python

class C1(object):
  def __init__(self, name='unknown'):
    print 'Hello', name
    self.name = name
  def __del__(self):
    print 'Goodbye', self.name

if __name__ == '__main__':
  x = C1('taro')

[kobakoba0723@fedora13-intel64 ~]$ python del.py 
Hello taro
Goodbye taro
その他

オーバーロード可能なメソッドの種類は以下のようにいっぱいある
The Python Language Reference 3.4 Specail method names

*1:ループ処理の中でインデクシングが行われるから反復処理が実現できる

*2:スタックオーバーフローして例外が出されて強制終了. 2.6.4 だと "RuntimeError: maximum recursion depth exceeded in cmp" が出た

*3:呼び出されるタイミングはガーベージコレクションのタイミング