Python デザインパターン サンプルコード Composite
Mark Summerfield『実践 Python 3』デザインパターンのサンプルコード
Python3(3.11)で動くソースコード(.pyファイル .ipynbファイル)あります
「anaconda3」on .py「PyCharm」.ipynb「Jupyter Notebook」
著作権の問題があるので,本に書いてないことだけを解説します。つまり,視点を変えて解説します。
◆◆Composite パターンの使われる場面◆◆
以下は,同じサイトの別記事について説明されている。
https://yamakatsusan.web.fc2.com/hp_software/pythonpattern15.html
『Composite パターン 結城 浩「Java言語で学ぶデザインパターン入門」をPython化』
Composite とは合成物や複合体のようなニュアンスがある。Composite パターンはツリー構造の一種であるが,ノードが2種類あるので Composite パターンと言われる。
Java サンプルはディレクトリエントリを対象としている。Windows で言えば,フォルダ(ディレクトリ)とファイルである。一般的なツリー構造と違う特徴は,フォルダは中身がなくてもフォルダであり将来に中身が入る可能性があり,ファイルは中身を入れることができない。
Java サンプルの出力のツリーを Windows Explorer 風に表現したもの(カッコ内はサイズ)
root
= bin
= vi(10000)
= latex(20000)
= tmp
= usr
= yuki
= diary.html(100)
= Composite.java(200)
= hanako
= memo.tex(300)
= tomura
= game.doc(400)
= junk.mail(500)
本サンプルコードの出力は下の方に表示してあります。
このような特徴のあるツリー構造を従来からあるグラフ理論のアルゴリズムではなくオブジェクト指向の考えで実装すると,GoF が言うようにフォルダ(ディレクトリ)とファイルを区別することなく扱うことができるようになる。結城氏はこのことを容器と中身の同一視と表現した。
GoFのC++サンプルは製品と部品(と中間にサブアッシー)を部分―全体階層(子が次に親になる階層のこと)に適用している。
ツリー構造の従来のアルゴリズムは,ノードの表現をリストや dict 辞書やその他であってもネスト(入れ子)により再帰定義を表現するのが普通である。Composite パターンでは,メソッドの中で自分自身を呼ぶ再帰呼び出しを使い,さらにインスタンスを巧みに使い分けることによって容器と中身の扱いを同じにしているのである。
この発想は個人が独力で思いつくことが難しいと思われ,この再帰呼び出しを応用するだけでも十分に役に立つと考えられる。
(2023-11-17)Python3.11で動作確認済み
◆◆Composite パターンとは◆◆
GoFによれば,Composite パターンの目的は,
「部分―全体階層を表現するために,オブジェクトを木構造に組み立てる。Composite パターンにより,クライアントは,個々のオブジェクトとオブジェクトを合成したものを一様に扱うことができるようになる。」
GoFによれば,次のような場合に,Composite パターンを使用する。
・オブジェクトの部分―全体階層を表現したい場合。
・クライアントが,オブジェクトを合成したものと個々のオブジェクトの違いを無視できるようにしたい場合。このパターンを用いることで,クライアントは,composite 構造内のすべてのオブジェクトを一様に扱うことができるようになる。
GoFによれば,Composite パターンの関連するパターンは次のようなものである。
Chain Of Responsibility パターン:親子関係にあるオブジェクト間のリンクは,Chain Of Responsibility パターンでしばしば使われる。
Decorator パターン:しばしば Composite パターンとともに使われる。decorator と composite を同時に使う場合,通常,これらは共通の親クラスを持つ。そのため decorator は,Add,Remove,GetChild のようなオペレーションで Component クラスのインタフェースをサポートしなければならなくなる。
Flyweight パターン:このパターンにより,component を共有できるようになる。しかし,共有されるオブジェクトは親オブジェクトを参照できなくなる。
Iterator パターン:composite を走査するために使われる。
Visitor パターン:Composite クラスや Leaf クラスに分散しているオペレーションや振る舞いを局所化する。
◆◆Composite パターンのサンプルのクラス構成◆◆
ソースコードは巻末にあり,ソースファイルはこのWebの左上隅にありダウンロードできます。
サンプルは2つあり,stationery1.py は,巻頭で紹介した別記事と同じで,古典的な方法である。
stationery2.py は,より進んだ方法であり,このWebではこちらを解説する。
【stationery2.py】
Item
(内容省略)
class Item は,コンポジットオブジェクトも非コンポジットオブジェクトも区別なく扱うことができます。
◆◆Composite パターンの実装◆◆
stationery1.py, stationery2.py の実装の第1の特徴は if 文が少ないということです。コンポジットオブジェクトと非コンポジットオブジェクトを区別する数か所にあるだけです。
最後にある if は,決り文句であり,メインルーチンを走らせるものです。オブジェクト指向を徹底すると if 文を減らせます。そうすると,アルゴリズムが簡単になりミスもなくなります。
Composite パターンのオブジェクトの使い方は独力で思いつくようなものではないです。そのあたりを詳細に説明します。
サンプルは,いわゆるツリー構造を提供するものですが,そのノードがコンポジットオブジェクトと非コンポジットオブジェクトの2種類あります。前者が「容器」に相当するもの,後者が「中身」に相当するものであり,サンプル出力を見ればすぐ判り,説明は不要であろう。
【サンプル出力】
$0.40 Pencil
$1.60 Ruler
$0.20 Eraser
$2.20 Pencil Set
$0.40 Pencil
$1.60 Ruler
$0.20 Eraser
$3.60 Boxed Pencil Set
$1.00 Box
$2.20 Pencil Set
$0.40 Pencil
$1.60 Ruler
$0.20 Eraser
$0.40 Pencil
$1.20 Pencil
$0.20 Eraser
$1.00 Box
$0.40 Pencil
コンポジットオブジェクトである容器に相当するものには,いくつかの中身がぶら下がっているし,中身の価格の合計が自動的に計算されている。
当然,非コンポジットオブジェクトである中身に相当するものには,それ以上のツリーはぶら下がらない。
これらの2種類のオブジェクトを区別なく扱うことができるのが,Composite パターンである。
Pyhtonでは,オブジェクトをつくるものをファクトリと呼ぶ。
非コンポジットオブジェクトつくるファクトリクラスメソッドが47行目にあり,同じ役割のファクトリ関数が92行目にあります。その使用例が,17行目と19行目にあります。引数に自分の名称と価格があります。
コンポジットオブジェクトつくるファクトリクラスメソッドが52行目にあり,同じ役割のファクトリ関数が96行目にあります。その使用例が,20行目と22行目にあります。引数に自分の名称と中身があります。中身はリストに入ります。
2種類ずつあるのは,便宜上,2種類の方法を提供しているだけです。
62行目のメソッド add は,itertools.chain によって,ぶら下がるオブジェクトを増やすことができる。驚異的なことに,非コンポジットをコンポジットに変換することができる。その例が27行目の pencil である。
66行目のメソッド remove は,ぶら下がるオブジェクトを減らすことができる。驚異的なことに,コンポジットを非コンポジットに変換することができる。その例が30行目,32行目の pencil である。
75行目と81行目に同じ名前のメソッド
def price
があり,デコレータ
@property
@price.setter
が付いている。前者が getter であり,後者が setter である。
getter は,ぶら下がるオブジェクトがあれば,価格の合計を計算している。setter は価格の代入に使われている(コメントアウトしてみればちゃんと使われているのが判る)。
特殊メソッド__iter__は,for in ループから呼ばれる。リスト children をイテラブル(反復呼出し可)にしているだけである。イテラブルとは for in ループを使えるようにする順序取り出しを可能にすることである。たいていの場合はこの特殊メソッドは明示されていなくても裏で動作している。
85行目のメソッド print は,この Composite パターンのポイントである。ぶら下がるオブジェクトを再帰的に呼んでいるので,きれいな階層が表現できる。
巻頭で紹介した古典的な方法では,コンポジットと非コンポジットが別のクラスなので,階層を表現するのが複雑になってしまった。debugで苦労したのを覚えている。
◆◆ソースコード◆◆
このWebページの左上隅からダウンロードできます。
2つのソースファイル,1つのサンプル出力があります。
・stationery1.py;Composite パターンのPythonサンプル(その1)(掲載・解説なし)
・stationery2.py;Composite パターンのPythonサンプル(その2)
【stationery2.py】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
#!/usr/bin/env python3
# Copyright c 2012-13 Qtrac Ltd. All rights reserved.
# This program or module is free software: you can redistribute it
# and/or modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version. It is provided for
# educational purposes and is distributed in the hope that it will be
# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
import itertools
import sys
def main():
pencil = Item.create("Pencil", 0.40)
ruler = Item.create("Ruler", 1.60)
eraser = make_item("Eraser", 0.20)
pencilSet = Item.compose("Pencil Set", pencil, ruler, eraser)
box = Item.create("Box", 1.00)
boxedPencilSet = make_composite("Boxed Pencil Set", box, pencilSet)
boxedPencilSet.add(pencil)
for item in (pencil, ruler, eraser, pencilSet, boxedPencilSet):
item.print()
assert not pencil.composite
pencil.add(eraser, box)
assert pencil.composite
pencil.print()
pencil.remove(eraser)
assert pencil.composite
pencil.remove(box)
assert not pencil.composite
pencil.print()
class Item:
def __init__(self, name, *items, price=0.00):
self.name = name
self.price = price
self.children = []
if items:
self.add(*items)
@classmethod
def create(Class, name, price):
return Class(name, price=price)
@classmethod
def compose(Class, name, *items):
return Class(name, *items)
@property
def composite(self):
return bool(self.children)
def add(self, first, *items):
self.children.extend(itertools.chain((first,), items))
def remove(self, item):
self.children.remove(item)
def __iter__(self):
return iter(self.children)
@property
def price(self):
return (sum(item.price for item in self) if self.children else
self.__price)
@price.setter
def price(self, price):
self.__price = price
def print(self, indent="", file=sys.stdout):
print("{}${:.2f} {}".format(indent, self.price, self.name),
file=file)
for child in self:
child.print(indent + " ")
def make_item(name, price):
return Item(name, price=price)
def make_composite(name, *items):
return Item(name, *items)
if __name__ == "__main__":
main()
以上