0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Python文法100本ノック vol.6 ~継承・ポリモーフィズム・特殊メソッド~

0
Last updated at Posted at 2025-07-31

まえがき

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}
実行例1:オブジェクト比較と出力
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}
実行例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}>"
実行例1:正常な属性変更
entity = DataEntity("Alice", 30)
entity.age = 31
実行結果1
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] データ保存完了
実行例2:型が間違っている場合
entity = DataEntity("Bob", 40)
entity.age = "forty"
実行結果2
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() LoggerMixinDataEntity の継承構造における親クラスの呼び出し
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})"
実行例1:デフォルトソート(priority → 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)
実行結果1
[MultiKeySortable(name='TaskC', priority=1, timestamp=99.9),
 MultiKeySortable(name='TaskB', priority=1, timestamp=101.1),
 MultiKeySortable(name='TaskA', priority=2, timestamp=100.5)]
実行例2:ソートキー変更(timestamp のみ)
entity = DataEntity("Bob", 40)
entity.age = "forty"
実行結果2
[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})")
実行例1:初期化と更新・バージョン確認
obj = VersionedDataObject({'name': 'Alice', 'age': 30})
print(obj)  # 初期状態

obj.update({'age': 31})  # 更新
print(obj.version)       # 2
print(obj.data)          # {'name': 'Alice', 'age': 31}
実行例2:等価性・ハッシュ性・シリアライズ確認
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]})"
実行例1:ベクトル同士の演算
obj = VersionedDataObject({'name': 'Alice', 'age': 30})
print(obj)  # 初期状態

obj.update({'age': 31})  # 更新
print(obj.version)       # 2
print(obj.data)          # {'name': 'Alice', 'age': 31}
実行例2:スカラー演算とノルム・等価比較
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})"
実行例1:ベクトル同士の演算
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)
実行例2:イミュータブル化とエラー処理
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})"
実行例1:ベクトル同士の演算
fc = FormatterChain()
fc.add(StripFormatter())                 # 余白除去
fc.add(UpperCaseFormatter())            # 大文字化
fc.add(SuffixFormatter("!"))            # サフィックス追加

print(fc("   hello world   "))  # 出力: "HELLO WORLD!"
print(repr(fc))                 # 出力: FormatterChain(['StripFormatter', 'UpperCaseFormatter', 'SuffixFormatter'])
実行例2:イミュータブル化とエラー処理
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
実行例1:基本的なネストと除外属性付き
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")
実行結果1
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'}
実行例2:ネスト + 循環参照の検出
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}")
実行例1:式の合成・評価・表示
expr = Value(3) + Value(4) * Value(2)
print(expr)            # => (3.0 + (4.0 * 2.0))
print(expr.evaluate()) # => 11.0
実行例2:タプルから構文木を構築 → ゼロ除算例外処理
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__}>"
実行例1:クラスの監査
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 = []            # 属性更新もログ対象
実行結果1
GETATTR: add -> <class 'method'>
CALL METHOD: add((3, 5), {})
RESULT: add -> 8
GETATTR: history -> <class 'list'>
SETATTR: history = []
実行例2:関数オブジェクトのラップ
def greet(name):
    return f"Hello, {name}!"

proxy_func = AuditProxy(greet)

print(proxy_func("Alice"))  # => "Hello, Alice!"
実行結果1
CALL: <function greet at 0x...>(('Alice',), {})
RESULT: Hello, Alice!

■ 文法・構文まとめ

文法・構文 説明
__getattr__ 属性アクセスの補足。存在しない属性アクセスをキャッチし、ラップ+ログ処理を行う。
__setattr__ 属性代入の補足。監査対象の属性更新をログ記録し、デリゲーションを行う。
__call__ callable なオブジェクトの呼び出しを監視・ログ記録する。
@wrapsfunctools ラップした関数に元の関数の名前や 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__()})"
実行例1:基本的な合成とバージョン確認
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})
実行例2:update・setdefault の監査と diff
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})"
実行例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)
実行結果1
[{
    'timestamp': ..., 
    'operation': 'merge',
    'diff': {'y': (2, 3), 'z': (None, 9)}, 
    'snapshot': {'x': 1, 'y': 3, 'z': 9}
}]
実行例2:update・setdefault の監査と diff
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)}}
実行結果2
{
    '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. 任意の対象(数値・オブジェクト・コレクションなど)に対して状態変更する SetValueCommandAppendItemCommand などを定義せよ。
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}")
実行例1:属性変更と取り消し
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
実行例2:リスト追加と PrintCommand
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 に保存して戻せる
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?