Python classmethodとstaticmethodを使う意味を考える

Web開発

Pythonのclassについて学んでいくと、インスタンスメソッドだけでなくクラスメソッドとスタティックメソッドなるものの存在に出会います。

これらの実装方法についてはいろんな記事で紹介されていますし、実装自体はデコレータを一つ書くだけで簡単なので、使うだけならすぐに使えます。

ただ、なぜわざわざこれらを使い分けるのか?についての記事には出会ったことがありませんでした。

ずっと疑問だったclassmethodとstaticmethodの使い分けについて、そもそもなぜ使い分ける必要があるのか?なんのメリットがあるのか?について、ようやく自分の中で腑に落ちてきたので気持ちをメモしておきたいと思います。

classmethodとstaticmethodとは

軽くおさらいしておきます。

classmethod

クラスメソッドです。以下の様に@classmethodをクラス関数につけるだけです。慣習で第一引数を普段使うselfではなく、clsと書きます。

class MyClass:
    CLASS_PARAM = 100

    def __init__(self, instance_param):
        self.instance_param = instance_param

    @classmethod
    def method(cls):
        print(cls.CLASS_PARAM)
        # print(cls.instance_param) これはできない

関数内ではインスタンス変数にはアクセスできませんが、クラス変数にはアクセスができます。また、他のクラスメソッドやスタティックメソッドにもアクセス可能です。

要するにインスタンス変数に用事がない場合に使用できます。

staticmethod

スタティックメソッドです。インスタンスメソッドやクラスメソッドと異なり、指定される第一引数はなく、必要な引数を1番目から書きます。

class MyClass:
    CLASS_PARAM = 100

    def __init__(self, instance_param):
        self.instance_param = instance_param

    @classmethod
    def method(cls):
        print(cls.CLASS_PARAM)
        # print(cls.instance_param) これはできない

    @staticmethod
    def method_2(param):
        print("Static!!", param)
        # cls.method() できない
        # print(cls.CLASS_PARAM) できない
        # print(self.instance_param) できない

関数内ではインスタンス変数やクラス変数、他の関数にもアクセスできません。クラスの他の実装に依存しない関数です。

要するに独立した関数として実装できる場合に使えます。

使い方は分かった。で??

使い方は冒頭記載した通り難しくありません。それぞれの制約も割とシンプルだと思います。

ただ、おそらくほとんどの人の感想は以下の様なものでしょう。

「なるほど、メソッドにも種類があるのね。で??全部インスタンスメソッドで書けばいいんじゃない?」

実際その通りで、上記の例においても以下の様に全部インスタンスメソッドとして実装できます。

class MyClass:
    CLASS_PARAM = 100

    def __init__(self, instance_param):
        self.instance_param = instance_param

    def method(self):
        print(self.CLASS_PARAM)

    def method_2(self, param):
        print("Static!!", param)

使い分けるメリット(個人見解)

使い分けるメリットとしては以下の5つかなと考えています。(2021/11に2個追加)

  • 可読性・保守性
  • ファクトリー関数
  • 名前空間の制御
  • インスタンス化の手間を省く
  • パフォーマンス

一個ずつ気持ちを書いていきます。

可読性・保守性

人によって答えの異なる分野だとは思いますが、個人的には各種メソッドを使い分けるもっとも大きなメリットだと考えています。

デコレータが出てくるし、selfだったり、clsだったり、第一引数なしだったりでややこしいから逆に可読性下がるんじゃないかといった意見もきっとあると思います。

ただ、他人のコードを読んだり、修正する機会が増えてきて感じたのは、「インスタンス変数が絡む関数は読むのが大変な傾向にある」ということです。

特に、別の関数で操作されたインスタンス変数がさらに別の関数で登場してくる様なケースは割としんどくて、一直線にコードが読めないのと、インスタンス変数への意識に脳のリソースを奪われる感じがあってとても疲れます。

個人的には読むのがしんどい = 修正もしんどいなので、保守の観点でもやはり辛いです。

やりたいことの都合上、インスタンス変数を操作せざるを得ないことはままあると思うのでそれ自体は頑張るだけなのですが、全部インスタンスメソッドとして実装されていると、この関数はどこかでインスタンス変数を操作しているのか??という割と大事な情報が、関数を全部読むまでわからないという事態になります。

関数の頭にデコレータで@staticmethodと書いてあれば、「この関数はインスタンス変数にはアクセスしないのか」とすぐにわかるので読むのも楽ですしそれはイコール修正するときも気が楽です。

長くなりましたが、つまるところ、インスタンス変数やクラス変数・関数への依存が一目見てわかるのは可読性・保守性の観点から大きなメリットではないか

というのが私の気持ちです。

ファクトリー関数

もう一つ大きな要素です。適切な名前がわからないので、仮にファクトリー関数と名付けます。

