[Python]クラスってなんだろう

最近よく耳にするオブジェクト指向プログラミング

オブジェクトって何?それメリットあんの?

そう思う方も多いことでしょう。

Python3を利用する場合、どんな使い方ができるのかを例にあげながら解説します。

クラス(class)

オブジェクト指向プログラミングでは多くの言語でクラス(class)と言うものが利用されます。
Pythonでもオブジェクト指向の書き方をする際はclassというキーワードが利用されます。

オブジェクト指向プログラミング

プログラムの中に登場する部品を現実世界における(オブジェクト)のように扱うプログラムの書き方のことです。

ここでは銀行を例にとって紹介しようと思います。

クラスの定義

オブジェクト指向プログラミングにおけるクラスとはいわば設計図のようなものです。

以下に、銀行の設計図classを書いてみました。

class Bank:
    pass

class Bankって書いただけで中身は空っぽです。

passは何もしないという意味です。

これがPythonにおける最小のクラス定義の仕方です。

インスタンス

インスタンスというのもオブジェクト指向プログラミングではよく聞く単語です。
先ほどの、クラスが設計図というならばインスタンスとはその実体、先ほどの例で言うならば銀行の支店と言ったところでしょうか。

世の中に銀行っていっぱいありますよね。
それと同じように、先ほど定義したBankもいっぱいあった方が便利。

なら、さっきの設計図を利用して銀行を量産しちゃえばいいじゃん!っていうのがインスタンス化です。

で、実際どうやってインスタンス化するの?

class Bank:
    pass

bank1 = Bank()

先ほど定義したクラスBankの後ろに()をつけました。

これだけ。

関数呼び出しとそっくりですね。
これで、銀行はめでたくインスタンス化(建設)されました。

クラスのインスタンスは一つだけでなく複数作ることができます。

bank2 = Bank()
bank3 = Bank()
bank4 = Bank()

理屈の上では、いくらでも作れます。

やったぁこれでお金預け放題だぁ〜(預ける金がねぇ!)

インスタンス変数

お金がないのは置いておいて、自分が銀行を利用するならそれぞれに支店ごとに口座情報がないとダメですよね。

そこで登場するのがインスタンス変数

先頭にインスタンスなんてついていますが、いわゆる変数と同じです。

普通の変数と少し異なるのは、その変数はインスタンス毎に独立という点です。

まずは、銀行に講座情報が保存できるようにバンククラスの定義を変更しましょう。

class Bank:

    def __init__(self, amounts):
        self._amounts = amounts.copy()

はい、変更しました。

何やら見慣れないものがいくつか混じっていますね。
それぞれ解説します。

__init__これは、クラスをインスタンス化する際に自動的に呼ばれる関数です。
俗に、コンストラクタなんて呼ばれ方をします。

先ほどクラスをインスタンス化する際にクラス名の後ろに()をつけましたよね?
あのタイミングで呼ばれるのが、__init__関数です。

bank1 = Bank() # <- ここで__init__関数が呼ばれる

さっき__init__定義しなかったじゃん…
__init__を自分で定義しなかった場合は、自動的に

class Bank:

    def __init__(self):
        pass

になると思ってください。

__init__を定義する際は、第一引数に必ずselfとかく必要があります。
これは決まりごとなので、そういうものだと思ってください。

第二引数以降は定義してもしなくても構いません。

初期状態として、値を渡したい場合に引数をとるようにします。

今回は、銀行の口座情報を辞書で表現したいと思いますのでamountsという引数をとるようにしました。

辞書のキーがユーザー名でバリューが残高です。

続いて__init__の中身についてです。

class Bank:

    def __init__(self, amounts):
        self._amounts = amounts.copy()

amounts.copy()でわざわざcopyを実行している理由は、pythonの辞書はただ代入するだけではコピー代入にならないからです。
詳しい話は、shallow copyとかdeep copyでググると出てきます。

第二引数で受け取ったamountsself._amountsに代入しています。

amountsは、わかるけどself._amountsってなんじゃい!

_amountsこそがインスタンス変数です。

selfというのが、そのクラスがインスタンス化(建設)された時のインスタンス(銀行)自身を指します。

なのでself._amountsというのは、インスタンス化されたBankクラスの口座情報ということになります。

そこにamountsを代入しているので、インスタンス化した銀行に口座情報を持たせることができるという流れです。

聞いてもわからんの人も多いと思うので実際に動かしてみましょう。

