検索ガイド -Search Guide-

単語と単語を空白で区切ることで AND 検索になります。
例: python デコレータ ('python' と 'デコレータ' 両方を含む記事を検索します)
単語の前に '-' を付けることで NOT 検索になります。
例: python -デコレータ ('python' は含むが 'デコレータ' は含まない記事を検索します)
" (ダブルクオート) で語句を囲むことで 完全一致検索になります。
例: "python data" 実装 ('python data' と '実装' 両方を含む記事を検索します。'python data 実装' の検索とは異なります。)
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
>>
practical_python_design_patterns

Practical Python Design Patterns - Python で学ぶデザインパターン: The Visitor Pattern part. 1「第18章: ビジターパターン」の巻 投稿一覧へ戻る

Published 2022年8月12日19:43 by mootaro23

SUPPORT UKRAINE

- Your indifference to the act of cruelty can thrive rogue nations like Russia -

Practical Python Design Patterns - The Visitor Pattern 編

Chapter 18: Visitor Pattern
(第18章: ビジターパターン)

I want to believe.
- X-Files
Overview
(概要 - ビジターパターン編)
Python の応用範囲は広いですから、Python を使ってホームオートメーションを構築してみたい、と思っている方もいるかもしれません。シングルボードコンピュータをいくつか入手しそれらをハードウェアセンサーやアクチュエーターと接続すれば、全て自分で制御可能なデバイスネットワークを作ることができます。ネットワーク内の各デバイスはそれぞれ独自の機能を有しています; 家の中の明るさのレベルを計測したり、温度を測定する、etc...。そして、物理的な有線ネットワークと一致する仮想ネットワーク (virtual network) を構成するオブジェクトとして各デバイスをカプセル化することで、ホームオートメーションネットワークシステム全体をプログラム環境における通常のオブジェクトネットワークとして扱うことができるようになります。この章では、全てのデバイスは既に配線済みであり、また、それぞれがすでにオブジェクトとしてモデル化されている、ということを前提として、システムの実装に焦点を当てることにします。
我々のホームオートメーションシステムは以下のデバイスから構築されているとしましょう:
サーモスタット (thermostat)
エアコンディショナー (temperature regulator)
オートロック (front door lock)
コーヒーメーカー (coffee machine)
寝室照明 (bedroom lights)
キッチン照明 (kitchen lights)
システムタイマー (clock)
それぞれのデバイスを象徴するクラスは該当するデバイスの状態をモニタする機能を有しているものとします。またそれぞれのデバイスは取り得る状態によって様々な値を返します。例えば照明デバイスであれば、点灯時は 1、消灯時は 0、デバイスに接続できなかったなどのエラー発生時には -1 が返されるかもしれませんし、サーモスタットからは実際の計測温度、もしくはオフライン時であれば None が返されるかもしれません。玄関のオートロックデバイスは照明と同じく、施錠時は 1、開錠時は 0、エラー時は -1、コーヒーメーカーであればエラー、電源オフ、電源オン、抽出中、待機中、保温中に応じて、それぞれ -1 から 4 までの数値が返ってくるでしょう。エアコンディショナーなら、暖房中、冷房中、電源オン、電源オフ、エラーといった状態に応じたものになるでしょうし、システムタイマーは Python の time オブジェクトかエラー値が返されるでしょう。
これらのデバイスに応じたクラスを実装してみましょう:
import random


class Light:

def __init__(self):
pass

def get_status(self):
return random.choice(range(-1, 2))


class Thermostat:

def __init__(self):
pass

def get_status(self):
temp_range = [x for x in range(-20, 40)]
temp_range.append(None)
return random.choice(temp_range)


class TemperatureRegulator:

def __init__(self):
pass

def get_status(self):
return random.choice(['暖房中', '冷房中', '電源オン', '電源オフ'])


class DoorLock:

def __init__(self):
pass

def get_status(self):
return random.choice(range(-1, 2))


class CoffeeMachine:

def __init__(self):
pass

def get_status(self):
return random.choice(range(-1, 5))


class Clock:

def __init__(self):
pass

def get_status(self):
return f'{random.randrange(24)}:{random.randrange(60)}'


def main():
device_network = [
Light(),
Thermostat(),
TemperatureRegulator(),
DoorLock(),
CoffeeMachine(),
Clock(),
]

for device in device_network:
print(device.get_status())