やりたいことはインスタンス作成用の関数を_init_とは別に用意することです。例えば以下のような。

class PreData:
    def __init__(self, a: int, b: int):
        self.a = a
        self.b = b


class Product:
    def __init__(self, d: int, e: int):
        self.d = d
        self.e = e

    @classmethod
    def from_pre_data(cls, pre_data: PreData):
        return cls(
            d=pre_data.a * 2,
            e=pre_data.b * 10
        )

    @staticmethod
    def from_pre_data_static(pre_data: PreData):
        return Product(
            d=pre_data.a * 2,
            e=pre_data.b * 10
        )


pre_data = PreData(5, 10)
product1 = Product.from_pre_data(pre_data)
product2 = Product.from_pre_data_static(pre_data)

上記のように、ある別のデータ構造から該当インスタンスを作成する関数を作りたくなるケースはままあるかと思います。(個人的にはよく使います)

こういった関数をinstancemethodで実装するのは無理(できなくはないが)なので、必然的に、classmethodかstaticmethodとして実装することになるでしょう。(PreDataにto_productのようなinstancemethodを作る方法もあると思いますが、それはまた別の論点だと思うので、一旦置いておきます。)

オブジェクト間の連携がわかりやすくなる(と思う)ので、実装の幅を広げる意味でも、classmethod, staticmethodを使うメリットになるのではないでしょうか。

なお、例に示したように、classmethodでもstaticmethodでもどちらでも実装可能なので、どちらで実装するべきなのかとても気になっています。

個人的には、自分自身のインスタンスを作っているのがわかりやすいので、classmethodの方がいいのかなという意見です。

名前空間の制御

こちらは可読性に関しての別の視点のお話です。

どこに何が書いてあるかをわかりやすくするために、適切にモジュールやクラスを切っていくと思うのですが、大切なのは上手に階層構造を作ることなのかなと考えています。

例えばあるファイル(モジュール)に4つのスタティックな関数があったとして、役割としては2つのグループに分かれているとします。

この際ファイル(モジュール)を分割しても良いですが、行数そんなにないし、ファイルが無駄にたくさん増えるのも気持ち悪いので、モジュール内でグルーピングしたくなることがあります。

スタティックな関数をモジュール内でグルーピングする場合、以下のようにstaticmethodを使うことになります。(後述するインスタンス化の手間を減らすため)

# グルーピングせずにフラットに4つ並べる
def func_1():
    pass


def func_2():
    pass


def func_3():
    pass


def func_4():
    pass


# 意味合いの近い関数をクラスでグループ化してまとめる
class GroupA:
    @staticmethod
    def func_1():
        pass

    @staticmethod
    def func_2():
        pass


class GroupB:
    @staticmethod
    def func_3():
        pass

    @staticmethod
    def func_4():
        pass

この例については、正直どちらがいいのかなんとも言えないところです。

classでグルーピングすることで、関数呼びだしの名前空間も制御できるので、やり方次第では有用かもしれないというイメージを持っています。

無駄にグルーピングすると逆にわかりにくくなるケースも出てくると思うので、乱用すべきではないと思いますが、選択肢として一つ持っておくのはありかなと考えています。

インスタンス化の手間を省く

残りはおまけ程度ですが、一応記載しておきます。

こちらは割とわかりやすいメリットです。

インスタンスメソッドを使うときは一度インスタンス化しないとダメですが、クラスメソッドやスタティックメソッドであればその必要がありません。

無駄な処理をするのってすごく気持ちが悪いですし、やはりスマートでかっこいい実装ができた方がテンションもあがるので、こういう細かいところもこだわっていきたいなという気持ちです。

# インスタンスメソッドとして実装されている場合
object = MyClass()
object.method()

# クラスメソッドとして実装されている場合
MyClass.method()

パフォーマンス

最後です。これはまだ実感としてもっているわけではないのですが理論的にはという話です。

毎回インスタンス化しなくていいということは、処理も減るし、無駄なインスタンスにメモリを使わなくて良いということです。

クラスの関数をいろんなところで使う場合に、インスタンスメソッドとして実装していると10個、20個とインスタンスを作るはめになりますが、それが全部不要になります。

塵も積もればなんとやらなので、規模の大きいシステムの場合はこういった細かいところの配慮も大事になってくるのかもしれないです。

まとめ

ということでようやく謎が解けた気分なので気持ちを書いてみました。(まだまだ謎も多いですが)

こういう考えであることを職場の方と相談したところ、共感していただけてこれまで全部インスタンスメソッドで実装していたんですが、これからはそれぞれ使い分けて行こうぜという話になりました!

正解はないのかもしれませんが、こういうところの思想についていろんな方とお話ししてみたいものです。(ご意見待ってます)

コメント

タイトルとURLをコピーしました