[Python]クラス設計

Python

このページでは、自動販売機をクラスとして設計しながら、クラスの扱い方についてまとめました。

使用した環境
Windows10
WSL2 - Ubuntu 20.04 LTS
Python 3.10.0

 Androidアプリを作成しました。
 感情用のメモ帳です。

スポンサーリンク
スポンサーリンク

インスタンス作成

Pythonにはあらかじめ組み込まれている型(int, str, list等)がありますが、クラスはこの型を新しく定義する機能です。

文型は以下のようになります。

class クラス名(基底クラス):
    内容

()のなかには継承するクラスを記述します。
ない場合は省略可能です。その際はobjectクラス(すべてのクラスの基底となるもの)が継承されます。

まずは自動販売機クラスの名前だけ定義しましょう。

Python 3.10.0 (default, Oct 12 2021, 16:02:08) [GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> class VendingMachine:
...     pass
... 

クラス名の一文字めは、大文字のアルファベットとするのが一般的です。

単語をつなげる場合、つなげた単語の頭文字も大文字にします(キャメルケース)。

フタコブラクダのいらすと
ラクダ(キャメル)

クラス名を決めただけで中身は何もありませんが、これだけでもクラスからインスタンス(実体)を作成することができます。

インスタンスとは、クラスが実体のないもの――設計図だとすると、その設計図を元に作り出された具体的なものです。

自動販売機の図面や仕様などをまとめたのがクラス、街中に実際に置いてある自動販売機がインスタンスだと言えるでしょうか。

インスタンスを作成するには、クラス名に()を付けます。

>>> tokyo = VendingMachine()

変数「tokyo」にはVendingMachineクラスのインスタンスが入っています。
東京に自動販売機を1つ設置したイメージです。(中身のない筐体ですが)

インスタンス変数

実際には自動販売機には色々な飲みものが入っていますよね。しかも個体ごとに商品のラインナップは異なります。

独自の商品ラインナップを保持させるために、インスタンスに変数を持たせます。

やり方は、まず、インスタンス作成時に暗黙的に呼ばれる初期化メソッド「__init__」を定義します。

def __init__(self):

必ず第一引数を受け取る必要があり、その引数は慣例的に自分自身を表すselfが使われます。

そのメソッドの中で変数を定義します。

self.変数名


インスタンス自身に変数を取り込ませるイメージです。

初期化メソッドの中で、変数を定義すると、インスタンスごとに独自の値を持つようになります。

別のクラスを作って例示します。

>>> class Name:
...     def __init__(self, last, first):
...         self.last_name = last
...         self.first_name = first
... 
>>> sister = Name('フグ田', 'サザエ') 
>>> sister.last_name
'フグ田'
>>> sister.first_name
'サザエ'

上記Nameは、インスタンスが苗字と名前を保持するクラスです。

インスタンス変数の値の参照は、インスタンスにドットを付けます。

インスタンス名.変数名

sister.last_name

このようにドットをつけて参照、実行できるものを属性と言います。
「last_name」、「first_name」がそうです。

後に紹介するクラス内で定義した関数――メソッドも属性のひとつです。

インスタンス変数の値は、インスタンスごとに異なります(同じであっても構いません)。

>>> sister = Name('フグ田', 'サザエ') 
>>> sister.last_name
'フグ田'
>>> sister.first_name
'サザエ'
>>> me = Name('磯野', 'カツオ')
>>> me.last_name + me.first_name
'磯野カツオ'

では、VendingMachineクラスに戻り、初期化メソッドを定義しましょう。

自動販売機の商品と在庫を再現します。

>>> class VendingMachine:
...     def __init__(self, items: tuple):
...         self.stock = {items[i] : 10 for i in range(3)}
... 
>>> tokyo = VendingMachine(('Water', 'Coffee', 'Green-Tea'))
>>> osaka = VendingMachine(('Cola', 'Tea', 'Oshiruko'))
>>> tokyo.lineup
{''Water': 10, 'Coffee': 10, 'Green-Tea': 10}
>>> osaka.stock
{'Cola': 10, 'Tea': 10, 'Oshiruko': 10}

インスタンス変数「stock」は辞書で、タプルで渡された3つの飲み物を10個ずつ持てるようにしています。

東京に置いた自販機は、ミネラルウォーター・コーヒー・お茶がそれぞれ10個ずつ入っている、

大阪のは、コーラ・紅茶・おしるこ、が10個ずつ入っている、というイメージですね。

インスタンスメソッド

クラスのなかで定義した関数はメソッドと呼ばれ、インスタンスのみを介して呼び出せるものをインスタンスメソッドと言います。

自動販売機の一番の機能は飲み物を売ることです。

インスタンスメソッドとして定義してみましょう。

>>> class VendingMachine:
...     def __init__(self, items: tuple):
...         self.stock = {items[i] : 10 for i in range(3)}
...         self._count = 0
...     
...     def sell(self, name):
...         self.stock[name] -= 1
...         self._count += 1
...         print(f'ガコンッ\n/{name}/')
... 
>>> 
>>> tokyo = VendingMachine(('Water', 'Coffee', 'Green-Tea'))
>>> tokyo.sell('Coffee')
ガコンッ
/Coffee/
>>> tokyo.stock
{'Water': 10, 'Coffee': 9, 'Green-Tea': 10}

sellメソッドが実行されると、商品名の在庫が一つ減り、売り上げとして追加した変数_countに加算されます。
printは雰囲気を出したくて付けました。

_countのように、アンダースコアのついた変数は、外部からの利用を想定していないプライベート変数であることを表現します。

クラス変数

インスタンス変数はインスタンスごとに固有の値を持ちました。

インスタンスではなく、そのクラスに変数を持たせることもできます。

やり方は、クラス外での変数定義と同じです。

商品のラインナップ数を保持する変数を「lineup」、商品の値段を「price」、製造メーカーを「manufacturer」として定義します。

>>> class VendingMachine:
...     lineup = 3
...     price = 150
...     manufacturer = 'FU-JI-NO'
... 

クラス変数は、インスタンスがなくても参照できます。

>>> VendingMachine.lineup
3
>>> VendingMachine.manufacturer
'FU-JI-NO'
>>> VendingMachine.price
150

インスタンスを介しての参照もできます。

>>> tokyo = VendingMachine(('Water', 'Coffee', 'Green-Tea'))
>>> tokyo.lineup
3

クラス変数を利用した初期化メソッドに変更。

class VendingMachine:
    lineup = 3
    price = 150
    manufacturer = 'FU-JI-NO'
    def __init__(self, items: tuple):
        self.stock = {items[i] : 10 for i in range(self.lineup)}
        self._count = 0

クラスメソッド

クラスメソッドは、インスタンスがなくても実行できるメソッドです。

やり方は「@classmethod」を付けたあとでメソッドを定義します。

@classmethod
def メソッド名(cls):

この場合も第一引数を渡す必要があり、慣例的にclsが使われます。

>>> class VendingMachine:
...     lineup = 3
...     price = 150
...     manufacturer = 'FU-JI-NO'
...     @classmethod
...     def print_manufacture(cls):
...         print('--------------------')
...         print(f'|{cls.manufacturer.center(18)}|')
...         print('--------------------')
... 
>>> VendingMachine.print_manufacture()
--------------------
|     FU-JI-NO     |
--------------------

上記のクラスメソッドでは、製造メーカー名を表示させるようにしました。

スタティックメソッド

クラスやインスタンスを介して行うメソッドには、スタティックメソッドというものもあります。

自動販売機クラスでは使わないので、簡単にいきます。

やり方は、メソッド定義の前に「@staticmethod」をつけます。

>>> class Greeting:
...     @staticmethod
...     def hello(name):
...         print(f'Hello {name}.')
... 
>>> Greeting.hello('fuji')
Hello fuji.

@classmethod」や「@staticmethod」はデコレータというものの一種です。

メソッドごとの使い分け

これまで3つのメソッド――インスタンスクラススタティックメソッドと紹介してきました。

ちょっと違いがわかりにくかったかもしれません。

いったん自動販売機クラスから離れてまとめると、

  • そもそもメソッドに引数が必要ない場合、クラス外で関数を定義する時とまったく一緒。
>>> class Greeting:
...     def hello():
...         print('Hello')
... 
>>> Greeting.hello()
Hello
  • メソッドに引数は使うけど、クラス内部のものが必要ない場合は、スタティックメソッド。

@staticmethodをつける。

>>> class Greeting:
...     @staticmethod
...     def hello(name):
...         print(f'Hello {name}.')
... 
>>> Greeting.hello('fuji')
Hello fuji.

上記の二つのメソッドは、定義をクラス外に取り出してもそのまま機能します。

>>> def hello1():
...     print('Hello')
... 
>>> def hello2(name):
...     print(f'Hello {name.}')
... 
>>> hello1()
Hello
>>> hello2('fuji')
Hello fuji.

なのでクラス内で定義する必要はないかもしれません。

  • クラス変数を利用したものが、クラスメソッド。

引数にcls。

>>> class Greeting:
...     name = 'Human'
...     @classmethod
...     def hello(cls):
...         print(f'Hello {cls.name}.')
... 
>>> Greeting.hello()
Hello Human.
  • インスタンス変数を使う場合、インスタンスメソッド。

引数にself。

>>> class Greeting:
...     def __init__(self, name):
...         self.name = name
...     def hello(self):
...         print(f'{self.name} "Hello."')
...
>>> kei = Greeting('Kei')
>>> yui = Greeting('Yui')
>>> kei.hello()
Kei "Hello."
>>> yui.hello()
Yui "Hello."

プロパティ

メソッドに「@property」をつけるとそのメソッドはクラス変数・インスタンス変数と同じように使えるようになります。

売り上げを計算するsalesメソッドを@propertyをつけて定義します。

>>> class VendingMachine:
...     lineup = 3
...     price = 150
...     def __init__(self, items: tuple):
...         self.stock = {items[i] : 10 for i in range(self.lineup)}
...         self._count = 0
...     
...     def sell(self, name):
...         self.stock[name] -= 1
...         self._count += 1
...         print(f'ガコンッ\n/{name}/')
...     
...     @property
...     def sales(self):
...         return self._count * self.price
... 
>>> tokyo = VendingMachine(('Water', 'Coffee', 'Green-Tea'))
>>> tokyo.sell('Coffee')
ガコンッ
/Coffee/
>>> tokyo.sell('Green-Tea')
ガコンッ
/Green-Tea/
>>> tokyo.sales
300

salesを利用するときを見てほしいのですが、()は必要ありません。

tokyo.sales

インスタンス変数と同じように使っていますね。

ただ上記はクラス・インスタンス変数と違い、読み取り専用です。

>>> tokyo.price = 200
>>> tokyo.price
200
>>> tokyo.sales = 10000
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute 'sales'

自動販売機クラスの実現

せっかくなので、もう少し自動販売機クラスに手を加えて、終わりたいと思います。

  • 自動販売機を利用するユーザークラスの作成。
class User:
    def __init__(self, wallet):
        self.wallet = wallet

Userクラスはwalletという属性を持ちます。
財布ですね。

  • VendingMachineクラスのメソッドの変更と追加。
# 上部省略
    def show(self):
        self.print_manufacture()
        for item in self.stock:
            print(f'|{item.center(18)}|')
            print(f'|{str(self.price).center(18)}|')
            print('--------------------')
        print('|     |      |     |')
        print('--------------------')

    def sell(self, user: User, name):
        if user.wallet - self.price < 0:
            print('お金が足りません...')
        elif self.stock[name] == 0:
            print('売り切れ!')
        else:
            user.wallet -= self.price
            self.stock[name] -= 1
            self._count += 1
            print(f'ガコンッ\n/{name}/')
    
    def restock(self):
        for name in self.stock:
            self.stock[name] = 10

自動販売機の商品リストを表示させる、showメソッド。

Userのwallet、VendingMachineのstockを確認し、OKなら販売するsellメソッド。

在庫を補充するrestockメソッドを定義しました。

長くなったのでモジュールとしてまとめます。

class User:
    def __init__(self, wallet):
        self.wallet = wallet

class VendingMachine:
    lineup = 3
    price = 150
    manufacturer = 'FU-JI-NO'
    def __init__(self, items: tuple):
        self.stock = {items[i] : 10 for i in range(self.lineup)}
        self._count = 0
    
    @classmethod
    def print_manufacture(cls):
        print('--------------------')
        print(f'|{cls.manufacturer.center(18)}|')
        print('--------------------')

    def show(self):
        self.print_manufacture()
        for item in self.stock:
            print(f'|{item.center(18)}|')
            print(f'|{str(self.price).center(18)}|')
            print('--------------------')
        print('|     |      |     |')
        print('--------------------')

    def sell(self, user: User, name):
        if user.wallet - self.price < 0:
            print('お金が足りません...')
        elif self.stock[name] == 0:
            print('売り切れ!')
        else:
            user.wallet -= self.price
            self.stock[name] -= 1
            self._count += 1
            print(f'ガコンッ\n/{name}/')
    
    def restock(self):
        for name in self.stock:
            self.stock[name] = 10
    
    @property
    def sales(self):
        return self._count * self.price

インポートして使ってみます。

$ python
Python 3.10.0 (default, Oct 12 2021, 16:02:08) [GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from vmachine import User, VendingMachine
>>> tokyo = VendingMachine(('Cola', 'Milk', 'Tea'))
>>> kids = User(100)
>>> user1 = User(1000)
>>> tokyo.show()
--------------------
|     FU-JI-NO     |
--------------------
|       Cola       |
|       150        |
--------------------
|       Milk       |
|       150        |
--------------------
|       Tea        |
|       150        |
--------------------
|     |      |     |
--------------------
>>> tokyo.sell(kids, 'Cola')
お金が足りません...
>>> tokyo.sell(user1, 'Tea')
ガコンッ
/Tea/
>>> tokyo.sell(user1, 'Milk')
ガコンッ
/Milk/
>>> tokyo.sell(user1, 'Cola')
ガコンッ
/Cola/
>>> print(f'user1:{user1.wallet}円, kids:{kids.wallet}円')
user1:550円, kids:100円
>>> tokyo.stock
{'Cola': 9, 'Milk': 9, 'Tea': 9}
>>> tokyo.stock['Milk'] = 0
>>> tokyo.sell(user1, 'Milk')
売り切れ!
>>> tokyo.restock()
>>> tokyo.stock
{'Cola': 10, 'Milk': 10, 'Tea': 10}
>>> print(f'現在の売り上げは{tokyo.sales}円です。')
現在の売り上げは450円です。

簡略化はしましたが、これで自動販売機を再現できました。

書くときに参考にした資料

クラス――Python3 ドキュメント

[書籍]Python実践入門

このページが少しでもお役に立てたのなら幸いです。

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