Pythonのインスタンス変数の定義場所と注意点

📕 新コースを公開しました。→クーポン掲載ページ

Pythonのクラスではインスタンス変数を定義できますが、定義する場所について解説してみます。

本記事の要約

インスタンス変数は

  • 基本的に初期化メソッドで定義するのが良い。
  • 通常のメソッド内で初めて定義する場合は実行順序に注意。

いただいた質問の内容

本記事は、Djangoのコースでいただいたご質問をもとに解説するものです。質問内容を要約すると次のような感じです。

get_querysetメソッドで定義したself.totalなどは、同じクラス内にあるget_context_dataメソッドなどでもselfの値は保持されるという解釈でいいでしょうか?

...
class CartListView(ListView):
    model = Item
    template_name = 'pages/cart.html'

    def get_queryset(self):
        cart = self.request.session.get('cart', None)
        if cart is None or len(cart) == 0:
            return redirect('/')
        self.queryset = []
        self.total = 0  # 🔴 ここでself.totalを定義
        for item_pk, quantity in cart['items'].items():
            obj = Item.objects.get(pk=item_pk)
            obj.quantity = quantity
            obj.subtotal = int(obj.price * quantity)
            self.queryset.append(obj)
            self.total += obj.subtotal
        self.tax_included_total = int(self.total * (settings.TAX_RATE + 1))
        cart['total'] = self.total
        cart['tax_included_total'] = self.tax_included_total
        self.request.session['cart'] = cart
        return super().get_queryset()

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        try:
            context["total"] = self.total  # 🔴 ここで使用
            context["tax_included_total"] = self.tax_included_total
        except Exception:
            pass
        return context

このコードをシンプルな形に直して解説してみます。

インスタンス変数は初期化メソッドで定義するのが一番いい

まず前提として、できるならインスタンス変数は初期化メソッド(__init__メソッド)で設定した方がいいと思います。

例のコードにおいてはそうではなく、任意のメソッドの中で新しく定義しています。
なぜかというと、

  • 継承している親クラスのメソッドが呼ばれる実行順序を知っていた

ので、わざわざ初期化メソッドを呼び出さなくてもいいかな、という判断でこのようにしました。

本記事はこのように初期化メソッドを使わずに、インスタンス変数を定義、使用する場合の挙動についての解説になります。

💡 継承していても初期化メソッドを呼び出してインスタンス変数を定義することはできます。初期化メソッドの中でまず親クラスの初期化メソッドを実行してあげれば大丈夫です。

簡単なコードで再現

では初期化メソッドを使わないでインスタンス変数を定義する場合の挙動を確認していきます。

まずは例のコードではなく簡単なコードに書き換えてみます。

class Dog():

    def born(self):
        print('bornメソッドが呼ばれた。')
        self.dog = 'ワンッワンッ'

    def bark(self):
        print('barkメソッドが呼ばれた。')
        print(self.dog)

self.dog はbornメソッドで定義され、

barkメソッド内で呼び出しています。

Dogクラスをインスタンス化して実行。下記のようにうまく「ワンッワンッ」と表示されています。

my_dog = Dog()
my_dog.born()
my_dog.bark()

# 実行結果 ---
# bornメソッドが呼ばれた。
# barkメソッドが呼ばれた。
# ワンッワンッ

うまくいかないケース

先ほどうまくいったのは実行する順番が正しかったからです。

次のように、my_dog.born() を実行する前に my_dog.bark() を実行すると、self.dog が定義されていないため、失敗することに。。。

my_dog = Dog()
# my_dog.born()
my_dog.bark()

# 実行結果 ---
# barkメソッドが呼ばれた。
# Traceback (most recent call last):
# ...
# AttributeError: 'Dog' object has no attribute 'dog'

このように実行順序が違えばうまくいかないことが起こり得るので、
基本的な考え方としては(自分でクラスを定義するときは特に)まず初期化メソッドでインスタンス変数を定義するのがいいと思います。

継承したクラスの場合でも、実行順序がよくわからないとかであれば、初期化メソッド自体をオーバーライドして定義しておくのが無難です。

以上、なぜインスタンス変数をインスタンスメソッドの中で新しく定義したのかについての解説でした。

🎓✍️コース一覧

プログラミング関係のビデオコースを提供しています。クーポンも発行していますので、ぜひ一度チェックしてみてください。

Twitter @takumafujimoto

記事を読んでいただきありがとうございます。ツイッターではプログラミング以外についてや、たまにクーポン情報もツイートしたり。。。ツイッターでもお待ちしてます。