if __name__ == '__main__':
main()
少し見辛いですが出力結果は次のようになります:
1
18
冷房中
-1
0
19:41
実際のデバイスからの入力、プログラムからの出力はこれよりはるかに「整頓されていない」ものになるはずですが、それでもその特徴はそれなりに反映できているのではないかと思います。
さて、ホームオートメーションシステムを構成するデバイスネットワークのシミュレーションは出来上がりました。これを土台として本来の目的である「デバイスを利用して『何かしらを処理』する」ことを考えていきましょう。
まず最初に思いつく機能は、デバイスが現在オンラインかオフラインかという状態をチェックできるようにするものです。そのために各デバイスを象徴するクラスに is_online() メソッドを定義し、オンライン or オフラインの状態を返すようにしましょう。また同時に各クラスのコンストラクタでは name 属性を受け取るようにします。そうすれば今どのデバイスと「やり取り」をしているのか迷うことはありません:
import random


class Light:

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

def get_status(self):
return random.choice(range(-1, 2))

def is_online(self):
return self.get_status() != -1


class Thermostat:

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

def get_status(self):
temp_range = [x for x in range(-20, 40)]
temp_range.append(None)
return random.choice(temp_range)

def is_online(self):
return self.get_status() is not None


class TemperatureRegulator:

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

def get_status(self):
return random.choice(['暖房中', '冷房中', '電源オン', '電源オフ'])

def is_online(self):
return self.get_status() != '電源オフ'


class DoorLock:

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

def get_status(self):
return random.choice(range(-1, 2))

def is_online(self):
return self.get_status() != -1


class CoffeeMachine:

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

def get_status(self):
return random.choice(range(-1, 5))

def is_online(self):
return self.get_status() != -1


class Clock:

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

def get_status(self):
return f'{random.randrange(24)}:{random.randrange(60)}'

def is_online(self):
return True


def main():
device_network = [
Light('寝室照明'),
Light('キッチン照明'),
Thermostat('サーモスタット'),
TemperatureRegulator('エアコンディショナー'),
DoorLock('オートロック'),
CoffeeMachine('コーヒーメーカー'),
Clock('システムタイマー'),
]

for device in device_network:
space_len = len('エアコンディショナー') - len(device.name)
arrange_len = len(device.name) + space_len * 2
print(f'{device.name:<{arrange_len}}: {"On" if device.is_online() else "Off"}')

if __name__ == '__main__':
main()
実行結果はランダムですから出力の値は異なるかもしれませんが、形式は次のようになるはずです:
寝室照明 : Off
キッチン照明 : Off
サーモスタット : On
エアコンディショナー: Off
オートロック : Off
コーヒーメーカー : On
システムタイマー : On
次なる機能は、全てのデバイスを稼働 (On) させそれぞれの初期状態にするブート機能です。システムタイマーは 00:00 にセットし、コーヒーメーカーを起動し、全ての照明をオフにし、エアコンディショナーをオンにする (ただし設定は変更しません)、といった具合です。オートロックに関しては現在の状態を維持するようにしましょう。
さてブート時にも各デバイスの状態が変化するようになりますから、デバイスの現在の状態を保持しておくための属性を __init__() で定義しておきたいと思います。
それからもう1つ、unittest ライブラリから TestCase クラスをインポートしてテストを記述してみましょう。今回追加するブート機能を実行した際に、各デバイスがちゃんと期待通りの状態になっているかを検証します。このシリーズは Python におけるユニットテストのチュートリアルではありませんからそれほど細かいところまでは取り上げませんが、それでも Python に備わっているテストツールを使いこなせるくらいの知識は得られるはずです:
import random
import unittest


class Light:
"""
照明器具の状態:
-1: エラー
0: OFF
1: ON
"""

def __init__(self, name):
self.name = name
self.status = self.get_status()

def get_status(self):
return random.choice(range(-1, 2))

def is_online(self):
return self.get_status() != -1

def boot_up(self):
self.status = 0


class Thermostat:
"""
サーモスタットの状態:
None: エラー
それ以外: 現在温度
"""

def __init__(self, name):
self.name = name
self.status = self.get_status()

def get_status(self):
temp_range = [x for x in range(-20, 40)]
temp_range.append(None)
return random.choice(temp_range)

def is_online(self):
return self.get_status() is not None

def boot_up(self):
pass


class TemperatureRegulator:
"""
エアーコンディショナーの状態:
暖房中、冷房中、電源オン、電源オフ、のいずれか
"""

def __init__(self, name):
self.name = name
self.status = self.get_status()

def get_status(self):
return random.choice(['暖房中', '冷房中', '電源オン', '電源オフ'])

