DjangoBrothers BLOG ✍️

2018/10/25

このエントリーをはてなブックマークに追加
Python

【Python】インスタンスメソッド、staticmethod、classmethodの違いと使い方

クラス内で関数を作るときに出てくるstaticmethodやclassmethodについて説明してみます。

クラスについて、より基本的なことは こちらの記事に書いています。

この記事では、3種のクラス内関数について説明します。

  1. 通常のメソッド(インスタンスメソッド)
  2. staticmethod(静的関数)
  3. classmethod

実装方法

実装方法は難しくありません。それぞれの関数の上に@staticmethodや、@classmethodと記述するだけで、それぞれの関数がstaticmethodとclassmethodとして認識されるようになります。@マークがついたものはデコレータと呼びますが、デコレータ自体の意味は今回は割愛して別の機会に解説します。

今回は、以下のように定義したPersonクラスを例に説明していきます。Personクラスは、初期化メソッド、そして関数(インスタンスメソッド、staticmethod)を持ちます。また、最後の2行ではPersonクラスをもとに2つのインスタンスを生成しています。

Personクラスの定義

class Person:
    def __init__(self, first, last):
        self.first = first
        self.last = last

    #インスタンスメソッド
    def self_introduction(self):
        print("私の名前は、" + self.last + self.first + "です。")

    @staticmethod
    def hello():
        print ("Hello")

# 2つのインスタンスを生成
person1 = Person("太郎", "山田")
person2 = Person("一人", "殿馬")

通常のクラスメソッド(インスタンスメソッド)

まずはデコレータがつかない、いわゆる「普通の」メソッドについてみてみましょう。このメソッドは、インスタンスに紐づき、「self」を第一引数にとります。この関数を呼び出した時は自動的に第一引数(self)にはインスタンスが渡されます。

インスタンスメソッドself_introductionの呼び出し

person1.self_introduction()
# 私の名前は、山田太郎です。
person2.self_introduction()
# 私の名前は、殿馬一人です。

person1.self_introduction()では、インスタンスのperson1がself_introduction(self)関数の第一引数として渡されます。つまりself=person1となります。

このようにインスタンスに紐づくため、各インスタンスごとに変わるプロパティを出力させたい場合は便利です。今回の例だと、self.lastやself.firstを出力しました。

またインスタンスメソッドは、以下のようにクラス名.メソッド(インスタンス)とすることでも呼び出すことができます。クラスから呼び出す場合はメソッドの引数にインスタンスを与える必要があります。

Personクラスからself_introductionを呼び出す

Person.self_introduction(person1)
# 私の名前は、山田太郎です。

staticmethod(静的関数)

インスタンスメソッドでは、インスタンスごとによって動的に変化するものを出力させましたが、staticmethodは毎回同じ結果を出力したいときに使います。

インスタンスに紐づく関数ではないため、第一引数を取る必要がありません。そのため、hello関数は引数をとっていませんし、関数の中でも「self」を使用していません。

staticmethod hello( )の呼び出し

class Person:
    #初期化メソッド
    def __init__(self, first, last):
        self.first = first
        self.last = last

    @staticmethod
    def hello():
        print ("Hello")

# 2つのインスタンスを生成
person1 = Person("太郎", "山田")
person2 = Person("一人", "殿馬")

person1.hello()
# Hello
person2.hello()
# Hello

person1から呼び出されようが、person2から呼び出されようが、出力結果は同じ"Hello"です。

インスタンスメソッドとの違い

仮に、hello関数から@staticmethodを取り払ったらどうなるでしょうか。つまり、hello関数はインスタンスメソッドとなり、第一引数に(self)をとらない状態としてみます。

すると、呼び出し時に以下のようなエラーとなります。

hello( )の呼び出し(第一引数を指定していないインスタンスメソッド)

# 省略

    def hello():
        print ("Hello")

person1 = Person("太郎", "山田")

person1.hello()
# エラー発生
TypeError: hello() takes 0 positional arguments but 1 was given

hello( )関数は引数を取らないはずなのに、1つの引数が与えられてしまっていますよというエラーメッセージですね。ここでいう与えられた1つの引数というのは、person1のことです。

インスタンスメソッドでは、自動的に第一引数にインスタンスが渡されるということは説明しましたよね。しかし、hello関数は何も引数を取らない設定にしているので、このようなエラーが起きてしまいます。

def hello(self):のようにhello関数に第一引数を指定すれば、普通のインスタンスメソッドとなるので、エラーは解消されます。

しかし、hello関数は単に文字列を出力するだけの関数でインスタンスを扱うことはないので、selfを引数にとること自体無意味です。そこで、@staticmethodをつけてあげることで、引数を何も取らなくてもよい静的関数にしてあげるのです。明示的に静的関数であることを示してあげることで、関数の役割も把握しやすくなります。

