Documentation Python

Pythonの@classmethodデコレータは、クラス自体に対して操作を行うメソッドを定義するために使用されます。インスタンスメソッドが個別のオブジェクトに対して動作するのに対し、クラスメソッドはクラス全体に関連する処理を担当します。

メソッドの種類と比較

メソッド種別デコレータ第一引数クラス属性インスタンス属性主な用途
インスタンスメソッドなしselfオブジェクト固有の処理
クラスメソッド@classmethodcls×クラス共通の処理、ファクトリ
静的メソッド@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は、クラス全体に関連する処理を定義するための強力なツールです。ファクトリメソッドパターン、シングルトン、レジストリなど、様々なデザインパターンの実装に活用できます。

参考文献

円