def is_online(self):
return self.get_status() != '電源オフ'

def boot_up(self):
self.status = '電源オン'


class DoorLock:
"""
オートロックの状態:
-1: エラー
0: 開錠
1: 施錠
"""

def __init__(self, name):
self.name = name
self.status = self.get_status()

def get_status(self):
return random.choice(range(-1, 2))

def is_online(self):
return self.get_status() != -1

def boot_up(self):
pass


class CoffeeMachine:
"""
コーヒーメーカーの状態:
-1: エラー
0: 電源OFF
1: 電源ON
2: 抽出中
3: 待機中
4: 保温中
"""

def __init__(self, name):
self.name = name
self.status = self.get_status()

def get_status(self):
return random.choice(range(-1, 5))

def is_online(self):
return self.get_status() != -1

def boot_up(self):
self.status = 1


class Clock:
"""
システムタイマーの状態:
hh:mm フォーマット
"""

def __init__(self, name):
self.name = name
self.status = self.get_status()

def get_status(self):
return f'{random.randrange(24)}:{random.randrange(60)}'

def is_online(self):
return True

def boot_up(self):
self.status = '00:00'


class HomeAutomationBootTest(unittest.TestCase):

def setUp(self):
self.bedroom_light = Light('寝室照明')
self.thermostat = Thermostat('サーモスタット')
self.thermal_regulator = TemperatureRegulator('エアコンディショナー')
self.front_door_lock = DoorLock('オートロック')
self.coffee_machine = CoffeeMachine('コーヒーメーカー')
self.system_clock = Clock('システムタイマー')

def test_boot_light_turns_it_off(self):
self.bedroom_light.boot_up()
self.assertEqual(self.bedroom_light.status, 0)

def test_boot_thermostat_does_nothing_to_state(self):
state_before = self.thermostat.status
self.thermostat.boot_up()
self.assertEqual(state_before, self.thermostat.status)

def test_boot_thermal_regulator_turns_it_on(self):
self.thermal_regulator.boot_up()
self.assertEqual(self.thermal_regulator.status, '電源オン')

def test_boot_front_door_lock_does_nothing_to_state(self):
state_before = self.front_door_lock.status
self.front_door_lock.boot_up()
self.assertEqual(state_before, self.front_door_lock.status)

def test_boot_coffee_machine_truns_it_on(self):
self.coffee_machine.boot_up()
self.assertEqual(self.coffee_machine.status, 1)

def test_boot_system_clock_zeros_it(self):
self.system_clock.boot_up()
self.assertEqual(self.system_clock.status, '00:00')


if __name__ == '__main__':
unittest.main()
コマンドラインからプログラムを実行する際のエントリポイントを unittest.main() に設定すると Python インタプリタは unittest.TestCase のサブクラスを探し、そのクラス内で定義されている test で始まる名前を持つ関数を実行します。ここでは6つのテストを記述しましたので実行結果は次のようになります:
......
----------------------------------------------------------------------
Ran 6 tests in 0.000s

OK
この例では、それぞれのデバイス (を象徴するクラス) の初期値として「望ましいであろう」値を設定しましたが、異なる複数のプロファイル (profile: 選択の組み合わせ) を実装したい場合にはどうしたらよいのでしょうか?つまり、もし「人物1」と「人物2」がこの家をシェアしていたとしたら、それぞれの起きる時刻も、眠りにつく時間も、朝晩のルーティンも、好みの温度帯も全てが全く同じ、ということは勿論ないはずです。まぁ、ハウスシェアをするぐらいですからこの二人の仲は非常によく、お互いを思いやる気持ちも人一倍持っています。ですから二人が一緒に家にいるときには、二人の好みのちょうど中間でうまく折り合いをつけられるでしょう。しかしどちらかしか家にいない時ぐらいは自分の「最適な環境」でくつろぎたいでしょう。
これまでも見てきた「単純な拡張」を行なった場合は次のようになるでしょう:
import random
import unittest


class Light:
"""
照明器具の状態:
-1: エラー
0: OFF
1: ON
"""

def __init__(self, name):
self.name = name
self.status = self.get_status()

def get_status(self):
return random.choice(range(-1, 2))

def is_online(self):
return self.get_status() != -1

def boot_up(self):
self.status = 0

def update_status(self, person_1_home, person_2_home):
if person_1_home:
if person_2_home:
self.status = 1
else:
self.status = 0
elif person_2_home:
self.status = 1
else:
self.status = 0


