Python – OOP Part 4. クラスメソッドとスタティックメソッド (Class Method and Static Method)

読了 7分

今回の講座ではクラスメソッドとスタティックメソッドについて学んでいきます。クラスメソッドとスタティックメソッドはクラスの中で定義されるメソッドで、クラスメソッドはクラス変数を生成、変更または参照するためのメソッドであり、スタティックメソッドはクラスと関連性のある関数をクラスの中に定義し、クラスやインスタンスを通じて呼び出してより便利に使えるユーティリティメソッドです。

前回の講座で学んだインスタンスメソッドが self であるインスタンスを引数として受け取り、インスタンス変数のように一つのインスタンスにのみ限定されたデータを生成、変更、参照するのに対し、クラスメソッドは cls であるクラスを引数として受け取り、すべてのインスタンスが共有するクラス変数のようなデータを生成、変更または参照するためのメソッドだと考えてください。例題を見ながらクラスメソッドの使用方法について説明します。

oop_4.py
class Employee:
raise_amount = 1.1  # 1 年俸昇給率のクラス変数

    def __init__(self, first, last, pay):
        self.first = first
        self.last = last
        self.pay = pay
        self.email = first.lower() + '.' + last.lower() + '@schoolofweb.net'

    def apply_raise(self):
        self.pay = int(self.pay * self.raise_amount)

    def full_name(self):
        return '{} {}'.format(self.first, self.last)

    def get_pay(self):
        return '現在 "{}" の年俸は "{}" です。'.format(self.full_name(), self.pay)


emp_1 = Employee('Sanghee', 'Lee', 50000)
emp_2 = Employee('Minjung', 'Kim', 60000)

# 年俸引き上げ前
print(emp_1.get_pay())
print(emp_2.get_pay())

# 年俸引き上げ
emp_1.apply_raise()
emp_2.apply_raise()

# 年俸引き上げ後
print(emp_1.get_pay())
print(emp_2.get_pay())
実行結果
現在 "Sanghee Lee" の年俸は "50000" です。
現在 "Minjung Kim" の年俸は "60000" です。
現在 "Sanghee Lee" の年俸は "55000" です。
現在 "Minjung Kim" の年俸は "66000" です。

上のコードを見ると、クラス変数 raise_amount を定義し、apply_raise インスタンスメソッドを実行して各従業員の年俸を引き上げる際、すべての従業員のインスタンスに同じ昇給率が適用されるようにしました。もし翌年に昇給率を変更しなければならない場合、クラスメソッドを使って変更するのが良いでしょう。もちろん Employee.raise_amount = 1.2 のように直接クラス変数を変更する方法もありますが、データチェックや他の付加機能などの追加が必要なときには、クラスメソッドを使うととても便利です。下のコードをご覧ください。

oop_4.py
class Employee:
raise_amount = 1.1  # 年俸昇給率のクラス変数

    def __init__(self, first, last, pay):
        self.first = first
        self.last = last
        self.pay = pay

    def apply_raise(self):
        self.pay = int(self.pay * self.raise_amount)

    def full_name(self):
        return '{} {}'.format(self.first, self.last)

    def get_pay(self):
        return '現在 "{}" の年俸は "{}" です。'.format(self.full_name(), self.pay)

    # 1 クラスメソッドデコレータを使ってクラスメソッドを定義
    @classmethod
    def change_raise_amount(cls, amount):
        # 2 昇給率が "1" より小さければ再入力を要求
        while amount < 1:
            print('[警告] 昇給率は "1" より小さくすることはできません。')
            amount = input('[入力] 昇給率を再入力してください。\n=> ')
            amount = float(amount)
        cls.raise_amount = amount
        print('昇給率 "{}" が適用されました。'.format(amount))


emp_1 = Employee('Sanghee', 'Lee', 50000)
emp_2 = Employee('Minjung', 'Kim', 60000)

# 年俸引き上げ前
print(emp_1.get_pay())
print(emp_2.get_pay())

# 年俸昇給率の変更
Employee.change_raise_amount(0.9)

# 年俸引き上げ
emp_1.apply_raise()
emp_2.apply_raise()

# 年俸引き上げ後
print(emp_1.get_pay())
print(emp_2.get_pay())
実行結果
現在 "Sanghee Lee" の年俸は "50000" です。
現在 "Minjung Kim" の年俸は "60000" です。
[警告] 昇給率は "1" より小さくすることはできません。
[入力] 昇給率を再入力してください。
=> 1.2
昇給率 "1.2" が適用されました。
現在 "Sanghee Lee" の年俸は "60000" です。
現在 "Minjung Kim" の年俸は "72000" です。

#1 でクラスメソッドデコレータを使ってクラスメソッドを定義しました。#2 ではデータの整合性チェックを実施した後、データが1より大きい場合にだけクラス変数を変更しました。クラスメソッドはインスタンスのコンストラクタ(constructor)と同じ用途で使う場合もあります。例題を見てみましょう。

oop_4.py
class Person:
def __init__(self, year, month, day, sex):
self.year = year
self.month = month
self.day = day
self.sex = sex

    def __str__(self):
        return '{}{}{}日生まれの{}です。'.format(self.year, self.month, self.day, self.sex)


person_1 = Person(1990, 8, 29, '男性')
print(person_1)
実行結果
1990年 8月 29日生まれの男性です。

生年月日と性別データを持つクラスを使ってみました。しかし、常に「年、月、日、性別」を引数として受け取るのではなく、場合によっては住民登録番号を引数として受け取ってインスタンスを生成しなければならない場合があると考えてみましょう。その場合、次のようなコードを作ることができます。

