まえがき
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})")
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')]
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)})")
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)
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)")
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
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)
acc = SecureBankAccount("Alice", 1000, "secure123")
acc.deposit(500)
acc.withdraw(300, "secure123")
print(acc) # => SecureBankAccount(owner='Alice', balance=1200.00)
print(acc.show_logs())
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
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()) # ログに失敗が記録されている
認証失敗: 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})>"
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)>
c = TaggedCounterCollection()
c.add_counter("downloads", 1)
c.increment("downloads") # 1回目は成功
try:
c.increment("downloads") # 2回目はエラー
except LimitExceededError as e:
print("例外発生:", e)
例外発生: 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})"
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要素になる
p = ImmutablePersonV2("Bob", 1985)
try:
p.name = "Charlie" # 変更を試みる → 例外
except ImmutableViolationError as e:
print("例外:", e)
try:
p._name = "Charlie" # 直接の変更も禁止されている
except ImmutableViolationError as e:
print("例外:", e)
例外: 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>"
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}
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>"
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
success = analyzer.save_to_file("output_analysis.txt")
print("保存成功:", success)
# 保存成功: True
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
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) ✗>
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>"
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
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 |
重複登録や不正値リセットへの対処 |