まえがき
Python100本ノックについての記事です。既存の100本ノックは幾分簡単すぎるようにも感じており、それに対するアンサー記事となります。誤りなどがあれば、ご指摘ください。今回は本番編として、継承・ポリモーフィズム・特殊メソッドを中心に10問扱います。
Q.51
| 項目 | 内容 |
|---|---|
| クラス名 |
Shape3D(ABC) + Sphere, Cube, Cylinder
|
| 発展仕様 | - abc.ABC, @abstractmethod による抽象基底設計- volume/surface_areaの抽象定義- @total_ordering で体積比較- __eq__, __lt__ で比較対応- __repr__ 明示設計- to_dict, to_json によるシリアライズ対応 |
| 使用構文 |
abc.ABC, @abstractmethod, @total_ordering, __eq__, __lt__, math, json, __repr__, super()
|
| 問題文 |
Shape3D 抽象基底クラスを定義し、volume()・surface_area() の抽象メソッドを持たせよ。その上で、球体 Sphere・立方体 Cube・円柱 Cylinder を具象サブクラスとして実装し、体積順の大小比較・等価比較・repr表現・辞書/JSON形式のシリアライズ機能も実装すること。 また、浮動小数点の誤差にも配慮した比較演算が行えるようにせよ。 |
A.51
■ 模範解答
from abc import ABC, abstractmethod # 抽象基底クラスのためのインポート
from functools import total_ordering # __lt__ 定義だけで他の比較演算を補完
import math
import json
@total_ordering
class Shape3D(ABC):
@abstractmethod
def volume(self) -> float:
"""体積を返す抽象メソッド"""
pass
@abstractmethod
def surface_area(self) -> float:
"""表面積を返す抽象メソッド"""
pass
@abstractmethod
def to_dict(self) -> dict:
"""シリアライズ用:辞書変換"""
pass
def to_json(self) -> str:
# JSON文字列に変換(共通実装)
return json.dumps(self.to_dict())
def __eq__(self, other) -> bool:
# 等価比較は体積で行う
if not isinstance(other, Shape3D):
return NotImplemented
return math.isclose(self.volume(), other.volume(), rel_tol=1e-6)
def __lt__(self, other) -> bool:
# 小なり比較も体積基準
if not isinstance(other, Shape3D):
return NotImplemented
return self.volume() < other.volume()
def __repr__(self) -> str:
return f"<{self.__class__.__name__}: volume={self.volume():.2f}, surface={self.surface_area():.2f}>"
# -------------------
# 各サブクラス定義
# -------------------
class Sphere(Shape3D):
def __init__(self, radius: float):
self.radius = radius
def volume(self) -> float:
return (4/3) * math.pi * self.radius ** 3
def surface_area(self) -> float:
return 4 * math.pi * self.radius ** 2
def to_dict(self) -> dict:
return {"type": "Sphere", "radius": self.radius}
class Cube(Shape3D):
def __init__(self, side: float):
self.side = side
def volume(self) -> float:
return self.side ** 3
def surface_area(self) -> float:
return 6 * self.side ** 2
def to_dict(self) -> dict:
return {"type": "Cube", "side": self.side}
class Cylinder(Shape3D):
def __init__(self, radius: float, height: float):
self.radius = radius
self.height = height
def volume(self) -> float:
return math.pi * self.radius ** 2 * self.height
def surface_area(self) -> float:
return 2 * math.pi * self.radius * (self.radius + self.height)
def to_dict(self) -> dict:
return {"type": "Cylinder", "radius": self.radius, "height": self.height}
s1 = Sphere(2)
c1 = Cube(2.5)
cy1 = Cylinder(1, 6)
print(s1) # <Sphere: volume=33.51, surface=50.27>
print(c1) # <Cube: volume=15.62, surface=37.50>
print(s1 > c1) # True
print(s1.to_json()) # {"type": "Sphere", "radius": 2}
shapes = [Cylinder(1, 2), Sphere(1.5), Cube(2)]
sorted_shapes = sorted(shapes) # 体積順に並び替え
for s in sorted_shapes:
print(s)
print(Sphere(1.5) == Sphere(1.5)) # True
print(Sphere(1.5) == Cylinder(1, 3)) # False(体積が異なるため)
■ 文法・構文まとめ
| 使用構文 | 解説 |
|---|---|
abc.ABC |
抽象基底クラスの設計により「共通インターフェース」を強制する構造を実現 |
@abstractmethod |
抽象メソッドとして volume / surface_area / to_dict を定義 |
@total_ordering |
__lt__ と __eq__ の定義だけで全ての比較演算子が機能する |
math.isclose() |
浮動小数点の比較に安全な体積等価判定 |
json.dumps() |
クラス状態をJSONとして出力可能 |
super() |
継承関係の構造設計でも活用可能(今回は不要だが拡張に備え意識させる) |
Q.52
| 項目 | 内容 |
|---|---|
| クラス名 |
LoggerMixin, DataEntity
|
| 使用構文 | 多重継承, super(), logging, __getattr__, __setattr__, @property, __slots__, isinstance, datetime, typing
|
| 問題文 | 属性アクセス時にログを出力し、更新時には型チェックおよび自動保存を行う LoggerMixin を定義せよ。これを継承する DataEntity クラスでは name (str) と age (int) を保持し、値の変更はログに記録されるとともに、型が合致しない場合は例外を送出せよ。ログには日時も記録され、保存処理はコンソール出力により模倣されること。 |
A.52
■ 模範解答
import logging
from datetime import datetime
from typing import Any
# ロガーの初期設定
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
class LoggerMixin:
def __getattr__(self, name: str) -> Any:
# 存在しない属性にアクセスした際のログ出力
logging.info(f"[GET] 属性 '{name}' を取得しようとしました")
raise AttributeError(f"{name} 属性は存在しません")
def __setattr__(self, name: str, value: Any) -> None:
# 属性を更新する際のログ出力と自動保存
if hasattr(self, '__slots__') and name in self.__slots__:
expected_type = self.__annotations__.get(name, Any)
if not isinstance(value, expected_type):
raise TypeError(f"{name} には {expected_type.__name__} 型が必要です")
logging.info(f"[SET] 属性 '{name}' を {value!r} に更新")
super().__setattr__(name, value)
self.save() # 自動保存を模擬的に呼び出す
def save(self):
# 保存処理の模倣(ここではコンソール出力)
print(f"[{datetime.now()}] データ保存完了")
class DataEntity(LoggerMixin):
__slots__ = ('name', 'age')
name: str
age: int
def __init__(self, name: str, age: int):
self.name = name # __setattr__ 経由
self.age = age
def __repr__(self):
return f"<DataEntity name={self.name!r}, age={self.age}>"
entity = DataEntity("Alice", 30)
entity.age = 31
INFO - [SET] 属性 'name' を 'Alice' に更新
[2025-07-27 22:00:00] データ保存完了
INFO - [SET] 属性 'age' を 30 に更新
[2025-07-27 22:00:00] データ保存完了
INFO - [SET] 属性 'age' を 31 に更新
[2025-07-27 22:00:00] データ保存完了
entity = DataEntity("Bob", 40)
entity.age = "forty"
INFO - [SET] 属性 'name' を 'Bob' に更新
[2025-07-27 22:00:00] データ保存完了
INFO - [SET] 属性 'age' を 40 に更新
[2025-07-27 22:00:00] データ保存完了
TypeError: age には int 型が必要です
■ 文法・構文まとめ
| 構文・機能 | 説明 |
|---|---|
__slots__ |
属性名を固定化し、インスタンスの軽量化および属性検査に活用 |
__setattr__ |
属性変更をフックし、ログ出力+型検証+保存処理を統合 |
__getattr__ |
存在しない属性アクセス時のログ記録と例外送出 |
super() |
LoggerMixin → DataEntity の継承構造における親クラスの呼び出し |
isinstance() |
型安全性チェック(ヒントは __annotations__ 経由) |
logging |
INFOレベルでログ出力。動的属性アクセスログや変更履歴の追跡に活用 |
@property(未使用) |
__slots__ と __setattr__ を併用することで十分な制御を実現 |
datetime.now() |
保存タイミングの記録に使用 |
Q.53
| 項目 | 内容 |
|---|---|
| クラス名 | MultiKeySortable |
| 使用構文 |
@total_ordering, __eq__, __lt__, @property, operator.attrgetter, functools.cmp_to_key, classmethod, ValueError
|
| 問題文 | 複数のソートキーを持ち、順序変更も可能な比較クラス MultiKeySortable を定義せよ。全インスタンスに対して動的に優先ソートキーを変更でき、 __eq__/__lt__ の比較動作もキー順に従って行われるようにせよ。未定義属性をキーに指定した場合は例外を送出すること。 |
A.53
■ 模範解答
from functools import total_ordering
from operator import attrgetter
from typing import Any, ClassVar
@total_ordering
class MultiKeySortable:
# クラス変数: ソートに使用する属性のリスト(初期値)
_sort_keys: ClassVar[list[str]] = ["priority", "timestamp"]
def __init__(self, name: str, priority: int, timestamp: float):
self.name = name
self.priority = priority
self.timestamp = timestamp
@classmethod
def set_sort_keys(cls, keys: list[str]) -> None:
"""ソート優先順を設定。存在しない属性を指定した場合は例外。"""
for key in keys:
if not hasattr(cls, key):
raise ValueError(f"'{key}' は存在しない属性です")
cls._sort_keys = keys
def _comparison_tuple(self) -> tuple:
"""現在のソートキー順に従って比較対象となる値のタプルを返す"""
return tuple(getattr(self, key) for key in self._sort_keys)
def __eq__(self, other: Any) -> bool:
if not isinstance(other, MultiKeySortable):
return NotImplemented
return self._comparison_tuple() == other._comparison_tuple()
def __lt__(self, other: Any) -> bool:
if not isinstance(other, MultiKeySortable):
return NotImplemented
return self._comparison_tuple() < other._comparison_tuple()
def __repr__(self):
return f"{self.__class__.__name__}(name={self.name!r}, priority={self.priority}, timestamp={self.timestamp})"
objs = [
MultiKeySortable("TaskA", priority=2, timestamp=100.5),
MultiKeySortable("TaskB", priority=1, timestamp=101.1),
MultiKeySortable("TaskC", priority=1, timestamp=99.9),
]
sorted_objs = sorted(objs)
print(sorted_objs)
[MultiKeySortable(name='TaskC', priority=1, timestamp=99.9),
MultiKeySortable(name='TaskB', priority=1, timestamp=101.1),
MultiKeySortable(name='TaskA', priority=2, timestamp=100.5)]
entity = DataEntity("Bob", 40)
entity.age = "forty"
[MultiKeySortable(name='TaskC', priority=1, timestamp=99.9),
MultiKeySortable(name='TaskA', priority=2, timestamp=100.5),
MultiKeySortable(name='TaskB', priority=1, timestamp=101.1)]
■ 文法・構文まとめ
| 構文・機能 | 説明 |
|---|---|
@total_ordering |
__eq__ と __lt__ から他の比較演算子を自動生成 |
@classmethod |
クラス全体のソートキーを外部から設定できるメソッド |
getattr |
動的に属性値を取得するための組み込み関数 |
tuple(...) 比較 |
優先キー順に比較を行うために、属性値をタプルにまとめて比較 |
__eq__, __lt__
|
オブジェクトの等価性および大小比較のために明示的に定義 |
ClassVar(typing) |
sort_keys をインスタンスではなくクラス変数として型注釈 |
ValueError |
存在しない属性をソートキーに指定した場合の防御的設計 |
Q.54
| 項目 | 内容 |
|---|---|
| クラス名 | VersionedDataObject |
| 概要 | 一意なID(UUID)と version 属性を持つデータオブジェクト。変更は update() 経由でのみ可能で、変更時は version を自動インクリメント。 |
| 使用構文 |
__hash__, __eq__, @property, __dict__, copy.deepcopy, uuid, json, property.setter, classmethod, @staticmethod
|
| 特記事項 | - __hash__ は不変情報(uuid)を元に計算- __eq__ は uuid ベースで比較- serialize() で JSON 化- update() により安全な更新と version 管理 |
一意な UUID、version 属性、データ辞書を持つクラス VersionedDataObject を定義せよ。更新は update(data: dict) メソッドを通じてのみ行い、そのたびに version をインクリメントすること。オブジェクトは __hash__ によりハッシュ可能であり、UUID に基づいて等価比較が可能であること。また、JSON 文字列に変換可能な serialize() メソッドも実装せよ。 |
A.54
■ 模範解答
import uuid
import json
import copy
from typing import Any
class VersionedDataObject:
def __init__(self, data: dict[str, Any]):
# 一意なIDを生成
self._uuid = uuid.uuid4()
# データ内容はディープコピーで保持(外部参照を遮断)
self._data = copy.deepcopy(data)
self._version = 1 # 初期バージョン
@property
def uuid(self) -> str:
# UUIDを外部公開(読み取り専用)
return str(self._uuid)
@property
def version(self) -> int:
return self._version
@property
def data(self) -> dict[str, Any]:
# 外部に渡す際もコピーを返して不変性を保持
return copy.deepcopy(self._data)
def update(self, new_data: dict[str, Any]) -> None:
# 入力はディクショナリのみ許可
if not isinstance(new_data, dict):
raise TypeError("update() expects a dict")
# データを更新し、バージョンを1つ進める
self._data.update(new_data)
self._version += 1
def serialize(self) -> str:
# UUID, version, data を JSON 文字列に変換
return json.dumps({
"uuid": self.uuid,
"version": self.version,
"data": self._data
})
def __hash__(self) -> int:
# UUID を基にハッシュ可能にする
return hash(self._uuid)
def __eq__(self, other: Any) -> bool:
# UUID が一致すれば等価とする
if not isinstance(other, VersionedDataObject):
return NotImplemented
return self._uuid == other._uuid
def __repr__(self) -> str:
return (f"{self.__class__.__name__}(uuid={self.uuid}, "
f"version={self.version}, data={self._data})")
obj = VersionedDataObject({'name': 'Alice', 'age': 30})
print(obj) # 初期状態
obj.update({'age': 31}) # 更新
print(obj.version) # 2
print(obj.data) # {'name': 'Alice', 'age': 31}
entity = DataEntity("Bob", 40)
entity.age = "forty"
■ 文法・構文まとめ
| 使用構文 | 説明 |
|---|---|
__hash__ |
UUID を基にオブジェクトをハッシュ可能に |
__eq__ |
UUID を基にオブジェクトの等価性を判断 |
@property |
uuid, version, data を読み取り専用属性として公開 |
copy.deepcopy |
外部からの変更を防ぐため、内部データを複製 |
uuid.uuid4() |
各インスタンスを一意に識別するための ID 生成 |
json.dumps() |
シリアライズ形式(辞書)を JSON 文字列に変換 |
__repr__ |
開発者向けのデバッグ出力に役立つ形式で情報を出力 |
例外処理 (TypeError) |
update() に無効な型を渡した場合に明示的にエラーを発生させる |
Q.55
| 項目 | 内容 |
|---|---|
| クラス名 | Vector |
| 概要 | 数値ベクトルの演算クラス。+, -, *, ==, len, getitem, contains, abs に対応し、スカラー/ベクトル両対応演算・内積・ノルムなども実装。 |
| 使用構文 |
__add__, __sub__, __mul__, __eq__, __getitem__, __contains__, __len__, __abs__, zip, enumerate, isinstance, math.sqrt, round
|
数値ベクトルを表す Vector クラスを定義せよ。2つのベクトル間での加算・減算・乗算(内積)、およびスカラーとの加減乗算も可能とすること。また、ベクトルの長さ len()、要素アクセス、メンバシップ演算、ノルム計算(abs())をサポートせよ。等価比較はすべての要素の一致に基づき、浮動小数点誤差を許容すること。
A.55
■ 模範解答
import math
from typing import Union, Iterable
class Vector:
def __init__(self, values: Iterable[float]):
# 入力が数値のリストであることをチェック
self._data = [float(v) for v in values]
def __len__(self) -> int:
# ベクトルの次元(要素数)を返す
return len(self._data)
def __getitem__(self, index: int) -> float:
# 添字アクセスを許可
return self._data[index]
def __contains__(self, value: float) -> bool:
# 値がベクトル内に存在するかを確認(浮動小数点誤差を考慮)
return any(math.isclose(v, value, rel_tol=1e-9) for v in self._data)
def __add__(self, other: Union['Vector', float]) -> 'Vector':
# ベクトル同士の加算 または スカラー加算
if isinstance(other, Vector):
if len(self) != len(other):
raise ValueError("Vector dimensions must match for addition")
return Vector(a + b for a, b in zip(self._data, other._data))
elif isinstance(other, (int, float)):
return Vector(v + other for v in self._data)
return NotImplemented
def __sub__(self, other: Union['Vector', float]) -> 'Vector':
# ベクトル同士 または スカラーとの減算
if isinstance(other, Vector):
if len(self) != len(other):
raise ValueError("Vector dimensions must match for subtraction")
return Vector(a - b for a, b in zip(self._data, other._data))
elif isinstance(other, (int, float)):
return Vector(v - other for v in self._data)
return NotImplemented
def __mul__(self, other: Union['Vector', float]) -> Union['Vector', float]:
# スカラー乗算 または ベクトル間の内積
if isinstance(other, Vector):
if len(self) != len(other):
raise ValueError("Vector dimensions must match for dot product")
return sum(a * b for a, b in zip(self._data, other._data)) # 内積を返す
elif isinstance(other, (int, float)):
return Vector(v * other for v in self._data) # スカラー乗算
return NotImplemented
def __eq__(self, other: object) -> bool:
# 要素ごとの一致を浮動小数点誤差を許容して比較
if not isinstance(other, Vector) or len(self) != len(other):
return False
return all(math.isclose(a, b, rel_tol=1e-9) for a, b in zip(self._data, other._data))
def __abs__(self) -> float:
# ユークリッドノルム(2-norm)を返す
return math.sqrt(sum(v ** 2 for v in self._data))
def __repr__(self) -> str:
# デバッグ・表示用の文字列表現
return f"Vector({[round(v, 4) for v in self._data]})"
obj = VersionedDataObject({'name': 'Alice', 'age': 30})
print(obj) # 初期状態
obj.update({'age': 31}) # 更新
print(obj.version) # 2
print(obj.data) # {'name': 'Alice', 'age': 31}
entity = DataEntity("Bob", 40)
entity.age = "forty"
■ 文法・構文まとめ
| 使用構文 | 解説 |
|---|---|
__add__, __sub__
|
ベクトル同士の要素ごとの演算、およびスカラーとの演算に対応 |
__mul__ |
ベクトルとの内積/スカラーとの各要素乗算を両対応 |
__getitem__ |
添字アクセス(例: v[1])をサポート |
__contains__ |
in 演算子で含まれるかチェック(誤差許容) |
__eq__ |
ベクトル同士の誤差付き等価性比較 |
__len__ |
ベクトルの次元数を返す |
__abs__ |
ユークリッドノルム(ベクトルの大きさ)を返す |
enumerate, zip
|
ベクトル要素の並列処理・インデックス操作 |
isinstance |
動的型検査により多態的な演算子対応を可能に |
math.isclose |
浮動小数点演算での誤差許容 |
Q.56
| 項目 | 内容 |
|---|---|
| クラス名 | AdvancedContainer |
| 概要 | 任意要素を保持するコンテナ。スライス/インデックス取得、map/filter対応、ジェネレータ式対応、イミュータブル化オプション付き。 |
| 使用構文 |
__getitem__, __iter__, __len__, slice, map, filter, @property, isinstance, collections.abc.Sequence, frozen設計 |
| 発展仕様 | - スライス・インデックスアクセス - map/filter適用メソッド - イミュータブル化スイッチ - 部分取得の再帰対応 - __repr__あり |
AdvancedContainer クラスを定義せよ。このクラスは任意のデータ列を保持し、以下の要件を満たすこと:
・インデックスアクセス/スライスに対応( __getitem__ )
len(), iter() 対応
map(func) や filter(func) による関数適用
frozen=True のとき、内容変更は禁止(属性書き換え不可)
__repr__ で内容を簡潔に表示
スライス取得は再帰的に AdvancedContainer を返す
A.56
■ 模範解答
from typing import Iterable, Callable, Iterator, Any, Union
import copy
class AdvancedContainer:
def __init__(self, data: Iterable, frozen: bool = False):
# データはリストとして内部保持
self._data = list(data)
# frozen によって変更可能性を制御
self._frozen = frozen
def __len__(self) -> int:
# 要素数の取得
return len(self._data)
def __iter__(self) -> Iterator:
# イテレータ対応
return iter(self._data)
def __getitem__(self, index: Union[int, slice]) -> Union[Any, 'AdvancedContainer']:
# インデックスまたはスライスの取得に対応
if isinstance(index, slice):
# スライスは新しい AdvancedContainer として返す
return AdvancedContainer(self._data[index], frozen=self._frozen)
return self._data[index]
def map(self, func: Callable) -> 'AdvancedContainer':
# map 適用。結果は新しいコンテナにする
return AdvancedContainer(map(func, self._data), frozen=self._frozen)
def filter(self, predicate: Callable) -> 'AdvancedContainer':
# filter 適用。条件に合う要素だけを保持
return AdvancedContainer(filter(predicate, self._data), frozen=self._frozen)
@property
def frozen(self) -> bool:
# frozen 状態の参照用プロパティ
return self._frozen
def __setattr__(self, key, value):
# frozen のときは属性変更禁止(初期化除く)
if hasattr(self, '_frozen') and self._frozen and key not in {'_frozen', '_data'}:
raise AttributeError(f"Cannot modify attribute '{key}' in frozen container")
# 通常の属性設定
object.__setattr__(self, key, value)
def __repr__(self) -> str:
# 表示用:最初の数個だけ省略形式で表示
preview = self._data[:5]
return f"AdvancedContainer({preview}{'...' if len(self._data) > 5 else ''}, frozen={self._frozen})"
ac = AdvancedContainer(range(10))
print(ac[2]) # 2(インデックス)
print(ac[3:7]) # AdvancedContainer([3, 4, 5, 6], frozen=False)
print(ac.map(lambda x: x * 2)) # AdvancedContainer([0, 2, 4, 6, 8]..., frozen=False)
print(ac.filter(lambda x: x % 2 == 0)) # AdvancedContainer([0, 2, 4, 6, 8], frozen=False)
frozen_ac = AdvancedContainer([1, 2, 3], frozen=True)
try:
frozen_ac.new_attr = 999 # エラー: frozen状態では属性追加不可
except AttributeError as e:
print(f"Error: {e}")
print(frozen_ac[0]) # 1
print(frozen_ac.map(lambda x: x + 1)) # AdvancedContainer([2, 3, 4], frozen=True)
■ 文法・構文まとめ
| 使用構文 | 解説 |
|---|---|
__getitem__ |
インデックス/スライス対応を定義 |
__iter__, __len__
|
イテラブルコンテナとして振る舞うための必須構文 |
map, filter
|
関数型操作(遅延評価を明示せず eager に即評価) |
@property |
frozen属性の保護された参照 |
__setattr__ |
属性変更禁止ロジック(frozen対応) |
__repr__ |
可読性の高い省略形式表示 |
list, slice, Union
|
内部表現・型対応を柔軟に処理 |
Q.57
| 項目 | 内容 |
|---|---|
| クラス名 |
BaseFormatter, UpperCaseFormatter, StripFormatter, SuffixFormatter, FormatterChain
|
| 概要 |
format(value) を持つ複数の Formatter をチェーンで連結し、逐次適用。__call__ でも呼び出せる。クロージャやラムダもサポート。 |
| 発展仕様 | - FormatterChain.add() で動的に連結- 各フォーマッタは format(value) を持つ- __call__ でチェーン全体が適用可能- lambdaや関数も追加可能 |
| 使用構文 | 抽象クラス, 継承, ポリモーフィズム, __call__, super(), lambda, リスト内包, クロージャ的保持, 型チェック, Callable
|
文字列整形器を合成的に構築する FormatterChain クラスを定義せよ。
各フォーマッタは format(self, value: str) -> str を実装するクラスとする(例:空白除去・大文字化など)。
FormatterChain は複数のフォーマッタを順番に保持し、call で逐次適用する。
フォーマッタにはラムダ関数・クロージャも許容し、lambda v: v[::-1] のような関数型も連結可能とする。** **add(formatter) でチェーンに追加可能とし、str -> str を満たす限り任意の関数/オブジェクトを受け付ける。** **repr()` はチェーン構成を可視化せよ。**
A.57
■ 模範解答
from typing import Callable, Union
# 基底クラス: すべてのFormatterが継承する
class BaseFormatter:
def format(self, value: str) -> str:
raise NotImplementedError("Must implement format() method")
def __call__(self, value: str) -> str:
# 呼び出し可能にする(関数風)
return self.format(value)
# 具体的Formatter 1: 文字列を大文字化
class UpperCaseFormatter(BaseFormatter):
def format(self, value: str) -> str:
return value.upper()
# 具体的Formatter 2: 両端の空白を除去
class StripFormatter(BaseFormatter):
def format(self, value: str) -> str:
return value.strip()
# 具体的Formatter 3: サフィックスを追加
class SuffixFormatter(BaseFormatter):
def __init__(self, suffix: str):
self.suffix = suffix
def format(self, value: str) -> str:
return value + self.suffix
# チェーンの管理クラス
class FormatterChain:
def __init__(self):
# フォーマッタのリスト(クラス or 関数)
self._formatters: list[Callable[[str], str]] = []
def add(self, formatter: Union[BaseFormatter, Callable[[str], str]]):
# Callable[str -> str] であることを保証
if not callable(formatter):
raise TypeError("Formatter must be callable (format(value: str) -> str)")
self._formatters.append(formatter)
def __call__(self, value: str) -> str:
# 全てのフォーマッタを順に適用
for f in self._formatters:
value = f(value)
return value
def __repr__(self):
# フォーマッタの一覧表示
names = [f.__class__.__name__ if hasattr(f, '__class__') else f.__name__ for f in self._formatters]
return f"FormatterChain({names})"
fc = FormatterChain()
fc.add(StripFormatter()) # 余白除去
fc.add(UpperCaseFormatter()) # 大文字化
fc.add(SuffixFormatter("!")) # サフィックス追加
print(fc(" hello world ")) # 出力: "HELLO WORLD!"
print(repr(fc)) # 出力: FormatterChain(['StripFormatter', 'UpperCaseFormatter', 'SuffixFormatter'])
fc2 = FormatterChain()
fc2.add(lambda v: v[::-1]) # 文字列を反転
fc2.add(lambda v: f"[{v}]") # ブラケットで囲む
print(fc2(" Python ")) # 出力: [ nohtyP ]
print(repr(fc2)) # 出力: FormatterChain(['<lambda>', '<lambda>'])
■ 文法・構文まとめ
| 使用構文 | 解説 |
|---|---|
__call__ |
オブジェクトを関数のように呼び出せるようにする (f(x) の形式) |
| 継承 + ポリモーフィズム |
BaseFormatter を基底にして統一インターフェースを確保 |
lambda, 関数型 |
lambda や任意の Callable を受け付ける柔軟設計 |
super() |
継承構造の拡張・スーパークラス呼び出し(必要に応じて) |
__repr__ |
チェーン構成の可視化 |
| クロージャ |
lambda 内部で状態を保持することで応用可能(例:クロージャで接尾辞管理) |
Q.58
| 項目 | 内容 |
|---|---|
| 概要 |
Serializable 抽象基底クラスを定義し、to_dict()・to_json() を提供せよ。動的属性取得、ネスト処理、循環参照対策、無視属性指定、関数属性除外に対応。 |
| 追加要件 | - ネストされた Serializable も再帰処理で辞書化- 循環参照検出( id() によるトラッキング)- callable 属性は除外 - exclude_fields による属性除外 |
| 使用構文 |
abc.ABC, @abstractmethod, __dict__, json.dumps, hasattr, callable, isinstance, super(), setattr, @property, try/except
|
| 発展仕様 | - _exclude_fields_ クラス属性により除外対象を定義可能- repr() にも to_json() を使用し表示を美しく定義- 非公開属性(先頭 _)もオプションで含める |
| 例外設計 | - 非シリアライズ可能な型に出会った場合に警告を出す(TypeError) |
Serializable という抽象基底クラスを設計せよ。このクラスは以下を満たすこと:
・to_dict() メソッドで自分自身を辞書化できる。
to_json() メソッドでJSON文字列に変換できる。
・自身の属性( ___dict____ )を再帰的に辞書化する。ただし関数属性は除外する。
・循環参照がある場合は "__circular__" として表現する。
・exclude_fields 引数で除外したい属性を指定できる。
・_exclude_fields_ というクラス属性であらかじめ除外属性を指定しておける。
・repr() は to_json() の出力を用いて定義すること。
・非公開属性( _name など)もオプションで含められるようにすること。
A.58
■ 模範解答
import json
from abc import ABC, abstractmethod
from typing import Any
class Serializable(ABC):
_exclude_fields_: set[str] = set() # デフォルトで除外するフィールド
def to_dict(self, exclude_fields=None, include_private=False, _seen=None) -> dict:
# 循環参照防止のために、すでに見たオブジェクトのIDを記録
if _seen is None:
_seen = set()
result = {}
exclude_fields = set(exclude_fields or [])
all_excludes = exclude_fields.union(getattr(self, "_exclude_fields_", set()))
# オブジェクトの ID を記録して循環参照を検出
obj_id = id(self)
if obj_id in _seen:
return "__circular__"
_seen.add(obj_id)
# 属性辞書の取得
for attr, value in self.__dict__.items():
if not include_private and attr.startswith("_"):
continue # 非公開属性を除外(オプションで含める)
if attr in all_excludes:
continue # 除外リストにある属性はスキップ
if callable(value):
continue # 関数属性は除外
try:
if isinstance(value, Serializable):
result[attr] = value.to_dict(exclude_fields, include_private, _seen)
elif isinstance(value, (list, tuple, set)):
result[attr] = [
v.to_dict(exclude_fields, include_private, _seen)
if isinstance(v, Serializable) else v
for v in value
]
elif isinstance(value, dict):
result[attr] = {
k: v.to_dict(exclude_fields, include_private, _seen)
if isinstance(v, Serializable) else v
for k, v in value.items()
}
else:
result[attr] = value
except Exception as e:
# 変換に失敗した場合は警告文字列を入れる
result[attr] = f"__unserializable__: {e}"
_seen.remove(obj_id)
return result
def to_json(self, **kwargs) -> str:
try:
return json.dumps(self.to_dict(), ensure_ascii=False, **kwargs)
except TypeError as e:
raise TypeError(f"Serialization failed: {e}") from e
def __repr__(self):
try:
return self.to_json(indent=2)
except Exception:
return f"<{self.__class__.__name__} (unserializable)>"
@abstractmethod
def validate(self) -> None:
"""継承クラスで必ず定義させるバリデーション用メソッド"""
raise NotImplementedError
class User(Serializable):
_exclude_fields_ = {"password"}
def __init__(self, name, age, password):
self.name = name
self.age = age
self.password = password
self._internal_id = "hidden"
def validate(self):
if not self.name:
raise ValueError("Name is required")
u = User("Alice", 30, "secret123")
print(u.to_dict())
# 出力例:
# {'name': 'Alice', 'age': 30}
print(u.to_dict(include_private=True))
# 出力例(非公開属性あり):
# {'name': 'Alice', 'age': 30, '_internal_id': 'hidden'}
class Post(Serializable):
def __init__(self, title, author=None):
self.title = title
self.author = author
def validate(self): pass
user = User("Bob", 25, "xxx")
post = Post("Hello", user)
user.post = post # 循環参照を形成
print(user.to_dict())
# 出力例(循環参照が __circular__ になる):
# {'name': 'Bob', 'age': 25, 'post': {'title': 'Hello', 'author': '__circular__'}}
■ 文法・構文まとめ
| 文法・構文 | 用途・意味 |
|---|---|
abc.ABC, @abstractmethod
|
継承クラスで必須メソッド (validate) を強制 |
__dict__ |
オブジェクトの内部属性を辞書として動的に取得 |
json.dumps |
PythonオブジェクトをJSON文字列に変換 |
hasattr, callable
|
属性の有無確認・関数属性の除外 |
super() |
抽象クラスの継承構文での利用 |
@property |
クラス設計時の属性制御(今回はオプション) |
try/except |
辞書化やシリアライズ失敗に対する安全なハンドリング |
id(self) と set()
|
循環参照のトラッキングに使うIDセット |
isinstance() |
型チェックで再帰呼び出しを適用する際に使用 |
__repr__ |
print時の出力を JSON にカスタマイズ |
setattr, クラス属性 |
除外属性の継承的制御 (_exclude_fields_) |
Q.59
| 項目 | 内容 |
|---|---|
| 概要 | 四則演算式を構文木で表現する演算ノードクラス群を定義せよ。各ノードは evaluate() によって数値に評価でき、文字列化(__str__)で中置記法として出力される。 |
| 問題文 | 演算式を木構造で表現するために、以下の仕様を満たすクラス階層を設計せよ: 1. Expression という抽象基底クラスを定義し、evaluate() メソッドを抽象化する。2. Value クラスは単一の数値(float/int)を保持し、それを評価結果として返す。3. BinaryOperation 抽象クラスは左右の Expression を持ち、評価時にそれらを評価して計算を行う。4. Add, Sub, Mul, Div などの具体的な演算クラスを実装する。5. 各クラスは __str__() をオーバーライドし、中置記法の文字列(例:(3 + 4))を返すようにする。6. Expression は __add__, __sub__, __mul__, __truediv__ を定義し、構文木の合成をオペレータで行えるようにする。7. from_nested_tuple(cls, expr) を実装し、再帰的に (op, left, right) の形のタプルから構文木を構成可能にする。8. DivisionByZeroError を定義し、ゼロ除算に対して適切な例外処理を行うこと。9. __repr__ は __str__ をそのまま返す形で統一的に記述する。10. 計算結果は float で統一されるものとする。 |
| 使用構文 |
abc.ABC, @abstractmethod, @classmethod, __add__, __mul__, __truediv__, __repr__, __str__, isinstance, super(), try/except, 再帰構文, クラス継承構造 |
| 発展仕様 | - クロージャ的に from_nested_tuple() で構文木を再帰的に構成可能にする- Expression 自体が演算子オーバーロードを持つことで演算式の合成が直感的に行える- 各演算ノードは再帰的な文字列変換と評価に対応 |
| 例外設計 | - ゼロ除算時に DivisionByZeroError を発生させる- タプル→構文木変換時に不正な形式があれば ValueError を発生させる |
A.59
■ 模範解答
from abc import ABC, abstractmethod
# カスタム例外:ゼロ除算用
class DivisionByZeroError(ZeroDivisionError):
pass
# 抽象基底クラス:すべての式のベース
class Expression(ABC):
@abstractmethod
def evaluate(self) -> float:
pass
def __add__(self, other):
return Add(self, other)
def __sub__(self, other):
return Sub(self, other)
def __mul__(self, other):
return Mul(self, other)
def __truediv__(self, other):
return Div(self, other)
def __repr__(self):
return str(self)
# 数値ノード:リーフノード
class Value(Expression):
def __init__(self, value: float):
self.value = float(value) # floatで統一
def evaluate(self) -> float:
return self.value
def __str__(self):
return str(self.value)
# 演算ノード:共通の2項演算をまとめた抽象クラス
class BinaryOperation(Expression):
def __init__(self, left: Expression, right: Expression):
self.left = left
self.right = right
@abstractmethod
def operator_symbol(self) -> str:
pass
@abstractmethod
def operate(self, left_val: float, right_val: float) -> float:
pass
def evaluate(self) -> float:
left_val = self.left.evaluate()
right_val = self.right.evaluate()
try:
return self.operate(left_val, right_val)
except ZeroDivisionError:
raise DivisionByZeroError("Attempted division by zero.")
def __str__(self):
return f"({self.left} {self.operator_symbol()} {self.right})"
# 各演算子の具象クラス定義
class Add(BinaryOperation):
def operator_symbol(self) -> str:
return "+"
def operate(self, left_val, right_val):
return left_val + right_val
class Sub(BinaryOperation):
def operator_symbol(self) -> str:
return "-"
def operate(self, left_val, right_val):
return left_val - right_val
class Mul(BinaryOperation):
def operator_symbol(self) -> str:
return "*"
def operate(self, left_val, right_val):
return left_val * right_val
class Div(BinaryOperation):
def operator_symbol(self) -> str:
return "/"
def operate(self, left_val, right_val):
if right_val == 0:
raise DivisionByZeroError("Division by zero.")
return left_val / right_val
# タプルから構文木を再帰構築するクラスメソッド
class ExpressionBuilder:
operator_map = {
'+': Add,
'-': Sub,
'*': Mul,
'/': Div,
}
@classmethod
def from_nested_tuple(cls, expr) -> Expression:
if isinstance(expr, (int, float)):
return Value(expr)
if isinstance(expr, tuple) and len(expr) == 3:
op, left, right = expr
if op not in cls.operator_map:
raise ValueError(f"Unknown operator: {op}")
return cls.operator_map[op](
cls.from_nested_tuple(left),
cls.from_nested_tuple(right)
)
raise ValueError(f"Invalid expression format: {expr}")
expr = Value(3) + Value(4) * Value(2)
print(expr) # => (3.0 + (4.0 * 2.0))
print(expr.evaluate()) # => 11.0
expr2 = ExpressionBuilder.from_nested_tuple(('/', 10, ('-', 5, 5)))
print(expr2) # => (10.0 / (5.0 - 5.0))
try:
print(expr2.evaluate())
except DivisionByZeroError as e:
print(f"Error: {e}") # => Error: Attempted division by zero.
■ 文法・構文まとめ
| 要素 | 解説 |
|---|---|
abc.ABC, @abstractmethod
|
式全体を抽象階層で統一し、各クラスの責務を明確化 |
__add__, __sub__ 等 |
演算子オーバーロードにより expr1 + expr2 のような直感的構文を可能に |
__str__, __repr__
|
再帰的に中置記法表現を生成し、printでも構文木が人間に読みやすく表現される |
@classmethod + 再帰構文 |
(op, left, right) のネスト構造からツリー構築を自動化し、クロージャ的な動的構成を可能に |
try/except, カスタム例外 |
安全な演算と堅牢なゼロ除算処理 |
| クラス継承による演算種別分離 | 1つのクラスにまとめず、各演算ごとに継承クラスを用意することで OCP(開放/閉鎖原則)に準拠 |
Q.60
| 項目 | 内容 |
|---|---|
| 概要 | 任意のオブジェクトをラップし、すべてのメソッド呼び出し・属性アクセス・例外を自動でログ記録する監査用プロキシ AuditProxy を設計せよ。 |
| 問題文 |
AuditProxy クラスを設計せよ。このクラスは任意のオブジェクトをラップし、以下を満たす:1. オブジェクトの属性アクセス/更新をすべて __getattr__, __setattr__ で監視し、ログに記録する。2. ラップ対象が callable な場合、 __call__ を通じて呼び出し可能とし、ログ・例外も記録する。3. メソッド呼び出しも監視可能とする。 @wraps を活用して透明性を保持せよ。4. logging モジュールを用いて、操作内容・引数・例外を INFO/ERROR レベルで記録する。5. プロキシ自身の内部属性(例:ログ設定、ターゲット参照など)は対象外とするために注意深く設計せよ(無限再帰防止)。 6. 任意の関数またはオブジェクト(関数オブジェクト、クラスインスタンスなど)に対応すること。 7. __repr__ によって、監査対象の型情報と操作対象の概要を出力可能にすること。8. ラップ対象の型が __call__ を持たない場合に __call__ を無効化する設計も検討せよ。 |
| 使用構文 |
__getattr__, __setattr__, __call__, functools.wraps, logging, try/except, isinstance, callable, type, デリゲーション、内部属性保護 |
| 発展仕様 | - AuditProxy 自身が callable をラップした場合のみ __call__ を有効化(TypeError ガード含む)- 内部属性は _proxy_ で始めることで保護- ログ記録は logger 名を指定可能にして柔軟性を向上 |
| 例外設計 | - 呼び出し・アクセス時の例外はすべて捕捉し、エラー内容をログ出力した上で再送出する |
A.60
■ 模範解答
import logging
from functools import wraps
class AuditProxy:
def __init__(self, target, logger_name="AuditProxy"):
# 内部用の属性は _proxy_ で始めて __getattr__ や __setattr__ の無限再帰を防ぐ
object.__setattr__(self, "_proxy_target", target)
object.__setattr__(self, "_proxy_logger", logging.getLogger(logger_name))
def __getattr__(self, name):
target = object.__getattribute__(self, "_proxy_target")
logger = object.__getattribute__(self, "_proxy_logger")
try:
attr = getattr(target, name) # ラップ対象から属性取得
logger.info(f"GETATTR: {name} -> {type(attr)}")
if callable(attr):
# 関数・メソッドをさらにラップしてログ記録機能を注入
@wraps(attr)
def wrapped(*args, **kwargs):
logger.info(f"CALL METHOD: {name}({args}, {kwargs})")
try:
result = attr(*args, **kwargs)
logger.info(f"RESULT: {name} -> {result}")
return result
except Exception as e:
logger.error(f"EXCEPTION in method '{name}': {e}")
raise
return wrapped
else:
return attr
except AttributeError as e:
logger.error(f"GETATTR FAILED: {name} - {e}")
raise
def __setattr__(self, name, value):
if name.startswith("_proxy_"):
# プロキシ内部属性はそのまま代入
object.__setattr__(self, name, value)
else:
logger = object.__getattribute__(self, "_proxy_logger")
logger.info(f"SETATTR: {name} = {value}")
setattr(object.__getattribute__(self, "_proxy_target"), name, value)
def __call__(self, *args, **kwargs):
target = object.__getattribute__(self, "_proxy_target")
logger = object.__getattribute__(self, "_proxy_logger")
if not callable(target):
raise TypeError(f"Target object of type {type(target).__name__} is not callable")
logger.info(f"CALL: {target}({args}, {kwargs})")
try:
result = target(*args, **kwargs)
logger.info(f"RESULT: {result}")
return result
except Exception as e:
logger.error(f"EXCEPTION in __call__: {e}")
raise
def __repr__(self):
target = object.__getattribute__(self, "_proxy_target")
return f"<AuditProxy of {type(target).__name__}>"
import logging
# ログ出力設定
logging.basicConfig(level=logging.INFO, format="%(message)s")
# 対象クラス
class Calculator:
def __init__(self):
self.history = []
def add(self, x, y):
result = x + y
self.history.append(result)
return result
calc = Calculator()
proxy = AuditProxy(calc)
# メソッド呼び出しと属性アクセス
print(proxy.add(3, 5)) # => 8
print(proxy.history) # => [8]
proxy.history = [] # 属性更新もログ対象
GETATTR: add -> <class 'method'>
CALL METHOD: add((3, 5), {})
RESULT: add -> 8
GETATTR: history -> <class 'list'>
SETATTR: history = []
def greet(name):
return f"Hello, {name}!"
proxy_func = AuditProxy(greet)
print(proxy_func("Alice")) # => "Hello, Alice!"
CALL: <function greet at 0x...>(('Alice',), {})
RESULT: Hello, Alice!
■ 文法・構文まとめ
| 文法・構文 | 説明 |
|---|---|
__getattr__ |
属性アクセスの補足。存在しない属性アクセスをキャッチし、ラップ+ログ処理を行う。 |
__setattr__ |
属性代入の補足。監査対象の属性更新をログ記録し、デリゲーションを行う。 |
__call__ |
callable なオブジェクトの呼び出しを監視・ログ記録する。 |
@wraps(functools) |
ラップした関数に元の関数の名前や docstring を保持させ、透明性を確保。 |
logging |
操作ログを INFO/ERROR レベルで出力する標準モジュール。 |
try/except |
呼び出し時の例外をログ記録し、必要に応じて再送出する。 |
isinstance, callable
|
関数かどうかの判定・柔軟なオブジェクトハンドリングに使用。 |
| 委譲(デリゲーション) | 操作自体は元のオブジェクトに転送し、Proxyは監査に専念する。 |
| 内部属性保護 |
_proxy_ プレフィックスで無限再帰回避、Proxyの自己保存属性と区別。 |
Q.61
| 項目 | 内容 |
|---|---|
| 概要 | 辞書同士の合成・差分比較・履歴記録・バージョン管理を備えた MergeableDict クラスを設計せよ。合成は + 演算子で行い、履歴は自動で記録される。 |
| 問題文 |
MergeableDict クラスを定義せよ。このクラスは dict を拡張し、以下の高度な機能を提供すること:1. + 演算子で他の辞書(または MergeableDict)と合成可能であり、キーが競合した場合は右辺の値で上書きされる。2. 合成のたびに履歴(操作時刻・操作種別・差分情報)を自動で記録する。 3. == による比較は通常の辞書のように動作するが、差分情報(追加/更新/削除キー)も取得可能とする。4. version(n) により過去バージョンを復元可能にする(ただし shallow copy でよい)。5. merge_log プロパティで履歴を参照できるようにし、各項目にタイムスタンプ・差分が含まれる。6. setdefault, update, __getitem__ などの辞書標準機能を維持しつつ、履歴記録に対応させる。7. copy() で内容と履歴を独立して複製可能にする。8. 不正な合成相手(辞書以外)に対しては例外を投げること。 |
| 使用構文 |
__add__, __eq__, __getitem__, update, setdefault, copy, datetime, deepcopy, @property, dict.items(), 差分計算ロジック、例外設計 |
| 発展仕様 | - merge_log にタイムスタンプ、変更点、旧値・新値を記録- version(n) によるバージョン復元(イミュータブル化はしなくてよい)- 差分比較関数 diff(other) 実装 |
| 例外設計 | - + に非辞書を渡した場合 TypeError を発生- バージョン番号が範囲外の場合 IndexError を発生 |
A.61
■ 模範解答
from datetime import datetime
from copy import deepcopy
class MergeableDict(dict):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._versions = [deepcopy(dict(self))] # バージョン履歴: 初期状態を保存
self._log = [] # マージ履歴: 各操作の差分・時刻を記録
def __add__(self, other):
if not isinstance(other, dict):
raise TypeError("Only dictionaries can be merged with MergeableDict.")
new = MergeableDict(self)
diff = {}
for key, val in other.items():
if key not in new or new[key] != val:
diff[key] = (new.get(key), val)
new[key] = val
new._versions = deepcopy(self._versions + [deepcopy(dict(new))])
new._log = deepcopy(self._log + [{
"timestamp": datetime.now(),
"operation": "merge",
"diff": diff
}])
return new
def __eq__(self, other):
if not isinstance(other, dict):
return False
return dict(self) == dict(other)
def diff(self, other):
if not isinstance(other, dict):
raise TypeError("Can only compare with another dictionary.")
added = {k: other[k] for k in other if k not in self}
removed = {k: self[k] for k in self if k not in other}
changed = {k: (self[k], other[k]) for k in self if k in other and self[k] != other[k]}
return {"added": added, "removed": removed, "changed": changed}
def version(self, n: int):
if not (0 <= n < len(self._versions)):
raise IndexError(f"Version index {n} out of range")
return MergeableDict(self._versions[n])
@property
def merge_log(self):
return self._log
def update(self, *args, **kwargs):
other = dict(*args, **kwargs)
diff = {}
for k, v in other.items():
if k not in self or self[k] != v:
diff[k] = (self.get(k), v)
super().update(other)
self._versions.append(deepcopy(dict(self)))
self._log.append({
"timestamp": datetime.now(),
"operation": "update",
"diff": diff
})
def setdefault(self, key, default=None):
if key not in self:
self._log.append({
"timestamp": datetime.now(),
"operation": "setdefault",
"diff": {key: (None, default)}
})
self._versions.append(deepcopy(dict(self)))
return super().setdefault(key, default)
def copy(self):
new = MergeableDict(self)
new._versions = deepcopy(self._versions)
new._log = deepcopy(self._log)
return new
def __getitem__(self, key):
try:
return super().__getitem__(key)
except KeyError as e:
raise KeyError(f"Key '{key}' not found in MergeableDict") from e
def __repr__(self):
return f"MergeableDict({super().__repr__()})"
d1 = MergeableDict({'a': 1, 'b': 2})
d2 = {'b': 3, 'c': 4}
d3 = d1 + d2
print(d3) # => MergeableDict({'a': 1, 'b': 3, 'c': 4})
print(d3.merge_log)
# => [{'timestamp': ..., 'operation': 'merge', 'diff': {'b': (2, 3), 'c': (None, 4)}}]
print(d3.version(0)) # => MergeableDict({'a': 1, 'b': 2})
d = MergeableDict({'x': 10})
d.update({'y': 20})
d.setdefault('z', 99)
print(d) # => MergeableDict({'x': 10, 'y': 20, 'z': 99})
print(d.merge_log[-2]['operation']) # => 'update'
print(d.merge_log[-1]['operation']) # => 'setdefault'
d2 = {'x': 10, 'y': 21, 'a': 1}
print(d.diff(d2))
# => {'added': {'a': 1}, 'removed': {'z': 99}, 'changed': {'y': (20, 21)}}
■ 文法・構文まとめ
| 構文/技法 | 説明 |
|---|---|
__add__ |
他の辞書との合成をサポートし、履歴を記録。 |
__eq__ |
通常の辞書比較に加えて、差分も取得可能に設計(diff())。 |
update, setdefault
|
辞書APIを拡張して操作履歴も残す。 |
deepcopy, version()
|
過去の状態を復元できるようバージョン管理(履歴付き shallow copy)。 |
datetime.now() |
操作タイムスタンプ記録。 |
@property |
merge_log を読み取り専用のプロパティとして提供。 |
__getitem__ |
標準辞書操作と同様だが、詳細なエラーメッセージを追加。 |
copy() |
オブジェクトと履歴を完全に複製可能に設計。 |
| 例外処理 |
TypeError(不正な合成)、IndexError(バージョン番号不正)、KeyError に対応。 |
Q.62
| 項目 | 内容 |
|---|---|
| 概要 |
MergeableDict クラスを定義せよ。このクラスは辞書のように動作しつつ、合成(+)、差分比較、更新履歴保持、バージョン復元、構造比較など高度な辞書拡張を提供する。 |
| 問題文 |
MergeableDict クラスは以下の要件を満たす必要がある:1. + 演算子で dict または MergeableDict と合成でき、競合時は右辺優先で上書きされる。2. 合成時には差分(新規追加・変更)を記録し、履歴(操作名・タイムスタンプ・差分)に残す。 3. diff(other) メソッドにより他辞書との構造差分(追加・削除・変更)を取得できる。4. version(n) メソッドにより過去の状態を MergeableDict として復元できる。5. merge_log プロパティは履歴リストを返す(読み取り専用)。6. update() / setdefault() も履歴に記録される。7. copy() により値と履歴を複製可能である。8. __getitem__ はキーが存在しないとき例外を投げるが、詳細な情報を与える。9. __eq__ は dict 互換比較が可能で、比較対象が不適切なら False を返す。10. 合成対象が辞書でない場合は TypeError を発生させ、version(n) の範囲外アクセスでは IndexError を投げる。 |
| 使用構文 |
__add__, __eq__, __getitem__, update, setdefault, copy, datetime, deepcopy, @property, 差分取得、履歴構造化、例外制御 |
| 発展仕様 | - ログ項目に timestamp, operation, diff, snapshot(状態)を含める。- __repr__ は内容+バージョン番号付きで出力。- 内部状態は _md_ 接頭辞で保護 |
A.62
■ 模範解答
from datetime import datetime
from copy import deepcopy
class MergeableDict(dict):
def __init__(self, *args, **kwargs):
# 通常の辞書初期化
super().__init__(*args, **kwargs)
# 履歴(マージログ)と状態バージョンを保持
self._md_versions = [deepcopy(dict(self))] # 初期バージョン
self._md_log = [] # 操作履歴ログ
def __add__(self, other):
if not isinstance(other, dict):
raise TypeError("MergeableDict can only be added to dict-like objects")
new_dict = MergeableDict(self)
diff = {}
for k, v in other.items():
# 差分計算:新規 or 値が変化した場合のみ記録
if k not in new_dict or new_dict[k] != v:
diff[k] = (new_dict.get(k), v)
new_dict[k] = v
new_dict._md_versions = deepcopy(self._md_versions + [deepcopy(dict(new_dict))])
new_dict._md_log = deepcopy(self._md_log + [{
"timestamp": datetime.now(),
"operation": "merge",
"diff": diff,
"snapshot": dict(new_dict)
}])
return new_dict
def update(self, *args, **kwargs):
update_dict = dict(*args, **kwargs)
diff = {}
for k, v in update_dict.items():
if k not in self or self[k] != v:
diff[k] = (self.get(k), v)
super().update(update_dict)
self._md_versions.append(deepcopy(dict(self)))
self._md_log.append({
"timestamp": datetime.now(),
"operation": "update",
"diff": diff,
"snapshot": dict(self)
})
def setdefault(self, key, default=None):
if key not in self:
self._md_log.append({
"timestamp": datetime.now(),
"operation": "setdefault",
"diff": {key: (None, default)},
"snapshot": dict(self)
})
self._md_versions.append(deepcopy(dict(self)))
return super().setdefault(key, default)
def __getitem__(self, key):
try:
return super().__getitem__(key)
except KeyError as e:
raise KeyError(f"Key '{key}' not found in MergeableDict (keys: {list(self.keys())})") from e
def __eq__(self, other):
if not isinstance(other, dict):
return False
return dict(self) == dict(other)
def diff(self, other):
if not isinstance(other, dict):
raise TypeError("diff() comparison target must be dict-like")
added = {k: other[k] for k in other if k not in self}
removed = {k: self[k] for k in self if k not in other}
changed = {k: (self[k], other[k]) for k in self if k in other and self[k] != other[k]}
return {"added": added, "removed": removed, "changed": changed}
def version(self, n: int):
if not (0 <= n < len(self._md_versions)):
raise IndexError(f"Version index {n} out of range (0–{len(self._md_versions)-1})")
return MergeableDict(self._md_versions[n])
def copy(self):
new = MergeableDict(self)
new._md_versions = deepcopy(self._md_versions)
new._md_log = deepcopy(self._md_log)
return new
@property
def merge_log(self):
return deepcopy(self._md_log)
def __repr__(self):
return f"MergeableDict({super().__repr__()}, version={len(self._md_versions)-1})"
d1 = MergeableDict({'x': 1, 'y': 2})
d2 = {'y': 3, 'z': 9}
d3 = d1 + d2
print(d3) # => MergeableDict({'x': 1, 'y': 3, 'z': 9}, version=1)
print(d3.version(0)) # => MergeableDict({'x': 1, 'y': 2}, version=0)
print(d3.merge_log)
[{
'timestamp': ...,
'operation': 'merge',
'diff': {'y': (2, 3), 'z': (None, 9)},
'snapshot': {'x': 1, 'y': 3, 'z': 9}
}]
d = MergeableDict({'x': 10})
d.update({'y': 20})
d.setdefault('z', 99)
print(d) # => MergeableDict({'x': 10, 'y': 20, 'z': 99})
print(d.merge_log[-2]['operation']) # => 'update'
print(d.merge_log[-1]['operation']) # => 'setdefault'
d2 = {'x': 10, 'y': 21, 'a': 1}
print(d.diff(d2))
# => {'added': {'a': 1}, 'removed': {'z': 99}, 'changed': {'y': (20, 21)}}
{
'added': {'d': 5},
'removed': {'c': 30},
'changed': {'b': (20, 21)}
}
■ 文法・構文まとめ
| 使用構文 | 解説 |
|---|---|
__add__ |
+ による辞書合成と履歴追加(immutable風) |
__eq__ |
dict 同士の等価比較(型が異なれば False) |
__getitem__ |
詳細なエラー出力付きアクセス |
update, setdefault
|
内部操作として変更差分記録・バージョン追加 |
deepcopy, copy()
|
バージョンと履歴の独立複製 |
diff() |
差分計算(追加・削除・変更キー) |
@property |
merge_log を外部読み取り専用にする |
datetime.now() |
ログタイムスタンプ |
IndexError, TypeError
|
明示的な例外設計(不正バージョン番号や型の検査) |
Q.63
| 項目 | 内容 |
|---|---|
| 概要 |
Command パターンを用いて、コマンドオブジェクトを定義せよ。各コマンドは execute() と undo() を持ち、コマンド履歴のスタックにより、連続操作/取り消し/再実行が可能な環境を構成する。 |
| 問題文 | 以下の仕様に従って、汎用的な「実行・取り消し機能付きコマンド管理システム」を構築せよ: 1. Command 抽象基底クラスを定義し、execute()・undo() を必ず実装させる。2. コマンドは __call__ により呼び出し可能であること(__call__ → execute())3. コマンド実行履歴は deque に記録され、undo() は LIFO 方式で履歴から最後のコマンドを取り出して逆操作を行う。4. 任意の対象(数値・オブジェクト・コレクションなど)に対して状態変更する SetValueCommand や AppendItemCommand などを定義せよ。5. Undo 不可なコマンドは NotUndoableError を明示的に投げること。6. CommandManager クラスを設計し、コマンドの実行・履歴管理・undo 操作の一元管理を行う。7. execute_all(), undo_all() などのバッチ操作機能も備える。8. 各操作のログを保持可能とし、ログ表示機能 show_history() を提供せよ。 |
| 使用構文 |
abc.ABC, @abstractmethod, __call__, deque, append, pop, undo, @property, isinstance, super(), TypeError, 例外制御、Command パターン |
| 発展仕様 | - コマンドの一括実行と一括ロールバックに対応 - Command オブジェクトは str 表現で操作ログ用の概要を持つ(__str__ 実装)- 状態変更は「外部 mutable オブジェクト」への操作を前提とする |
| 例外設計 | - NotUndoableError を独自実装し、undo 未対応コマンドではこの例外を raise- undo() 呼び出し時に履歴が空なら IndexError を raise |
A.63
■ 模範解答
from abc import ABC, abstractmethod
from collections import deque
# Undo 未対応コマンドの例外
class NotUndoableError(Exception):
pass
# コマンド基底クラス
class Command(ABC):
@abstractmethod
def execute(self):
pass
@abstractmethod
def undo(self):
pass
def __call__(self):
return self.execute()
@abstractmethod
def __str__(self):
pass
# 対象オブジェクトの属性値を設定するコマンド
class SetValueCommand(Command):
def __init__(self, target, attr, value):
self.target = target
self.attr = attr
self.value = value
self._previous = None
def execute(self):
# 変更前の値を記録しておく
self._previous = getattr(self.target, self.attr, None)
setattr(self.target, self.attr, self.value)
def undo(self):
# 取り消し時に以前の値へ戻す
setattr(self.target, self.attr, self._previous)
def __str__(self):
return f"Set {self.attr} = {self.value}"
# リストにアイテムを追加するコマンド
class AppendItemCommand(Command):
def __init__(self, target_list, item):
self.list = target_list
self.item = item
def execute(self):
self.list.append(self.item)
def undo(self):
if self.list and self.list[-1] == self.item:
self.list.pop()
else:
raise NotUndoableError("Cannot undo: item not at the end.")
def __str__(self):
return f"Append {self.item}"
# Undo 不可なコマンドの例
class PrintCommand(Command):
def __init__(self, message):
self.message = message
def execute(self):
print(self.message)
def undo(self):
raise NotUndoableError("PrintCommand cannot be undone.")
def __str__(self):
return f"Print '{self.message}'"
# コマンド履歴・実行管理クラス
class CommandManager:
def __init__(self):
self._history = deque() # コマンド履歴を保持
def do(self, command: Command):
command()
self._history.append(command)
def undo(self):
if not self._history:
raise IndexError("No command to undo.")
command = self._history.pop()
try:
command.undo()
except NotUndoableError as e:
print(f"[UNDO ERROR] {e}")
def execute_all(self, commands):
for cmd in commands:
self.do(cmd)
def undo_all(self):
while self._history:
self.undo()
def show_history(self):
print("Command History:")
for i, cmd in enumerate(self._history):
print(f"{i + 1}: {cmd}")
class Dummy:
def __init__(self):
self.value = 0
obj = Dummy()
manager = CommandManager()
# コマンド作成と実行
cmd1 = SetValueCommand(obj, 'value', 42)
manager.do(cmd1)
print(obj.value) # => 42
manager.undo()
print(obj.value) # => 0
log = []
manager = CommandManager()
cmds = [
AppendItemCommand(log, "start"),
PrintCommand("This is a side effect"),
AppendItemCommand(log, "end"),
]
manager.execute_all(cmds)
print(log) # => ['start', 'end']
manager.undo_all()
print(log) # => []
manager.show_history() # => PrintCommand は undo に失敗し履歴から除去されない
■ 文法・構文まとめ
| 使用構文 | 説明 |
|---|---|
abc.ABC, @abstractmethod
|
コマンドのインターフェース定義により共通仕様を強制 |
__call__ |
コマンドオブジェクトを関数のように呼び出せる(command() 形式) |
deque, append, pop
|
スタック構造でコマンド履歴を管理(LIFO) |
NotUndoableError |
取り消し不可なコマンドを明示的に扱う |
execute_all, undo_all
|
複数コマンドの一括実行・一括取り消し |
__str__, show_history
|
ログ表示を人間にわかりやすく構成 |
try/except |
Undo失敗を安全に扱い、実行を継続可能にする |
| 状態保存 |
SetValueCommand は以前の状態を _previous に保存して戻せる |