Pythonの@classmethodデコレータは、クラス自体に対して操作を行うメソッドを定義するために使用されます。インスタンスメソッドが個別のオブジェクトに対して動作するのに対し、クラスメソッドはクラス全体に関連する処理を担当します。
メソッドの種類と比較
| メソッド種別 | デコレータ | 第一引数 | クラス属性 | インスタンス属性 | 主な用途 |
|---|---|---|---|---|---|
| インスタンスメソッド | なし | self | ○ | ○ | オブジェクト固有の処理 |
| クラスメソッド | @classmethod | cls | ○ | × | クラス共通の処理、ファクトリ |
| 静的メソッド | @staticmethod | なし | × | × | ユーティリティ関数 |
基本的な使い方
構文
class MyClass:
@classmethod
def method_name(cls, arg1, arg2):
# clsはクラス自体を参照
pass
基本例
class Counter:
"""クラス変数を管理するカウンター"""
count = 0 # クラス変数
def __init__(self):
Counter.increment()
@classmethod
def increment(cls):
"""カウントを増加"""
cls.count += 1
@classmethod
def get_count(cls):
"""現在のカウントを取得"""
return cls.count
@classmethod
def reset(cls):
"""カウントをリセット"""
cls.count = 0
# 使用例
print(Counter.get_count()) # 0
c1 = Counter()
c2 = Counter()
c3 = Counter()
print(Counter.get_count()) # 3
Counter.reset()
print(Counter.get_count()) # 0
ファクトリメソッドパターン
基本的なファクトリメソッド
from datetime import datetime
class Person:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
@classmethod
def from_birth_year(cls, name: str, birth_year: int):
"""生年からインスタンスを生成"""
current_year = datetime.now().year
age = current_year - birth_year
return cls(name, age)
@classmethod
def from_dict(cls, data: dict):
"""辞書からインスタンスを生成"""
return cls(data['name'], data['age'])
@classmethod
def from_string(cls, info: str):
"""文字列からインスタンスを生成('名前,年齢'形式)"""
name, age = info.split(',')
return cls(name.strip(), int(age.strip()))
def __repr__(self):
return f"Person(name='{self.name}', age={self.age})"
# 様々な方法でインスタンス生成
p1 = Person("Alice", 30)
p2 = Person.from_birth_year("Bob", 1990)
p3 = Person.from_dict({'name': 'Charlie', 'age': 25})
p4 = Person.from_string("Diana, 28")
print(p1) # Person(name='Alice', age=30)
print(p2) # Person(name='Bob', age=34)
print(p3) # Person(name='Charlie', age=25)
print(p4) # Person(name='Diana', age=28)
複数のファクトリメソッド
import json
from pathlib import Path
from typing import Optional
class Configuration:
"""設定を管理するクラス"""
def __init__(self, settings: dict):
self.settings = settings
@classmethod
def from_json_file(cls, filepath: str):
"""JSONファイルから設定を読み込み"""
with open(filepath, 'r', encoding='utf-8') as f:
settings = json.load(f)
return cls(settings)
@classmethod
def from_json_string(cls, json_string: str):
"""JSON文字列から設定を読み込み"""
settings = json.loads(json_string)
return cls(settings)
@classmethod
def from_env(cls, prefix: str = 'APP_'):
"""環境変数から設定を読み込み"""
import os
settings = {
key[len(prefix):].lower(): value
for key, value in os.environ.items()
if key.startswith(prefix)
}
return cls(settings)
@classmethod
def default(cls):
"""デフォルト設定を生成"""
return cls({
'debug': False,
'log_level': 'INFO',
'timeout': 30,
})
def get(self, key: str, default=None):
return self.settings.get(key, default)
# 使用例
config = Configuration.default()
print(config.get('debug')) # False
print(config.get('timeout')) # 30
継承とクラスメソッド
サブクラスでの動作
class Animal:
species = "Unknown"
def __init__(self, name: str):
self.name = name
@classmethod
def create(cls, name: str):
"""クラスメソッドはサブクラスのコンテキストで動作"""
print(f"Creating {cls.__name__}: {name}")
return cls(name)
@classmethod
def get_species(cls):
return cls.species
class Dog(Animal):
species = "Canine"
class Cat(Animal):
species = "Feline"
# サブクラスからクラスメソッドを呼び出し
dog = Dog.create("Buddy") # Creating Dog: Buddy
cat = Cat.create("Whiskers") # Creating Cat: Whiskers
print(type(dog)) # <class '__main__.Dog'>
print(type(cat)) # <class '__main__.Cat'>
print(Dog.get_species()) # Canine
print(Cat.get_species()) # Feline
ファクトリメソッドの継承
from abc import ABC, abstractmethod
from datetime import datetime
class Document(ABC):
"""ドキュメントの基底クラス"""
def __init__(self, title: str, content: str):
self.title = title
self.content = content
self.created_at = datetime.now()
@classmethod
def from_file(cls, filepath: str):
"""ファイルからドキュメントを生成"""
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
title = Path(filepath).stem
return cls(title, content)
@abstractmethod
def render(self) -> str:
pass
class MarkdownDocument(Document):
"""Markdownドキュメント"""
def render(self) -> str:
return f"# {self.title}\n\n{self.content}"
class HTMLDocument(Document):
"""HTMLドキュメント"""
def render(self) -> str:
return f"<h1>{self.title}</h1>\n<p>{self.content}</p>"
# from_fileはサブクラスのインスタンスを返す
# md_doc = MarkdownDocument.from_file("readme.md")
# html_doc = HTMLDocument.from_file("index.html")
クラスメソッドとインスタンスメソッドの組み合わせ
class BankAccount:
"""銀行口座クラス"""
interest_rate = 0.02 # クラス変数(金利)
accounts = [] # 全アカウントのリスト
def __init__(self, account_id: str, balance: float = 0):
self.account_id = account_id
self.balance = balance
BankAccount.accounts.append(self)
# インスタンスメソッド
def deposit(self, amount: float):
"""入金"""
self.balance += amount
def withdraw(self, amount: float) -> bool:
"""出金"""
if amount <= self.balance:
self.balance -= amount
return True
return False
def apply_interest(self):
"""利息を適用(クラス変数の金利を使用)"""
self.balance *= (1 + self.__class__.interest_rate)
# クラスメソッド
@classmethod
def set_interest_rate(cls, rate: float):
"""金利を設定"""
cls.interest_rate = rate
@classmethod
def get_total_balance(cls) -> float:
"""全アカウントの合計残高"""
return sum(account.balance for account in cls.accounts)
@classmethod
def find_by_id(cls, account_id: str):
"""IDでアカウントを検索"""
for account in cls.accounts:
if account.account_id == account_id:
return account
return None
@classmethod
def apply_interest_to_all(cls):
"""全アカウントに利息を適用"""
for account in cls.accounts:
account.apply_interest()
# 使用例
BankAccount.accounts.clear() # リセット
acc1 = BankAccount("A001", 1000)
acc2 = BankAccount("A002", 2000)
acc3 = BankAccount("A003", 3000)
print(f"合計残高: {BankAccount.get_total_balance()}") # 6000
BankAccount.set_interest_rate(0.05) # 金利を5%に設定
BankAccount.apply_interest_to_all()
print(f"利息適用後の合計: {BankAccount.get_total_balance()}") # 6300
found = BankAccount.find_by_id("A002")
print(f"A002の残高: {found.balance}") # 2100.0
シングルトンパターン
class Singleton:
"""シングルトンパターンの実装"""
_instance = None
def __init__(self, value: str):
self.value = value
@classmethod
def get_instance(cls, value: str = "default"):
"""インスタンスを取得(なければ生成)"""
if cls._instance is None:
cls._instance = cls(value)
return cls._instance
@classmethod
def reset(cls):
"""インスタンスをリセット(テスト用)"""
cls._instance = None
# 使用例
s1 = Singleton.get_instance("first")
s2 = Singleton.get_instance("second")
print(s1.value) # first
print(s2.value) # first
print(s1 is s2) # True
レジストリパターン
from typing import Type, Dict
class Plugin:
"""プラグインの基底クラス"""
_registry: Dict[str, Type['Plugin']] = {}
@classmethod
def register(cls, name: str):
"""プラグインを登録するデコレータ"""
def decorator(subclass):
cls._registry[name] = subclass
return subclass
return decorator
@classmethod
def create(cls, name: str, *args, **kwargs):
"""名前からプラグインを生成"""
if name not in cls._registry:
raise ValueError(f"Unknown plugin: {name}")
return cls._registry[name](*args, **kwargs)
@classmethod
def list_plugins(cls):
"""登録済みプラグインの一覧"""
return list(cls._registry.keys())
@Plugin.register("json")
class JSONPlugin(Plugin):
def process(self, data):
import json
return json.dumps(data)
@Plugin.register("csv")
class CSVPlugin(Plugin):
def process(self, data):
return ",".join(str(item) for item in data)
# 使用例
print(Plugin.list_plugins()) # ['json', 'csv']
json_plugin = Plugin.create("json")
csv_plugin = Plugin.create("csv")
print(json_plugin.process({'a': 1})) # {"a": 1}
print(csv_plugin.process([1, 2, 3])) # 1,2,3
@staticmethodとの使い分け
class DateUtils:
"""日付ユーティリティクラス"""
default_format = "%Y-%m-%d" # クラス変数
@classmethod
def set_default_format(cls, fmt: str):
"""デフォルトフォーマットを設定(クラス変数を変更)"""
cls.default_format = fmt
@classmethod
def format_date(cls, date: datetime) -> str:
"""デフォルトフォーマットで日付を文字列化"""
return date.strftime(cls.default_format)
@staticmethod
def is_weekend(date: datetime) -> bool:
"""週末かどうか判定(クラス状態に依存しない)"""
return date.weekday() >= 5
@staticmethod
def days_between(date1: datetime, date2: datetime) -> int:
"""2つの日付間の日数(クラス状態に依存しない)"""
return abs((date2 - date1).days)
# 使用例
from datetime import datetime
now = datetime.now()
# クラスメソッド - クラス変数を使用
print(DateUtils.format_date(now)) # 2024-01-15
DateUtils.set_default_format("%d/%m/%Y")
print(DateUtils.format_date(now)) # 15/01/2024
# 静的メソッド - クラス状態に依存しない
print(DateUtils.is_weekend(datetime(2024, 1, 13))) # True (土曜日)
print(DateUtils.days_between(
datetime(2024, 1, 1),
datetime(2024, 1, 15)
)) # 14
バリデーション付きファクトリ
from typing import Optional
import re
class Email:
"""メールアドレスを表すクラス"""
def __init__(self, address: str):
self._address = address
@property
def address(self) -> str:
return self._address
@property
def domain(self) -> str:
return self._address.split('@')[1]
@classmethod
def create(cls, address: str) -> Optional['Email']:
"""バリデーション付きでインスタンスを生成"""
if cls.is_valid(address):
return cls(address)
return None
@classmethod
def is_valid(cls, address: str) -> bool:
"""メールアドレスの形式を検証"""
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return bool(re.match(pattern, address))
def __repr__(self):
return f"Email('{self._address}')"
# 使用例
email1 = Email.create("user@example.com")
email2 = Email.create("invalid-email")
print(email1) # Email('user@example.com')
print(email2) # None
if email1:
print(email1.domain) # example.com
まとめ
| 用途 | 使用するメソッド |
|---|---|
| クラス変数の操作 | @classmethod |
| ファクトリメソッド | @classmethod |
| サブクラス対応のファクトリ | @classmethod |
| シングルトン/レジストリ | @classmethod |
| ユーティリティ関数 | @staticmethod |
| インスタンス固有の処理 | 通常のメソッド |
@classmethodは、クラス全体に関連する処理を定義するための強力なツールです。ファクトリメソッドパターン、シングルトン、レジストリなど、様々なデザインパターンの実装に活用できます。