class Thermostat:
"""
サーモスタットの状態:
None: エラー
それ以外: 現在温度
"""

def __init__(self, name):
self.name = name
self.status = self.get_status()

def get_status(self):
temp_range = [x for x in range(-20, 40)]
temp_range.append(None)
return random.choice(temp_range)

def is_online(self):
return self.get_status() is not None

def boot_up(self):
pass

def update_status(self, person_1_home, person_2_home):
pass


class TemperatureRegulator:
"""
エアーコンディショナーの状態:
暖房中、冷房中、電源オン、電源オフ、のいずれか
"""

def __init__(self, name):
self.name = name
self.status = self.get_status()

def get_status(self):
return random.choice(['暖房中', '冷房中', '電源オン', '電源オフ'])

def is_online(self):
return self.get_status() != '電源オフ'

def boot_up(self):
self.status = '電源オン'

def update_status(self, person_1_home, person_2_home):
if person_1_home:
if person_2_home:
self.status = '電源オン'
else:
self.status = '暖房中'
elif person_2_home:
self.status = '冷房中'
else:
self.status = '電源オフ'


class DoorLock:
"""
オートロックの状態:
-1: エラー
0: 開錠
1: 施錠
"""

def __init__(self, name):
self.name = name
self.status = self.get_status()

def get_status(self):
return random.choice(range(-1, 2))

def is_online(self):
return self.get_status() != -1

def boot_up(self):
pass

def update_status(self, person_1_home, person_2_home):
if person_1_home:
self.status = 0
elif person_2_home:
self.status = 1
else:
self.status = 1


class CoffeeMachine:
"""
コーヒーメーカーの状態:
-1: エラー
0: 電源OFF
1: 電源ON
2: 抽出中
3: 待機中
4: 保温中
"""

def __init__(self, name):
self.name = name
self.status = self.get_status()

def get_status(self):
return random.choice(range(-1, 5))

def is_online(self):
return self.get_status() != -1

def boot_up(self):
self.status = 1

def update_status(self, person_1_home, person_2_home):
if person_1_home:
if person_2_home:
self.status = 2
else:
self.status = 3
elif person_2_home:
self.status = 4
else:
self.status = 0


class Clock:
"""
システムタイマーの状態:
hh:mm フォーマット
"""

def __init__(self, name):
self.name = name
self.status = self.get_status()

def get_status(self):
return f'{random.randrange(24)}:{random.randrange(60)}'

def is_online(self):
return True

def boot_up(self):
self.status = '00:00'

def update_status(self, person_1_home, person_2_home):
if person_1_home:
if person_2_home:
pass
else:
self.status = '00:01'
elif person_2_home:
self.status = '20:22'
else:
pass


class HomeAutomationWhoIsAtHomeTest(unittest.TestCase):

def setUp(self):
self.bedroom_light = Light('寝室照明')
self.thermostat = Thermostat('サーモスタット')
self.thermal_regulator = TemperatureRegulator('エアコンディショナー')
self.front_door_lock = DoorLock('オートロック')
self.coffee_machine = CoffeeMachine('コーヒーメーカー')
self.system_clock = Clock('システムタイマー')

def test_thermal_regulator_on_when_both_at_home(self):
self.thermal_regulator.update_status(True, True)
self.assertEqual(self.thermal_regulator.status, '電源オン')

def test_thermal_regulator_heating_when_only_person_1_at_home(self):
self.thermal_regulator.update_status(True, False)
self.assertEqual(self.thermal_regulator.status, '暖房中')

def test_thermal_regulator_cooling_when_only_person_2_at_home(self):
self.thermal_regulator.update_status(False, True)
self.assertEqual(self.thermal_regulator.status, '冷房中')

def test_thermal_regulator_off_when_no_one_at_home(self):
self.thermal_regulator.update_status(False, False)
self.assertEqual(self.thermal_regulator.status, '電源オフ')

if __name__ == '__main__':
unittest.main()
ここではエアコンディショナーについてのテストだけ記述してあります。是非すべてのパターンについてテストを記述して見てください。
さて、この実装方法では既に収拾がつかなくなりつつありますね、何とかしてもっと「クリーン」なコードにしなければいけません。State machine を使用することはできるでしょうか?できるかもしれませんが、ホームオートメーションを構成するそれぞれのデバイスにおけるそれぞれの状態を state machine として実装するのはどうもうまくいきそうもありません。もっと何か良い方法が必要です。
どんな方法が良いのか、を探るためにも、我々がしたいと考えていることは何なのか、を良く考えてみる必要がありそうです。