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.5 ~基本クラス設計と属性操作~

Posted at

まえがき

Python100本ノックについての記事です。既存の100本ノックは幾分簡単すぎるようにも感じており、それに対するアンサー記事となります。誤りなどがあれば、ご指摘ください。今回は本番編として、基本クラス設計と属性操作を中心に10問扱います。

Q.41

氏名・メール・役割・アクティブ状態を持つ AdvancedUserProfile クラスを定義せよ。メール更新は正規表現で検証し、履歴に記録すること。履歴は最大10件まで保持せよ。等価性の比較もサポートせよ。

項目 内容
クラス名 AdvancedUserProfile
引数 name: str, email: str, role: str, active: bool = True
出力 str 表現・等価性の比較・メール更新履歴の管理を含むインスタンス
発展仕様 ・メールは正規表現でバリデーション
・メール更新履歴(最大10件)を datetime 付きで保持
@property@setter によるカプセル化
__str__, __repr__, __eq__ 実装
・履歴表示メソッド email_history()
使用構文 class, __init__, @property, @<property>.setter, re.fullmatch, datetime, list, __str__, __repr__, __eq__, raise, zip, スライス, f-string

A.41

■ 模範解答

import re
from datetime import datetime

class AdvancedUserProfile:
    def __init__(self, name: str, email: str, role: str, active: bool = True):
        self._name = name                             # 氏名(変更不可とする)
        self._role = role                             # 役割(読み取り専用)
        self._active = active                         # アクティブ状態
        self._email_history = []                      # 更新履歴(最大10件)
        self.email = email                            # メールはsetter経由で検証される

    @property
    def name(self) -> str:
        return self._name

    @property
    def role(self) -> str:
        return self._role

    @property
    def active(self) -> bool:
        return self._active

    @active.setter
    def active(self, value: bool):
        if not isinstance(value, bool):
            raise TypeError("active は bool 型でなければなりません")
        self._active = value

    @property
    def email(self) -> str:
        return self._email

    @email.setter
    def email(self, new_email: str):
        # 正規表現でメールアドレス形式を検証
        if not re.fullmatch(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}", new_email):
            raise ValueError("不正なメールアドレス形式です")

        # 履歴に追加(最大10件)
        timestamp = datetime.now().isoformat(timespec='seconds')
        if hasattr(self, '_email'):
            self._email_history.append((self._email, timestamp))
            self._email_history = self._email_history[-10:]  # 最新10件に制限

        self._email = new_email  # 正常なメールとして更新

    def email_history(self) -> list[tuple[str, str]]:
        """(旧メールアドレス, 更新日時) の履歴リストを返す"""
        return self._email_history.copy()

    def __eq__(self, other) -> bool:
        if not isinstance(other, AdvancedUserProfile):
            return NotImplemented
        return (self.name, self.email, self.role, self.active) == \
               (other.name, other.email, other.role, other.active)

    def __str__(self) -> str:
        return f"{self.name} ({self.email}) - Role: {self.role}, Active: {self.active}"

    def __repr__(self) -> str:
        return (f"{self.__class__.__name__}(name={self.name!r}, email={self.email!r}, "
                f"role={self.role!r}, active={self.active!r})")
実行例1:基本使用(整数除算)
user = AdvancedUserProfile("Alice", "alice@example.com", "admin")
print(user)  # => Alice (alice@example.com) - Role: admin, Active: True

user.email = "alice.new@example.org"  # 正常更新
print(user.email_history())
# => [('alice@example.com', '2025-07-27T23:10:45')]
実行例2:不正メール形式と比較操作
try:
    user.email = "invalid-email"  # => ValueError: 不正なメールアドレス形式です
except ValueError as e:
    print("例外:", e)

user2 = AdvancedUserProfile("Alice", "alice.new@example.org", "admin")
print(user == user2)  # => True(属性が同じなら等価)

■ 文法・構文まとめ

構文 説明
@property email, name など読み取り専用属性の実装に使用
@<property>.setter email, active に対する更新の制御付きプロパティ
re.fullmatch() 正規表現でメールアドレスのバリデーション
datetime.now().isoformat() タイムスタンプとして履歴に記録(秒精度)
list[-10:] スライスで最大10件の履歴に制限
__eq__ 等価比較のカスタマイズ(値オブジェクトとして扱うため)
__str__, __repr__ ユーザーフレンドリーな出力とデバッグ向け文字列の整備

Q.42

ValidatedRectangle クラスを定義し、幅と高さは正数に制限すること。面積・周囲長をプロパティで取得可能にし、repr 表現では四捨五入で出力せよ。

項目 内容
クラス名 ValidatedRectangle
引数 width: float, height: float
出力 area(面積), perimeter(周囲長)のプロパティを持ち、__repr__ は四捨五入済で返す
発展仕様 - 幅・高さは正の浮動小数点数のみ許容(0や負数は例外)
- 面積・周囲長は @property で取得
- math.isclose() で誤差比較サポート(__eq__ 実装)
- __repr__ では小数点第2位までで四捨五入出力
使用構文 @property, @<prop>.setter, math.isclose, round(), __repr__, __eq__, ValueError, isinstance