クラスメソッド

クラスメソッドは、インスタンスメソッドと似ています。

インスタンスメソッドが各インスタンスに結びつくのに対して、classmethodはクラスに結びつきます。

そして、メソッドを呼び出した時に第一引数には、クラスが自動的に代入されます。

クラス変数について

まずはクラス変数について説明します。クラス変数は、クラスで共通して持つ変数です。以下の例だと、person1もperson2も共通して「リーグ優勝」という目標と、countを持つ定義をしています。

また、初期化メソッドにおいて新しくインスタンスが生成されたときは、countを1足す設定を定義しています。これによりPersonが全体で何人いるかを取得することができるようになります。

クラス変数を定義

class Person:
    #クラス変数
    goal = "リーグ優勝"
    count = 0

    def __init__(self, first, last):
        self.first = first
        self.last = last
        Person.count += 1

person1 = Person("太郎", "山田")
person2 = Person("一人", "殿馬")

print(person1.goal) # リーグ優勝
print(person2.goal) # リーグ優勝
print(Person.count) # 2

# クラス変数の書き換え
Person.goal = "日本一"
print(person1.goal) # 日本一
print(person2.goal) # 日本一

classmethodを使う

それでは、classmethodを使ってみましょう。上の例で、Person.goalとしてクラス変数を書き換えてるところをclassmethodを使って実装してみたいと思います。

classmethodを使ってクラス変数を書き換え

class Person:
    #クラス変数
    goal = "リーグ優勝"
    count = 0

    def __init__(self, first, last):
        self.first = first
        self.last = last
        Person.count += 1

    @classmethod
    def change_goal(cls, new_goal):
        cls.goal = new_goal

person1 = Person("太郎", "山田")
person2 = Person("一人", "殿馬")

print(person1.goal) # リーグ優勝
print(person2.goal) # リーグ優勝

# change_goal( )を呼び出す
Person.change_goal("日本一")
print(person1.goal) # 日本一
print(person2.goal) # 日本一

change_goal( )を呼び出した時に、第一引数clsには自動的にPersonクラスが与えられ、第二引数new_goalには「日本一」を与えています。

この例だけでは、いまいちclassmethodにしたメリットがわからないので、今度はハイフンで区切られた文字列がユーザー情報として与えられると仮定して、その文字列を元に新しいインスタンスを作成する場合を考えます。

classmethodを使わない場合は、以下のようになります。

与えられた文字列情報からインスタンスを生成

class Person:
    def __init__(self, first, last):
        self.first = first
        self.last = last

str_1 = "太郎-山田"
str_2 = "一人-殿馬"

first, last = str_1.split('-')
person1 = Person(first, last)

first, last = str_2.split('-')
person2 = Person(first, last)

一度split関数を使って、文字列を切り分け、それぞれをfirstとlastに代入しています。この例では、インスタンス生成の度に毎回この処理が必要となり、これを大量に繰り返すのは非常に面倒です。なので、この処理を関数化してしまいましょう。

classmethodを使う例は以下のようになります。

classmethodで処理を加えてインスタンスを生成

class Person:
    def __init__(self, first, last):
        self.first = first
        self.last = last

    @classmethod
    def from_str(cls, str):
        first, last = str.split('-')
        return cls(first, last)

str_1 = "太郎-山田"
str_2 = "一人-殿馬"

person1 = Person.from_str(str_1)
person2 = Person.from_str(str_2)

こうすることで、from_str関数を呼び出せば、引数として与えられた文字列を自動で処理してその結果を元にインスタンスを生成してくれるようになりました。

from_str関数はclassmethodなので第一引数clsにはPersonクラスが渡ってきており、cls(first, last)は、Person(first, last)と同義になりインスタンスが生成されます。

Djangoでの使用例

Djangoでは、Viewの関数内で、モデル.objects.all( )のように、objectsメソッドを使うことでDBにアクセスすることがあるかと思います。これを、モデル内でclassmethodを使って以下のように実装することもできます。

objectsをviewで使う例と、モデル内で使う例は以下の通りです。

view関数からobjectsメソッドを使う例

views.py

from .models import Person

def index(request):
    latest_people = Person.objects.all()[:5]
    return render(request, 'index.html', {'latest_people': latest_people})

モデル内でobjectsメソッドを使う例

モデルクラス内で関数化しておくことで、各Viewから同じ処理を呼び出すことができますし、テストがしやすくなるなどのメリットがあります。

models.py

from django.db import models

class Person(models.Model):
    first = models.CharField(max_length=150)
    last = models.CharField(max_length=150)

    @classmethod
    def get_latest_people(cls):
        return cls.objects.all()[:5]

views.py

from .models import Person

def index(request):
    latest_people = Person.get_latest_people()
    return render(request, 'index.html', {'latest_people': latest_people})