Pythonで、呼び出し方によってメソッドの振る舞いを変える

通常の定義と呼び出しの結果

Pythonのクラスのメソッドは3種類ある。

  • 通常のメソッド(インスタンスメソッド)
    • 第1引数は必須で、慣例としてselfにする。
    • インスタンス経由で呼び出すと、呼び出したインスタンスが第1引数に入る。
    • クラス経由で呼び出すと、呼び出したときの引数がそのまま渡される。
  • クラスメソッド
    • @classmethodを付けて定義する。第1引数は必須で、慣例としてclsにする。
    • インスタンス経由で呼び出すと、呼び出したインスタンスのクラスが第1引数に入る。
    • クラス経由で呼び出すと、そのクラスが第1引数に入る。
  • スタティックメソッド
    • @staticmethodを付けて定義する。引数は必須ではない。
    • 呼び出したときの引数がそのまま渡される。
class C:
  val = 20
  def __init__(self):
    self.val = 1
  def normal_method(self, v):
    return self.val + v + 2
  @classmethod
  def class_method(cls, v):
    return cls.val + v + 3
  @staticmethod
  def static_method(v):
    return C.val + v + 4

i = C()
i.normal_method(5)    # i.val + 5 + 2 = 1 + 5 + 2 = 8
i.class_method(6)     # C.val + 6 + 3 = 20 + 6 + 3 = 29
i.static_method(7)    # C.val + 7 + 4 = 20 + 7 + 4 = 31
C.normal_method(5)    # requires 2 args but 1: error
C.normal_method(i, 6) # i.val + 6 + 2 = 1 + 6 + 2 = 9
C.normal_method(C, 7) # C.val + 7 + 2 = 20 + 7 + 2 = 29
C.class_method(8)     # C.val + 8 + 3 = 20 + 8 + 3 = 31
C.static_method(9)    # C.val + 9 + 4 = 20 + 9 + 4 = 33

メソッドの呼び出し方で振る舞いを変える

通常のメソッドも関数であることに変わりはない。

  • 第1引数がselfというのは単なるお約束であって、selfの型については制約はない。
  • インスタンス経由で呼び出すと、処理系が勝手に第1引数にそのインスタンスを入れている。

これを逆手にとって、第1引数によって振る舞いを変えることができる。

class C:
  # 上記に追加
  def trick_method(arg, v):
    if isinstance(arg, C):
      return arg.val * 2 * v
    else:
      return C.val + arg * v

i.trick_method(4)    # i.val * 2 * 4 = 1 * 2 * 4 = 8
C.trick_method(5)    # requires 2 args but 1: error
C.trick_method(6, 7) # C.val + 6 * 7 = 20 + 6 * 7 = 62
C.trick_method(i, 8) # i.val * 2 * 8 = 1 * 2 * 8 = 16
C.trick_method(C, 9) # C.val + C * v: error

上記のtrick_method()は、インスタンス経由では通常のメソッドのように振る舞い、クラス経由ではスタティックメソッドのように振る舞っている。

本来であれば、振る舞いが違うのなら別メソッドにするべきである。

実例

こんな変な使い方は、インスタンスをグループで管理するクラスを製作中に思いついた。

class Student:
  rooms = {}
  def __init__(self, room):
    self.myroom = room
    if not room in Student.rooms:
      Student.rooms [room] = []
    Student.rooms [room].append(self)

  def classmates(self_or_room):
    if isinstance(self_or_room, Student):
      self = self_or_room
      for s in Student.rooms[self.myroom]:
        if s != self:
          yield s
    elif self_or_room in Student.rooms:
      room = self_or_room
      return iter(Student.rooms[room])

Ann = Student('3-A')
Bob = Student('3-B')
Cathy = Student('3-B')
Dan = Student('3-A')
Ellen = Student('3-A')
Fred = Student('3-B')

mates_of_ann = Ann.classmates() # Dan, Ellen
mates_of_3A = Student.classmates('3-A') # Ann, Dan, Ellen

mates_of_bob = Bob.classmates() # Cathy, Fred
mates_of_3B = Student.classmates('3-B') # Bob, Cathy, Fred

注意

デコレータ付き(@propertyとか@classmethodとか)のメソッドでは、この方法は使えない。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.