A.42

■ 模範解答

import math

class ValidatedRectangle:
    def __init__(self, width: float, height: float):
        self.width = width   # setter 経由で検証
        self.height = height # setter 経由で検証

    @property
    def width(self) -> float:
        return self._width

    @width.setter
    def width(self, value: float):
        # 数値型かつ正の値でなければ例外を送出
        if not isinstance(value, (int, float)):
            raise TypeError("width must be a number")
        if value <= 0:
            raise ValueError("width must be positive")
        self._width = float(value)

    @property
    def height(self) -> float:
        return self._height

    @height.setter
    def height(self, value: float):
        if not isinstance(value, (int, float)):
            raise TypeError("height must be a number")
        if value <= 0:
            raise ValueError("height must be positive")
        self._height = float(value)

    @property
    def area(self) -> float:
        return self.width * self.height  # 面積 = 幅 × 高さ

    @property
    def perimeter(self) -> float:
        return 2 * (self.width + self.height)  # 周囲長 = 2 × (幅 + 高さ)

    def __eq__(self, other) -> bool:
        # 浮動小数点誤差を考慮した比較(math.isclose 使用)
        if not isinstance(other, ValidatedRectangle):
            return NotImplemented
        return (
            math.isclose(self.width, other.width, rel_tol=1e-9) and
            math.isclose(self.height, other.height, rel_tol=1e-9)
        )

    def __repr__(self) -> str:
        # 小数点第2位まで四捨五入して表示
        return (f"ValidatedRectangle("
                f"width={round(self.width, 2)}, "
                f"height={round(self.height, 2)})")
実行例1:正常動作とプロパティアクセス
r = ValidatedRectangle(4.0, 5.0)
print(r.area)         # => 20.0
print(r.perimeter)    # => 18.0
print(repr(r))        # => ValidatedRectangle(width=4.0, height=5.0)
実行例2:エラー処理と等価比較
try:
    r = ValidatedRectangle(-3, 5)
except ValueError as e:
    print("例外:", e)  # => width must be positive

r1 = ValidatedRectangle(3.00000001, 4.0)
r2 = ValidatedRectangle(3.0, 4.0)
print(r1 == r2)  # => True(浮動小数点誤差を許容)

■ 文法・構文まとめ