oop_4.py
class Person:
def __init__(self, year, month, day, sex):
self.year = year
self.month = month
self.day = day
self.sex = sex

    def __str__(self):
        return '{}{}{}日生まれの{}です。'.format(self.year, self.month, self.day, self.sex)


ssn_1 = '900829-1034356'
ssn_2 = '051224-4061569'


def ssn_parser(ssn):
front, back = ssn.split('-')
sex = back[0]

    if sex == '1' or sex == '2':
        year = '19' + front[:2]
    else:
        year = '20' + front[:2]

    if (int(sex) % 2) == 0:
        sex = '女性'
    else:
        sex = '男性'

    month = front[2:4]
    day = front[4:6]

    return year, month, day, sex


person_1 = Person(*ssn_parser(ssn_1))
print(person_1)

person_2 = Person(*ssn_parser(ssn_2))
print(person_2)
実行結果
1990年 08月 29日生まれの男性です。
2005年 12月 24日生まれの女性です。

上のような方法で ssn_parser という関数を使って住民登録番号を解析した後、タプルをクラスのコンストラクタの引数として渡してもよいですが、これよりもよい方法は、クラスメソッドで代替コンストラクタを作ることです。例題をご覧ください。

oop_4.py
class Person:
def __init__(self, year, month, day, sex):
self.year = year
self.month = month
self.day = day
self.sex = sex

    def __str__(self):
        return '{}{}{}日生まれの{}です。'.format(self.year, self.month, self.day, self.sex)

    @classmethod
    def ssn_constructor(cls, ssn):
        front, back = ssn.split('-')
        sex = back[0]

        if sex == '1' or sex == '2':
            year = '19' + front[:2]
        else:
            year = '20' + front[:2]

        if (int(sex) % 2) == 0:
            sex = '女性'
        else:
            sex = '男性'

        month = front[2:4]
        day = front[4:6]

        return cls(year, month, day, sex)


ssn_1 = '900829-1034356'
ssn_2 = '051224-4061569'

person_1 = Person.ssn_constructor(ssn_1)
print(person_1)

person_2 = Person.ssn_constructor(ssn_2)
print(person_2)
実行結果
1990年 08月 29日生まれの男性です。
2005年 12月 24日生まれの女性です。

クラスの外で関数を使ったときと結果は同じですが、関数がクラスの中にメソッドとして定義され、もう少し洗練されたコードになったことが分かります。

今度はスタティックメソッドの使用方法について確認していきます。多くの人々がクラスメソッドとスタティックメソッドを混同していますが、この講座でインスタンスメソッド、クラスメソッド、スタティックメソッドのこの3つのメソッドの概念をしっかりと押さえていきましょう。

これら3つのメソッドは、すべてクラスの中で定義されます。インスタンスメソッドはインスタンスを通じて呼び出され、最初の引数としてインスタンス自身が自動的に渡されます。慣習的にこの引数を self と呼びます。クラスメソッドはクラスを通じて呼び出され、@classmethod というデコレータで定義します。最初の引数にはクラス自身が自動的に渡され、この引数を慣習的に ‘cls’ と呼びます。スタティックメソッドは、先に説明した2つのメソッドとは違って、インスタンスやクラスを最初の引数として受け取りません。スタティックメソッドはクラスの中で定義されてクラスの名前空間の中にあるだけで、一般の関数と全く違いはありません。しかし、クラスと関連性のある関数をクラスの中に定義してクラスやインスタンスを通じて呼び出すことで、もう少し便利に使えるのです。下の例をご覧ください。

oop_4.py
import datetime


class Person:
my_class_var = 'sanghee'

    def __init__(self, year, month, day, sex):
        self.year = year
        self.month = month
        self.day = day
        self.sex = sex

    def __str__(self):
        return '{}{}{}日生まれの{}です。'.format(self.year, self.month, self.day, self.sex)

    @classmethod
    def ssn_constructor(cls, ssn):
        front, back = ssn.split('-')
        sex = back[0]

        if sex == '1' or sex == '2':
            year = '19' + front[:2]
        else:
            year = '20' + front[:2]

        if (int(sex) % 2) == 0:
            sex = '女性'
        else:
            sex = '男性'

        month = front[2:4]
        day = front[4:6]

        return cls(year, month, day, sex)

    @staticmethod
    def is_work_day(day):
        # weekday() 関数の戻り値は
        # 月: 0, 火: 1, 水: 2, 木: 3, 金: 4, 土: 5, 日: 6
        if day.weekday() == 5 or day.weekday() == 6:
            return False
        return True


ssn_1 = '900829-1034356'
ssn_2 = '051224-4061569'

person_1 = Person.ssn_constructor(ssn_1)
print(person_1)

person_2 = Person.ssn_constructor(ssn_2)
print(person_2)

# 日曜日の日付オブジェクト生成
my_date = datetime.date(2016, 10, 9)

# クラスを通じてスタティックメソッドを呼び出し
print(Person.is_work_day(my_date))
# インスタンスを通じてスタティックメソッドを呼び出し
print(person_1.is_work_day(my_date))
実行結果
1990年 08月 29日生まれの男性です。
2005年 12月 24日生まれの女性です。
False
False

@staticmethod デコレータを使って is_work_day という名前で、ある日付における勤務の有無を返す機能を持つスタティックメソッドを作ってみました。

次回はクラスの継承とサブクラスを取り上げます。

X