class Bank:

    def __init__(self, amounts):
        self._amounts = amounts.copy()

bank1 = Bank({"太郎": 1000, "二郎": 2000})

おいどうした、さっきインスタンス化するときに__init__関数が呼ばれるとか言ってたけど引数一個しか取ってねぇじゃねえか!!

はいそうです、ごめんなさい。
説明が遅れました。

__init__は定義するときは、第一引数にselfを書く必要があるのですが、呼び出すときは必要ありません
なんでかって、インスタンス化する前だと、どうやって第一引数にインスタンス化された自分自身を入れればいいの?っていう話になりますし、毎回書くのめんどくさいですから。

なので、第一引数は省略して、第二引数から始めています。

そんなわけで銀行に"太郎"さんと"二郎"さんの口座情報を持たせてインスタンス化してみました。

インスタンス化した銀行クラスは、変数bank1に入ってもらったので本当に口座情報が正しいか確認してみます。

インスタンス.クラス変数名インスタンスの後ろに.ドットをつけてインスタンス変数名を書くことで、インスタンス変数にアクセスできます。

bank1 = Bank({"太郎": 1000, "二郎": 2000}) # Bankクラスのインスタンスを作成して変数`bank1`に代入  
print(bank1._amounts)  # `変数.クラス変数名`で中身を確認  
# {'太郎': 1000, '二郎': 2000}

ちゃんと入ってました。

いろんな銀行作っちゃお

bank2 = Bank({"三郎": 100})
print(bank2._amounts)
# {'三郎':100}
bank3 = Bank({"四郎": 1400, "五郎": 198})
print(bank3._amounts)
# {'四郎':1400, '五郎':198}

さて、お気付きの方も多いとは思いますが、先ほどインスタンス変数はインスタンス毎に独立、と申し上げた通りそれぞれの銀行のインスタンスに同じ._amountsで名前にアクセスしていますが、全て違う口座情報が入っています。

これは_amountsという変数がそれぞれのインスタンス毎に別物であることを示しています。

bank2 = Bank({"三郎": 100})
print(bank2._amounts)
# {'三郎':100}
bank3 = Bank({"四郎": 1400, "五郎": 198})
print(bank3._amounts)
# {'四郎':1400, '五郎':198}

bank2._amounts = {"三郎": 500}
print(bank2._amounts)
# {'三郎':500}
print(bank3._amounts)
# {'四郎':1400, '五郎':198}

ちなみに、こんな感じでインスタンス化した後から口座情報を変更することもできます。

この際に、他の銀行の口座情報が変わっていないことからもインスタンス変数_amountsは、インスタンス毎に独立していることがわかります。

インスタンスメソッド

さっきのインスタンス変数がインスタンスに紐づいた変数だとすれば、インスタンスメソッドは、インスタンスに紐づいた関数です。

早速、インスタンスメソッドを作ってみましょう。

class Bank:

    def __init__(self, amounts):
        self._amounts = amounts.copy()
    
    def transfar(self, sender, receiver, yen):
        self._amounts[sender] -= yen
        self._amounts[reciver] += yen

新しくtransfarという関数(インスタンスメソッド)を定義しました。
同一支店内で口座間のお金を移動する動作を定義しています。

__init__関数 同様、第一引数には必ずselfをつける必要があります。

勘のいいみなさんはもうお気付きですね、transfarの中でself._amountsにアクセスしています。
__init__関数の説明で述べたように、selfはインスタンス化されたクラスを示すので、self._amountsのなかに入っているのは、インスタンス化された銀行の口座情報です。

実際に実行してみると以下のようになります。

