工場の人数計測で使うクラスの説明

PyQのクエスト「ポリモーフィズム(多態性)」で写経するクラスの説明をします。

統計量クラス概要

2つの統計量のクラスと1つのクラスの概要を説明します。

  • StatCount:観測統計量クラス

  • StatTime:時間統計量クラス(StatCountの派生クラス)

  • RoomCounter:エアシャワーと工場内の統計を管理するクラス

統計量クラスのオブジェクトは、実数のように使えます。ただし、どんな値を取ったかの統計量を取ることができます

以下のメソッドがあります。

  • assign:新しい値を代入します。

  • value:現在の値。

  • count:代入した回数。

  • mean:これまでの値の平均値。


観測統計量クラス

実数のように使えて、代入した値の回数や平均を取得できます。

class StatCount:
    def __init__(self):
        self._value = 0  # 現在値
        self._count = 0  # 代入回数
        self._sum = 0  # 合計

    def assign(self, value):
        self._value = value
        self._count += 1
        self._sum += value

    def value(self):
        """現在値"""
        return self._value

    def count(self):
        """代入回数"""
        return self._count

    def mean(self):
        """代入あたりの平均"""
        return self._sum / max(1, self._count)  # ゼロ割を避けるため

観測統計量の使用例

i = StatCount()
i.assign(10)
i.assign(20)
print(i.value())  # 20
print(i.count())  # 2
print(i.mean())  # 15.0
  • i = StatCount() のようにして観測統計量オブジェクトを作成します。

  • 代入は、i.assign(10)のようにします。

  • 現在の値の取得は、i.value()で取れます。

  • 代入した回数は、i.count()で取れます。

  • 代入した値の平均は、i.mean()で取れます。ここでは「10と20」を代入したので、平均は15です。


時間統計量クラス

StatTimeのオブジェクトも実数のように使えて、代入した値の回数や平均を取得できます。 ただし、平均の計算方法がStatCountと異なります。

StatTimemeanは時間平均を返します。 f(t)を時刻tにおける値とすると、時間平均は下記のように計算します。

時間平均 = (t0からt1までのf(t)の積分値) /(t1 - t0)

ただし、t0StatTimeオブジェクトの作成時刻を、t1は現在時刻を表します。

たとえば、人気ラーメン店の店先に並ぶ行列の長さを時間統計量オブジェクトで表したとすると、その平均は行列の時間平均になります。

class StatTime(StatCount):
    Time = 0  # 時刻

    def __init__(self):
        super().__init__()  # StatCountの初期化処理
        self._initime = StatTime.Time  # 作成時の時刻
        self._pretime = 0  # 前回代入時の時刻

    def assign(self, value):
        t = StatTime.Time - self._pretime  # 前回からの時間
        self._pretime = StatTime.Time  # 時刻を更新
        s = self._sum + self._value * t  # 合計面積
        super().assign(value)  # StatCountの代入処理
        self._sum = s  # _sumは再設定

    def mean(self):
        """時間で見た平均"""
        t = StatTime.Time - self._pretime  # 前回からの時間
        s = self._sum + self._value * t  # 合計面積
        return s / (StatTime.Time - self._initime)

時間統計量の使用例

StatTime.Time = 9
t = StatTime()
t.assign(1)
StatTime.Time = 11
t.assign(4)
print(t.mean())  # 1.0
StatTime.Time = 12
print(t.mean())  # 2.0
  • StatTimeクラスを使うときは、StatTime.Timeで時刻を管理します。

  • StatTime.Time = 9で時刻を9時にします。時間の単位は利用者が自由に決められます。ここでは時間の単位をhourとします。

  • t = StatTime()で時間統計量オブジェクトを作成します。作成時刻が9時なので、統計量は9時から計算します。

  • t.assign(1)で9時での値を1にします。

  • StatTime.Time = 11で時刻を11時に進めます。

  • t.assign(4)で11時での値を4にします。

  • print(t.mean())時間平均を1と出力します。11時での値は4ですが、9時から11時まででは値が1であったことに注意してください。

  • StatTime.Time = 12で時刻を12時に進めます。

  • print(t.mean())時間平均を2と出力します。

12時での時間平均の計算を図で確認してみましょう。

https://images.pyq.jp/repo/prod/oop_basic_5/oop_basic_03.jpg

  • 図の青い部分の面積が(1 * (11 - 9) + 4 * (12 - 11))です。

  • 時間平均は、9時から12時までの3時間(12 - 9)で計算します。

  • 時間平均は、(1 * (11 - 9) + 4 * (12 - 11)) / (12 - 9)を計算して2になります。

  • より詳細な計算は後述の「時間平均の計算の流れ」を確認ください。

時間統計量クラスのやっていること