構文 用途
@property 面積・周囲長・属性取得を読み取り専用にする(area, perimeter
@<property>.setter 属性変更を安全に制御(width, height
math.isclose(a, b) 浮動小数点誤差を含む等価性検証に用いる (__eq__)
round(value, ndigits) 表示用の小数点丸め(__repr__ 用)
__eq__, __repr__ 等価性の判定、およびインスタンスの文字列表現を明確に
ValueError, TypeError バリデーションによる厳密な入力制御
float() int などの入力も強制的に float に変換

Q.43

TemperatureConverterV2 を定義し、celsius, fahrenheit, kelvin を双方向で変換できるようにせよ。内部的にはケルビンで一貫して管理すること。

項目 内容
クラス名 TemperatureConverterV2
属性 内部的に _kelvin: float のみ保持
出力 celsius, fahrenheit, kelvin の双方向プロパティ
発展仕様 - 内部状態は常にケルビン
- 負の絶対温度は例外
- 小数点第2位で丸め
- __repr__ で全単位表示
- @property + @setter で完全双方向変換
使用構文 @property, @<prop>.setter, round, __repr__, isinstance, ValueError, float

A.43

■ 模範解答

class TemperatureConverterV2:
    def __init__(self, kelvin: float):
        self.kelvin = kelvin  # kelvin setter 経由で検証と設定

    @staticmethod
    def _validate_temperature(value: float, scale: str = "kelvin"):
        # 数値かどうかの検証
        if not isinstance(value, (int, float)):
            raise TypeError(f"{scale} must be a number")
        # ケルビンは絶対温度:0未満は禁止
        if scale == "kelvin" and value < 0:
            raise ValueError("Kelvin temperature cannot be negative")

    @property
    def kelvin(self) -> float:
        return self._kelvin

    @kelvin.setter
    def kelvin(self, value: float):
        self._validate_temperature(value, "kelvin")
        self._kelvin = float(value)

    @property
    def celsius(self) -> float:
        # ケルビン → セルシウス
        return round(self.kelvin - 273.15, 2)

    @celsius.setter
    def celsius(self, value: float):
        # セルシウス → ケルビン
        self._validate_temperature(value + 273.15, "kelvin")
        self._kelvin = float(value + 273.15)

    @property
    def fahrenheit(self) -> float:
        # ケルビン → 華氏
        return round((self.kelvin - 273.15) * 9 / 5 + 32, 2)

    @fahrenheit.setter
    def fahrenheit(self, value: float):
        # 華氏 → ケルビン
        c = (value - 32) * 5 / 9
        k = c + 273.15
        self._validate_temperature(k, "kelvin")
        self._kelvin = float(k)

    def __repr__(self) -> str:
        # 全単位を文字列で表示
        return (f"TemperatureConverterV2("
                f"C={self.celsius}°C, "
                f"F={self.fahrenheit}°F, "
                f"K={round(self.kelvin, 2)}K)")
実行例1:各単位からの双方向設定
t = TemperatureConverterV2(300)
print(t)  # => TemperatureConverterV2(C=26.85°C, F=80.33°F, K=300.0K)

t.celsius = 0
print(t.kelvin)     # => 273.15
print(t.fahrenheit) # => 32.0
実行例2:異常系の検出と例外発生
try:
    t = TemperatureConverterV2(-5)  # ケルビンに負は不可
except ValueError as e:
    print("例外:", e)  # => Kelvin temperature cannot be negative

try:
    t = TemperatureConverterV2(273.15)
    t.fahrenheit = -1000  # 絶対温度下回る変換
except ValueError as e:
    print("例外:", e)  # => Kelvin temperature cannot be negative

■ 文法・構文まとめ

構文 説明
@property, @setter 双方向変換用の温度プロパティを提供
round(value, 2) 小数点第2位での丸め(表示用)
__repr__ インスタンスの全単位を人間に読める形式で出力
@staticmethod 検証用のヘルパー関数をクラスの外部依存なしで設計
float, isinstance 型強制と安全性の保証
ValueError, TypeError 異常な温度値(例:負のケルビンや非数値)への堅牢な例外制御

Q.44

認証付きの銀行口座クラス SecureBankAccount を実装せよ。出金時にはパスワード認証が必要であり、操作ログはタイムスタンプ付きで保持すること。

項目 内容
クラス名 SecureBankAccount
属性 _balance: float, _password_hash: str, _log: list[str], _owner: str
メソッド deposit(amount), withdraw(amount, password), verify_password(pw), log_operation(msg)
発展仕様 - 出金時にパスワード照合
- SHA256ハッシュによるパスワード管理
- ログは内部リストに保持(時刻付き)
- プロパティによる残高取得
- __str__で状態出力
使用構文 __str__, @property, raise, ValueError, datetime, hashlib, list, str.format, f-string

A.44

■ 模範解答

import hashlib
from datetime import datetime

class SecureBankAccount:
    def __init__(self, owner: str, initial_balance: float, password: str):
        self._owner = owner
        self._balance = float(initial_balance)  # 初期残高はfloat型で統一
        self._password_hash = self._hash(password)  # パスワードはハッシュで保存
        self._log = []  # 操作ログを保持

        self.log_operation("Account created")

    def _hash(self, password: str) -> str:
        # SHA256でパスワードをハッシュ化
        return hashlib.sha256(password.encode()).hexdigest()

    def verify_password(self, password: str) -> bool:
        # 入力パスワードのハッシュと照合
        return self._hash(password) == self._password_hash

    def log_operation(self, message: str):
        # タイムスタンプ付きでログを追加
        timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        self._log.append(f"[{timestamp}] {message}")

    def deposit(self, amount: float):
        if amount <= 0:
            raise ValueError("Deposit amount must be positive")
        self._balance += amount
        self.log_operation(f"Deposited: {amount}")

    def withdraw(self, amount: float, password: str):
        if not self.verify_password(password):
            self.log_operation("Failed withdrawal attempt: incorrect password")
            raise ValueError("Authentication failed")

        if amount <= 0:
            raise ValueError("Withdrawal amount must be positive")
        if amount > self._balance:
            raise ValueError("Insufficient funds")

        self._balance -= amount
        self.log_operation(f"Withdrew: {amount}")

    @property
    def balance(self) -> float:
        return self._balance

    def __str__(self):
        return f"SecureBankAccount(owner='{self._owner}', balance={self._balance:.2f})"

    def show_logs(self) -> str:
        # ログを一括で表示
        return "\n".join(self._log)
実行例1:入金・出金成功とログ出力
acc = SecureBankAccount("Alice", 1000, "secure123")
acc.deposit(500)
acc.withdraw(300, "secure123")

print(acc)  # => SecureBankAccount(owner='Alice', balance=1200.00)
print(acc.show_logs())
実行結果1
SecureBankAccount(owner='Alice', balance=1200.00)
[2025-07-27 22:30:00] Account created
[2025-07-27 22:30:01] Deposited: 500
[2025-07-27 22:30:02] Withdrew: 300
実行例2:認証失敗 → 例外+ログ記録
acc = SecureBankAccount("Bob", 500, "mypw")
try:
    acc.withdraw(100, "wrongpw")
except ValueError as e:
    print("認証失敗:", e)

print(acc.balance)        # => 500.0
print(acc.show_logs())    # ログに失敗が記録されている
実行結果2
認証失敗: Authentication failed
[2025-07-27 22:35:00] Account created
[2025-07-27 22:35:01] Failed withdrawal attempt: incorrect password

■ 文法・構文まとめ

構文 用途
@property balance取得をgetterで定義
__str__ インスタンス情報の可読性向上
ValueError 不正な金額・認証失敗に対する例外処理
hashlib.sha256 パスワードをセキュアに保存
datetime.now() ログの時刻記録
f-string, str.format 出力整形、ログ整形に使用
list[str] 操作ログを内部で記録

Q.45

各カウンターごとに max_limit を設定し、increment(name) で上限を超えたら例外を送出するクラスを定義せよ。カウンターの存在検査や辞書的アクセスも可能にすること。

項目 内容
クラス名 TaggedCounterCollection
属性 _counters: dict[str, dict](構造:{"tag": {"count": int, "limit": int}}
メソッド add_counter(tag, limit), increment(tag), __getitem__, __contains__
発展仕様 - 各カウンターに上限 (max_limit) を指定
- 上限超過で LimitExceededError 例外を送出
- in 句で存在確認
- obj[tag] 形式の取得
- __repr__ による概要表示
使用構文 dict, @property, custom Exception, __getitem__, __contains__, __repr__, f-string, raise

A.45

■ 模範解答

class LimitExceededError(Exception):
    """カウンターが上限を超えたときに送出される例外"""
    def __init__(self, tag: str, limit: int):
        super().__init__(f"Limit exceeded for tag '{tag}': max allowed is {limit}")

class TaggedCounterCollection:
    def __init__(self):
        self._counters: dict[str, dict] = {}  # 各タグに対応する {"count": int, "limit": int}

    def add_counter(self, tag: str, max_limit: int):
        # 新しいカウンターを登録。既存のタグは再登録不可とする。
        if tag in self._counters:
            raise ValueError(f"Counter '{tag}' already exists.")
        self._counters[tag] = {"count": 0, "limit": max_limit}

    def increment(self, tag: str) -> int:
        # カウントを1増加。上限を超えると例外を送出。
        if tag not in self._counters:
            raise KeyError(f"Counter '{tag}' does not exist.")
        
        counter = self._counters[tag]
        if counter["count"] >= counter["limit"]:
            raise LimitExceededError(tag, counter["limit"])
        
        counter["count"] += 1
        return counter["count"]

    def __getitem__(self, tag: str) -> int:
        # `obj[tag]` で現在のカウント値を取得
        if tag not in self._counters:
            raise KeyError(f"Counter '{tag}' does not exist.")
        return self._counters[tag]["count"]

    def __contains__(self, tag: str) -> bool:
        # `tag in obj` で存在確認可能にする
        return tag in self._counters

    def __repr__(self):
        # 全カウンターの概要を表示
        status = ", ".join(
            f"{tag}: {info['count']}/{info['limit']}"
            for tag, info in self._counters.items()
        )
        return f"<TaggedCounterCollection({status})>"
実行例1:正常にカウントアップし、表示
c = TaggedCounterCollection()
c.add_counter("clicks", 3)
c.add_counter("views", 5)

print(c.increment("clicks"))  # => 1
print(c.increment("clicks"))  # => 2
print(c["clicks"])            # => 2
print("views" in c)           # => True
print(c)                      # => <TaggedCounterCollection(clicks: 2/3, views: 0/5)>
実行例2:上限超過による例外処理
c = TaggedCounterCollection()
c.add_counter("downloads", 1)

c.increment("downloads")  # 1回目は成功

try:
    c.increment("downloads")  # 2回目はエラー
except LimitExceededError as e:
    print("例外発生:", e)
実行結果2
例外発生: Limit exceeded for tag 'downloads': max allowed is 1

■ 文法・構文まとめ

構文 用途
@property 使用していないが、拡張時に各属性へプロパティ化可能(例:全体使用率)
custom Exception LimitExceededError を定義して上限違反を表現
__getitem__ obj[tag] 形式で現在値取得を可能に
__contains__ tag in obj で存在確認
__repr__ インスタンスの概要を簡潔に表示
raise, f-string エラーメッセージやフォーマットに活用

Q.46

ImmutablePersonV2 を定義し、初期化後の属性変更を完全に禁止せよ。属性は __slots__ に限定し、ハッシュ可能とすること。変更時はカスタム例外を送出せよ。

項目 内容
クラス名 ImmutablePersonV2
属性 name: str, birth_year: int__slots__ 経由で定義)
発展仕様 - 属性は初期化後変更不可
- __slots__ により属性の制限とメモリ最適化
- __setattr__ を完全制御
- カスタム例外 ImmutableViolationError を送出
- ハッシュ可能にするため __hash__ 実装
使用構文 __slots__, __setattr__, @property, __eq__, __hash__, raise, Exception

A.46

■ 模範解答

class ImmutableViolationError(Exception):
    """不変オブジェクトへの変更が試みられたときに送出される例外"""
    def __init__(self, field):
        super().__init__(f"Cannot modify immutable field: '{field}'")

class ImmutablePersonV2:
    __slots__ = ('_name', '_birth_year', '_locked')  # 属性制限によりメモリ使用量削減+セキュリティ

    def __init__(self, name: str, birth_year: int):
        object.__setattr__(self, '_name', name)          # 最初のセットは __setattr__ をバイパス
        object.__setattr__(self, '_birth_year', birth_year)
        object.__setattr__(self, '_locked', True)        # 初期化終了後にロック

    def __setattr__(self, key, value):
        # 初期化後は全属性変更禁止
        if hasattr(self, '_locked') and self._locked:
            raise ImmutableViolationError(key)
        object.__setattr__(self, key, value)

    @property
    def name(self) -> str:
        return self._name

    @property
    def birth_year(self) -> int:
        return self._birth_year

    def __eq__(self, other):
        # 等価性比較: 同じ型で、同じ属性値を持つなら等しい
        if isinstance(other, ImmutablePersonV2):
            return (self._name, self._birth_year) == (other._name, other._birth_year)
        return False

    def __hash__(self):
        # ハッシュ化可能に(set や dict のキーで使用可能)
        return hash((self._name, self._birth_year))

    def __repr__(self):
        # 表示用
        return f"ImmutablePersonV2(name='{self._name}', birth_year={self._birth_year})"
実行例1:正常に初期化・取得・比較・repr
p1 = ImmutablePersonV2("Alice", 1990)
p2 = ImmutablePersonV2("Alice", 1990)

print(p1)                  # => ImmutablePersonV2(name='Alice', birth_year=1990)
print(p1.name)             # => Alice
print(p1 == p2)            # => True
print(set([p1, p2]))       # => ハッシュ可能で1要素になる
実行例2:属性変更の試行と例外捕捉
p = ImmutablePersonV2("Bob", 1985)

try:
    p.name = "Charlie"  # 変更を試みる → 例外
except ImmutableViolationError as e:
    print("例外:", e)

try:
    p._name = "Charlie"  # 直接の変更も禁止されている
except ImmutableViolationError as e:
    print("例外:", e)
実行結果2
例外: Cannot modify immutable field: 'name'
例外: Cannot modify immutable field: '_name'

■ 文法・構文まとめ

構文 役割
__slots__ 不正属性の追加防止・メモリ節約
__setattr__ 属性変更制御の中心ロジック。ロック後は例外を送出
@property 安全な getter を提供
__eq__, __hash__ 等価性比較とハッシュ化によって辞書キーやセット要素として使える
Exception 独自例外 ImmutableViolationError を定義

Q.47

商品をカテゴリ付きで登録・削除できる ProductCatalogV2 クラスを定義せよ。カテゴリ別平均価格、全体平均、価格帯別ヒストグラムを取得できるようにすること。

項目 内容
クラス名 ProductCatalogV2
属性 商品情報(カテゴリ・価格)を格納する内部構造(defaultdictベース)、読み取り専用ビュー、カテゴリ平均、価格帯分布を提供
発展仕様 - 商品の登録/削除
- カテゴリごとの平均価格取得
- 全体平均価格
- 価格帯ヒストグラム(rangeを指定可能)
- ディープコピー経由の読み取り専用ビュー
- 存在チェックと辞書的アクセスサポート
使用構文 defaultdict, Counter, @property, lambda, sum, len, copy.deepcopy, __getitem__, __contains__

A.47

■ 模範解答

from collections import defaultdict, Counter
from typing import Optional
import copy

class ProductCatalogV2:
    def __init__(self):
        # カテゴリごとに商品(名前: 価格)を辞書で保持
        self._catalog = defaultdict(dict)

    def add_product(self, name: str, price: float, category: str) -> None:
        # 商品をカテゴリ付きで登録。価格は非負の float を想定
        if price < 0:
            raise ValueError("Price must be non-negative.")
        self._catalog[category][name] = price

    def remove_product(self, name: str, category: str) -> bool:
        # 商品が存在すれば削除し、成功を返す。なければ False
        return self._catalog.get(category, {}).pop(name, None) is not None

    def get_product_price(self, name: str, category: str) -> Optional[float]:
        # 商品の価格を返す(存在しなければ None)
        return self._catalog.get(category, {}).get(name)

    def __contains__(self, name: str) -> bool:
        # 商品名がどこかのカテゴリに含まれているか
        return any(name in products for products in self._catalog.values())

    def __getitem__(self, name: str) -> float:
        # 商品名での辞書的アクセス(カテゴリ問わず最初に見つかったもの)
        for products in self._catalog.values():
            if name in products:
                return products[name]
        raise KeyError(f"Product '{name}' not found.")

    @property
    def categories(self) -> list[str]:
        # 登録されているカテゴリの一覧
        return list(self._catalog.keys())

    @property
    def all_products(self) -> dict:
        # 読み取り専用ビューを deepcopy により提供
        return copy.deepcopy({cat: dict(products) for cat, products in self._catalog.items()})

    @property
    def average_prices(self) -> dict[str, float]:
        # カテゴリ別平均価格
        return {
            cat: round(sum(products.values()) / len(products), 2)
            for cat, products in self._catalog.items() if products
        }

    @property
    def overall_average(self) -> float:
        # 全カテゴリの平均価格
        prices = [price for products in self._catalog.values() for price in products.values()]
        return round(sum(prices) / len(prices), 2) if prices else 0.0

    def price_histogram(self, bin_size: int = 100) -> dict[str, int]:
        # bin_size に従った価格帯別ヒストグラム
        counter = Counter()
        for products in self._catalog.values():
            for price in products.values():
                bin_label = f"{int(price//bin_size)*bin_size}-{int(price//bin_size)*bin_size + bin_size - 1}"
                counter[bin_label] += 1
        return dict(counter)

    def __repr__(self):
        # カタログの概要表示
        return f"<ProductCatalogV2: {len(self._catalog)} categories, {sum(len(v) for v in self._catalog.values())} products>"
実行例1:登録と各種集計の使用
catalog = ProductCatalogV2()
catalog.add_product("MacBook", 1800, "Electronics")
catalog.add_product("iPhone", 1000, "Electronics")
catalog.add_product("Desk", 300, "Furniture")
catalog.add_product("Chair", 150, "Furniture")

print(catalog.categories)
# ['Electronics', 'Furniture']

print(catalog.all_products)
# {'Electronics': {'MacBook': 1800, 'iPhone': 1000}, 'Furniture': {'Desk': 300, 'Chair': 150}}

print(catalog.average_prices)
# {'Electronics': 1400.0, 'Furniture': 225.0}

print(catalog.overall_average)
# 731.25

print(catalog.price_histogram(bin_size=500))
# {'1500-1999': 1, '1000-1499': 1, '0-499': 2}
実行例2:存在チェック・削除・例外処理
print("MacBook" in catalog)     # True
print("Camera" in catalog)      # False

print(catalog["Desk"])          # 300.0

try:
    print(catalog["Unknown"])   # KeyError
except KeyError as e:
    print("例外:", e)

catalog.remove_product("Chair", "Furniture")
print("Chair" in catalog)       # False

■ 文法・構文まとめ

構文 説明
defaultdict(dict) カテゴリごとに商品を辞書で保持
@property 読み取り専用ビュー(平均・全体平均・カテゴリ名・全商品など)を提供
Counter 価格帯ヒストグラムの実装に使用
copy.deepcopy 不変ビュー用に内部構造を安全に複製
__getitem__, __contains__ カタログを辞書的にアクセス・検索できるように

Q.48

文書の解析器 DocumentAnalyzer を定義し、語数・頻度・平均行長・先頭行・保存メソッドなどを備えること。

項目 内容
クラス名 DocumentAnalyzer
属性 text: str(入力文書)
発展仕様 - 総語数、語の出現頻度、平均行長、最長行、先頭行をプロパティで提供
- 任意ファイル保存
- 入力の正規化処理
- 非ASCIIの語も許容
- 空行除去処理
- カスタム除外語対応(stopwords)
使用構文 str.split, collections.Counter, @property, __len__, with open, statistics.mean, str.strip, str.lower, Exception, try-except, re

A.48

■ 模範解答

import re
from collections import Counter
from statistics import mean
from typing import Optional, Iterable

class DocumentAnalyzer:
    def __init__(self, text: str, stopwords: Optional[Iterable[str]] = None):
        # 文書を改行単位で分割し、空行を除外
        self._lines = [line.strip() for line in text.strip().split('\n') if line.strip()]
        self._stopwords = set(word.lower() for word in (stopwords or []))

        # 単語リストを正規表現により抽出(英単語・数字・Unicode語など)
        self._words = []
        for line in self._lines:
            words = re.findall(r'\b[\w\'\-]+\b', line.lower())  # 単語のみを抽出、小文字化
            self._words.extend([w for w in words if w not in self._stopwords])  # ストップワード除外

        # 頻度分布を構築
        self._freq = Counter(self._words)

    @property
    def word_count(self) -> int:
        """文書内の総語数"""
        return len(self._words)

    @property
    def word_frequency(self) -> Counter:
        """単語の出現頻度(Counter)"""
        return self._freq

    @property
    def first_line(self) -> Optional[str]:
        """最初の行(空行除去済)"""
        return self._lines[0] if self._lines else None

    @property
    def longest_line(self) -> Optional[str]:
        """最も長い行"""
        return max(self._lines, key=len, default=None)

    @property
    def average_line_length(self) -> float:
        """平均行長"""
        return round(mean(len(line) for line in self._lines), 2) if self._lines else 0.0

    def save_to_file(self, path: str) -> bool:
        """解析結果をファイルに保存"""
        try:
            with open(path, 'w', encoding='utf-8') as f:
                f.write(f"Total words: {self.word_count}\n")
                f.write(f"Average line length: {self.average_line_length}\n")
                f.write(f"Most common words:\n")
                for word, count in self._freq.most_common(10):
                    f.write(f"  {word}: {count}\n")
            return True
        except Exception as e:
            print(f"保存失敗: {e}")
            return False

    def __len__(self) -> int:
        """行数を返す(空行除外済)"""
        return len(self._lines)

    def __repr__(self) -> str:
        """要約表示"""
        return f"<DocumentAnalyzer: {len(self)} lines, {self.word_count} words>"
実行例1:語数・頻度・平均行長の表示
text = """
The quick brown fox jumps over the lazy dog.
This is a test document. It contains several words.
The document is meant for analysis.
"""

analyzer = DocumentAnalyzer(text, stopwords=["the", "is", "a"])

print(analyzer.word_count)
# 13

print(analyzer.word_frequency.most_common(3))
# [('document', 2), ('quick', 1), ('brown', 1)]

print(analyzer.first_line)
# "The quick brown fox jumps over the lazy dog."

print(analyzer.average_line_length)
# e.g. 47.67

print(len(analyzer))
# 3
実行例2:ファイルへの保存と出力確認
success = analyzer.save_to_file("output_analysis.txt")
print("保存成功:", success)
# 保存成功: True
output_analysis.txtの例
Total words: 13
Average line length: 47.67
Most common words:
  document: 2
  quick: 1
  brown: 1
  fox: 1
  jumps: 1
  over: 1
  lazy: 1
  dog: 1
  this: 1
  test: 1

■ 文法・構文まとめ

構文/技法 解説
re.findall() 正規表現で単語抽出(Unicode対応)
str.strip() 空白除去。空行判定・整形に使用
Counter 単語頻度分布を記録
@property 呼び出し時に計算・取得できるインターフェース提供
statistics.mean() 平均行長の計算に使用
__len__ / __repr__ Python的なオブジェクト振る舞いの追加(組み込み関数対応)
stopwords引数 オプションで除外単語を定義可能
try-except + with open 例外安全なファイル保存処理

Q.49

タスクのコレクションを保持する TaskManager クラスを定義せよ。状態変更・優先度付きソート・タスクのインポートをサポートすること。

項目 内容
クラス名 TaskManager / Task
属性 Task: title, priority, completed, tags, created_at
発展仕様 - タスク個別クラスを導入
- タグによるフィルタ検索
- 優先度順・作成日順ソート
- 状態変更操作
- タスクのリストインポート対応
- __iter__ で反復可能に
使用構文 list, sort, filter, @property, __iter__, __repr__, classmethod, from_dicts, lambda, datetime

A.49

■ 模範解答

from datetime import datetime
from typing import List, Dict

class Task:
    def __init__(self, title: str, priority: int = 1, tags: List[str] = None, completed: bool = False):
        self.title = title
        self.priority = priority
        self.tags = tags or []
        self.completed = completed
        self.created_at = datetime.now()  # 作成時刻を記録

    def mark_done(self):
        self.completed = True  # 完了状態に変更

    def mark_undone(self):
        self.completed = False  # 未完了に戻す

    def __repr__(self):
        # タスクの要約表現
        status = "" if self.completed else ""
        return f"<Task '{self.title}' (P:{self.priority}) {status}>"

class TaskManager:
    def __init__(self):
        self._tasks: List[Task] = []  # タスクのリストを保持

    def add_task(self, task: Task):
        self._tasks.append(task)

    def get_all(self) -> List[Task]:
        return list(self._tasks)  # 保持タスクを全取得

    def get_pending(self) -> List[Task]:
        # 未完了タスクのみ取得
        return [t for t in self._tasks if not t.completed]

    def get_by_tag(self, tag: str) -> List[Task]:
        # 指定タグを含むタスクをフィルタ
        return [t for t in self._tasks if tag in t.tags]

    def sort_by_priority(self, reverse: bool = True) -> List[Task]:
        # 優先度でソート(高優先が先頭)
        return sorted(self._tasks, key=lambda t: t.priority, reverse=reverse)

    def sort_by_created(self) -> List[Task]:
        # 作成時刻でソート(古い順)
        return sorted(self._tasks, key=lambda t: t.created_at)

    def __iter__(self):
        # 反復可能にする(for t in manager)
        return iter(self._tasks)

    def __repr__(self):
        return f"<TaskManager with {len(self._tasks)} tasks>"

    @classmethod
    def from_dicts(cls, data: List[Dict]) -> "TaskManager":
        # dictのリストからTaskManagerを構築
        manager = cls()
        for item in data:
            task = Task(
                title=item.get("title", ""),
                priority=item.get("priority", 1),
                tags=item.get("tags", []),
                completed=item.get("completed", False)
            )
            manager.add_task(task)
        return manager
実行例1:登録と検索、優先度順
manager = TaskManager()

manager.add_task(Task("Write report", priority=3, tags=["work"]))
manager.add_task(Task("Buy groceries", priority=1, tags=["home"]))
manager.add_task(Task("Read book", priority=2, tags=["leisure"]))

for task in manager.sort_by_priority():
    print(task)

# 出力例(優先度降順)
# <Task 'Write report' (P:3) ✗>
# <Task 'Read book' (P:2) ✗>
# <Task 'Buy groceries' (P:1) ✗>
実行例2:状態変更とタグ検索、辞書から生成
data = [
    {"title": "Plan trip", "priority": 2, "tags": ["travel"]},
    {"title": "Pay bills", "priority": 1, "tags": ["finance"], "completed": True},
]

manager2 = TaskManager.from_dicts(data)
manager2.get_all()[0].mark_done()

for task in manager2.get_by_tag("travel"):
    print(task)

# 出力:
# <Task 'Plan trip' (P:2) ✓>

■ 文法・構文まとめ

使用構文 解説
__call__ 関数のようにIDを発行(gen() の形)
str.zfill() 数値のゼロ埋め(桁揃え)
f"{...}" プレフィクス/サフィクス付きの整形文字列構築
@property .current で次IDを安全に確認
__getitem__ manager['key'] でジェネレータアクセス
@classmethod 構成辞書からジェネレータ一括初期化
ValueError 重複登録や不正値リセットへの対処

Q.50

柔軟な構成で一意のIDを生成する CustomIDGenerator クラスを定義せよ。複数のジェネレータを辞書形式で管理できるインターフェースも備えること。

項目 内容
クラス名 CustomIDGenerator / IDGeneratorManager
属性 prefix, suffix, width, count
発展仕様 - str.zfill によるゼロ埋め
- f-string によるID構成
- 辞書型で複数のIDジェネレータ管理
- reset() でリセット可
- __call__ によるID生成
使用構文 str.zfill, f-string, @property, dict, __getitem__, __call__, reset, classmethod, ValueError

A.50

■ 模範解答

class CustomIDGenerator:
    def __init__(self, prefix: str = '', suffix: str = '', width: int = 4, start: int = 0):
        self.prefix = prefix            # IDの先頭に付ける文字列
        self.suffix = suffix            # IDの末尾に付ける文字列
        self.width = width              # 数字部分の桁数(ゼロ埋め用)
        self._count = start             # 現在のカウンタ値(非公開属性)

    def __call__(self) -> str:
        # ID生成機能(インスタンスを関数のように使う)
        core = str(self._count).zfill(self.width)      # 数字部分をゼロ埋め
        generated = f"{self.prefix}{core}{self.suffix}" # 全体構成
        self._count += 1
        return generated

    def reset(self, value: int = 0):
        # カウンタを任意の値にリセット
        if value < 0:
            raise ValueError("Reset value must be non-negative.")
        self._count = value

    @property
    def current(self) -> str:
        # 現在の次IDを確認(呼び出さずに確認)
        return f"{self.prefix}{str(self._count).zfill(self.width)}{self.suffix}"

    def __repr__(self):
        return f"<IDGenerator prefix={self.prefix!r} width={self.width} next={self._count}>"

class IDGeneratorManager:
    def __init__(self):
        self._generators: dict[str, CustomIDGenerator] = {}

    def add_generator(self, name: str, generator: CustomIDGenerator):
        # 名前付きでジェネレータを登録
        if name in self._generators:
            raise ValueError(f"Generator '{name}' already exists.")
        self._generators[name] = generator

    def __getitem__(self, name: str) -> CustomIDGenerator:
        # dictのようにアクセス
        return self._generators[name]

    def __call__(self, name: str) -> str:
        # ID生成:manager("user") のように呼べる
        return self._generators[name]()

    @classmethod
    def from_configs(cls, config_dict: dict[str, dict]) -> "IDGeneratorManager":
        # 辞書構成で一括初期化可能なクラスメソッド
        instance = cls()
        for name, cfg in config_dict.items():
            instance.add_generator(name, CustomIDGenerator(**cfg))
        return instance

    def __repr__(self):
        return f"<IDGeneratorManager with {len(self._generators)} generators>"
実行例1:基本的なID生成とリセット
gen = CustomIDGenerator(prefix="USR-", width=5)
print(gen())        # => USR-00000
print(gen())        # => USR-00001
print(gen.current)  # => USR-00002
gen.reset(100)
print(gen())        # => USR-00100
実行例2:複数IDジェネレータを辞書的に管理
configs = {
    "user": {"prefix": "USR-", "width": 4},
    "order": {"prefix": "ORD-", "width": 6, "start": 5000},
}

manager = IDGeneratorManager.from_configs(configs)

print(manager("user"))   # => USR-0000
print(manager("order"))  # => ORD-005000
print(manager["order"].current)  # => ORD-005001

■ 文法・構文まとめ

使用構文 解説
__call__ 関数のようにIDを発行(gen() の形)
str.zfill() 数値のゼロ埋め(桁揃え)
f"{...}" プレフィクス/サフィクス付きの整形文字列構築
@property .current で次IDを安全に確認
__getitem__ manager['key'] でジェネレータアクセス
@classmethod 構成辞書からジェネレータ一括初期化
ValueError 重複登録や不正値リセットへの対処
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?