bank = Bank({"太郎": 1000, "二郎": 200)
bank.transfar("太郎", "二郎", 100)
print(bank._amounts)
# {'太郎':900, '二郎':300}

このことからわかるように、インスタンスメソッドは、そのインスタンスのインスタンス変数にアクセスできるという特性があります。
インスタンスメソッドの中で、インスタンス変数の書き換えや、インスタンス変数を増やすこともできます。

__init__関数は呼び出されるタイミングが特殊なインスタンスメソッドである、と捉えることもできます。

インスタンスメソッドの定義の際、気をつけて欲しいのは、インデントを一つ下げて関数定義を行う、ということです。
これは先ほどの__init__関数に対しても言えることなのですが、その関数がクラスに紐づいていることを示すためにインデントをひとつ下げる必要があります。

クラス変数

インスタンス変数がインスタンス毎に独立した変数であったようにクラスそのものにも変数を持たせることができます。
それがクラス変数です。

class Bank:
    number = 0

    def __init__(self, amounts):
        self._amounts = amounts.copy()
        Bunk.number += 1
    
    def transfar(self, sender, receiver, yen):
        self._amounts[sender] -= yen
        self._amounts[reciver] += yen

インスタンスメソッドと同じインデントに変数number(支店数)を置きました。
インスタンスメソッドと同じインデント置いた変数はクラス変数と呼ばれます。

クラス変数は、全てのインスタンスで同じ変数を共有する特性があります。

bank1 = Bank({"太郎": 1000, "二郎": 2000})
print(bank1.number)
# 0
bank2 = Bank({"三郎": 100})
print(bank2.number)
# 1
bank3 = Bank({"四郎": 1400, "五郎": 198})
print(bank3.number)
# 2

__init__関数が実行されるたびに、つまりBankクラスのインスタンスが作られるたびに、Bankクラスのクラス変数numberが増えるように変更しました。

def __init__(self, amounts):
    self._amounts = amounts.copy()
    Bunk.number += 1 # <- この部分

つまり、numberは銀行の支店数と等しくなるわけです。

全てのBankクラスのインスタンスが指すnumberBankクラスのクラス変数numberです。

bank1 = Bank({"太郎": 1000, "二郎": 2000})
bank2 = Bank({"三郎": 100})
bank3 = Bank({"四郎": 1400, "五郎": 198})

Bank.number = 100 # ここでBankのnumber内容を書き換える

print(bank1.number) # すると他のインスタンスでも値が変わる
# 100
print(bank2.number) # なぜなら`number`はクラス変数で
# 100
print(bank3.number) # 全てのBankクラスで共有する変数だから
# 100

上記の例のようにクラス変数は、全てのインスタンスで共通のものなので、クラス変数がどこかで書き換わると他の全てのインスタンスでも値が変わってしまいます。

ちなみにクラス変数は、クラスをインスタンス化しなくとも参照することができます。
インスタンス変数はそういう訳にはいかない。

print(Bank.number)
# 0
print(Bank._amounts)
# AttributeError: type object 'Bank' has no attribute '_amounts'
# _amountsなんてものはないと怒られる

クラスメソッド

クラス変数が、クラスに紐づく変数であったり、インスタンスメソッドがインスタンスに紐づく関数であったのと同様、クラスメソッドはクラスに紐づく関数です。

class Bank:
    number = 0

    def __init__(self, amounts):
        self._amounts = amounts.copy()
        Bunk.number += 1
    
    def transfar(self, sender, receiver, yen):
        self._amounts[sender] -= yen
        self._amounts[reciver] += yen

    @classmethod
    def reset_number(cls):
        cls.number = 0

これだけは少し特殊で、定義の際に@classmethodと言うおまじないを書く必要があります。

第一引数に渡されるclsは、インスタンスではなくクラスになります。
今回の場合は、clsBankのことを指します。

Bank.reset_number()
print(Bank.number)
# 0 

クラスメソッドは、インスタンス化しなくても利用することができます。

もちろんインスタンスからも呼び出すことができます。

bank = Bank()
bank.reset_number()
print(bank.number)
# 0

クラスメソッドは、クラス変数へのアクセスはできますが、インスタンス変数へのアクセスはできないので注意してください。

class Bank:
    number = 0

    def __init__(self, amounts):
        self._amounts = amounts.copy()
        Bunk.number += 1
    
    def transfar(self, sender, receiver, yen):
        self._amounts[sender] -= yen
        self._amounts[reciver] += yen

    @classmethod
    def reset_number(cls):
        cls.number = 0
        print(cls._amounts) #  _amountsはインスタンス変数なのでクラスメソッドからはアクセスできない

まとめ

  • インスタンス変数はインスタンス毎に独立している
  • インスタンスメソッドはインスタンスに紐づいた関数インスタンス変数にアクセス出来る
  • クラス変数クラスの中で共通の変数
  • クラスメソッドクラス変数にアクセスが出来て、クラスをインスタンス化しなくとも利用できる

最後に

なんか久々に長々と書きましたが、オブジェクト指向プログラミングは奥が深いので、より複雑な話はまた機会があれば書こうと思います。
この記事が皆様の理解の助けになれば幸いです。