時間統計量(StatTime)クラスは観測統計量(StatCount)クラスを継承しているので、統計量を計算できます。 しかし、以下のように違いがあります。

  • 統計量クラスの平均:代入回数を分母とした平均

  • 時間統計量クラスの平均:時間を分母とした平均

すなわち、meanで時間平均の計算、assignで値の割当を修正しないといけません。

また、これらの計算に必要な変数は統計量クラスと同じで、さらに、変数の初期化処理もまったく同じです(具体的には、_value, _count, _sumに0を代入します)。 このことから、時間統計量クラスの__init__メソッドでは、統計量クラスの__init__メソッドを呼んで初期化しています(super().__init__())。

meanメソッドでやっていること

現在時刻における時間平均(グラフの面積/時間)を計算します。 面積の更新は、assignメソッドでしています。

assignメソッドでやっていること

時刻は、StatTime.Timeで管理しています。 時間統計量クラスではself._sumに、下図の青い部分の面積を計算して入れることにします。 また、統計量の計算の開始時刻をself._initimeに、assignの実行時刻をself._pretimeに入れます。 このとき、時間平均は、self._sum / (StatTime.Time - self._initime)になります。

https://images.pyq.jp/repo/prod/oop_basic_5/oop_basic_03.jpg

時間平均の計算の流れ

  • 9:00:self._sumを0で、self._initimeとを開始時刻9:00で初期化

  • 9:00:self._valueを1に、self._pretimeを時刻9:00に設定

  • 11:00:self._sumに「self._value×(現在時刻−前回代入時の時刻)= 1×2」を追加。self._pretimeを時刻11:00に設定。

  • 12:00:self._sumに「self._value×(現在時刻−前回代入時の時刻)= 4×1」を追加し、時間平均(= (1*2+4*1)/(12-9) = 2)と計算

assignでは、sに「前回代入してから今回代入するまでの面積」を計算しています。 super().assign(value)を実行すると、StatCountクラスのassignメソッドを実行します。そこでは「self._sumに別の値を設定する」という余計な処理をするので、直前に退避したsを戻しています。


RoomCounterクラス

RoomCounterは、エアシャワーの利用者数と工場の利用者数の統計量を管理するクラスです。 工場の利用者は、エアシャワーとクリーンルームの両方の利用者を対象にします。

  • エアシャワーの利用者数は、観測統計量(StatCount)で計算します。

  • 工場の利用者数は、時間統計量(StatTime)で計算します。

エアシャワーの利用者数のクラス

エアシャワーでは、除菌剤を使用しているため、1日に何回利用されたか知りたいそうです。 また、1度の利用者数の平均を元に、除菌剤の量も適切にしたいそうです。

このことから、同時に利用する人の回数や平均が必要です。時間は無関係の人数を計測すれば良いので、StatCountを使います。

工場の利用者数のクラス

工場では、労務上、時間で見た利用者数の平均(時間平均)が必要です。したがって、StatTimeを使います。

class RoomCounter:
    def __init__(self):
        self._num_air = StatCount()
        self._num_room = StatTime()

    def room_in(self, num):
        """エアシャワーにnum人、入る"""
        self._num_air.assign(num)
        self._num_room.assign(self._num_room.value() + num)

    def room_out(self):
        """工場から1人出る"""
        self._num_room.assign(self._num_room.value() - 1)

    def show_mean(self):
        """エアシャワーと工場内の平均表示"""
        print('エアシャワーの利用人数の回数平均', self._num_air.mean())
        print('工場内の人数の時間平均', self._num_room.mean())

RoomCounterのメソッド

  • room_in:工場にnum人、入ります。最初はエアシャワーに入り、その後クリーンルームに移動します。

  • room_out:工場から1人出ます。入るときは複数ですが、出るときは1人づつであることに注意してください。

  • show_mean:エアシャワーと工場内の平均を表示します。


継承について

「BはAである」ときだけ、クラスAからクラスBを継承してください。これをリスコフの置換原則といいます。このクエストでは「時間統計量は統計量である」が言えています。

参考:リスコフの置換原則

この原則が成り立つときでも、継承はなるべく使わない方がよいでしょう。理由は、クラスの結びつきが強くなるためです。その代わりに、結びつきが弱くなる方法としてコンポジション(※)がオススメです。

※ 別のクラスのオブジェクトを部品としてデータ属性に持つ方法をコンポジションといいます。

変数名のアンダースコアについて

RoomCounterクラスのデータ属性_num_air, _num_roomの変数名は、アンダースコア(_)で始まっています。

名前をアンダースコアで始めると、「名前を公開したくない」という意味になります。 ただし、習慣的なものなので、_num_airを直接使ってもエラーにはなりません。

しかし自由に値を変更されると、正しい状態を維持できない可能性があります。 アンダースコアで始まる変数にアクセスしていると、「何かおかしいことをしている」と注意を促すことになります。