12.

Python のクラスとメソッド|self・classmethod・staticmethod の違い

編集
この記事の要点
  • メソッドの第 1 引数は必ず self。呼ぶときは書かず、Python が自動で渡す
  • メソッドは 3 種類。インスタンスメソッド / @classmethod / @staticmethod
  • 属性はクラス直下に書くと全インスタンスで共有__init__ の中で self.x = と書くと個別
  • private 修飾子は無い。_name は「触らないでほしい」という合図だけ
  • getter / setter は書かず、普通の属性にしておき、必要になったら @property にする

クラスとメソッドの最小形

class User:
    def __init__(self, name, age):     # 初期化。インスタンス生成時に呼ばれる
        self.name = name               # インスタンス属性
        self.age = age

    def greet(self):                   # インスタンスメソッド
        return f"{self.name}({self.age}歳) です"

    def birthday(self):
        self.age += 1                  # 自分の状態を変える
        return self.age

u = User("田中", 30)      # new は要らない
print(u.greet())          # 田中(30歳) です
print(u.birthday())       # 31
print(u.name)             # 田中   属性に直接アクセスできる

定義には self を書き、呼び出しには書きません。u.greet() は内部的に User.greet(u) として実行されます。self を書き忘れると TypeError: greet() takes 0 positional arguments but 1 was given になります。

メソッドの 3 種類

種類第 1 引数使う場面
インスタンスメソッドselfそのインスタンスの状態を読み書きする
@classmethodcls別の作り方を提供する(ファクトリ)、クラス変数を扱う
@staticmethodなし状態を使わないがクラスに置きたい補助処理
class User:
    species = "human"                  # クラス変数(全員で共有)

    def __init__(self, name):
        self.name = name

    def greet(self):                   # インスタンスメソッド
        return f"{self.name} です"

    @classmethod
    def from_csv(cls, line):           # 別の作り方
        name = line.split(",")[0]
        return cls(name)               # cls は User。継承先では子クラスになる

    @staticmethod
    def is_valid(name):                # self も cls も使わない
        return bool(name) and len(name) <= 20

print(User.from_csv("田中,30").greet())   # 田中 です
print(User.is_valid(""))                  # False

@classmethodcls(...) と書くのがポイントです。User(...) と直書きすると、継承したクラスから呼んだときに親クラスのインスタンスができてしまいます。

クラス変数とインスタンス変数

class Counter:
    total = 0                # クラス変数
    log = []                 # ← 危険。全インスタンスで共有される

    def __init__(self):
        Counter.total += 1
        self.count = 0       # インスタンス変数

a, b = Counter(), Counter()
print(Counter.total)         # 2

a.log.append("x")
print(b.log)                 # ['x']   ← b にも見えてしまう

リストや辞書をクラス直下に書くと1 個の実体を全員で共有します。個別に持たせたいものは必ず __init__ の中で self.log = [] と作ります。詳細は クラス変数とインスタンス変数 を参照してください。

公開・非公開の扱い

class Account:
    def __init__(self, balance):
        self.balance = balance      # 公開
        self._rate = 0.01           # 内部用(触らないでほしい、の合図)
        self.__secret = "x"         # 名前マングリングされる

a = Account(100)
print(a._rate)                      # 0.01   アクセスできてしまう
# print(a.__secret)                 # AttributeError
print(a._Account__secret)           # x      実際はこの名前になっている

Python に private キーワードはありません。アンダースコアは規約による意思表示で、強制力はありません。

getter / setter は書かない

class Product:
    def __init__(self, price):
        self.price = price          # まずは素直な属性でよい

    @property
    def tax_included(self):         # 呼び出し側は p.tax_included と書ける
        return int(self.price * 1.1)

p = Product(1000)
print(p.tax_included)               # 1100   かっこは不要

実務では次のように書きます。あとから検証を足したくなったときだけ @property に変えるのが Python 流です。呼び出し側のコードは変えずに済みます。

class Product:
    def __init__(self, price):
        self._price = price

    @property
    def price(self):
        return self._price

    @price.setter
    def price(self, value):
        if value < 0:
            raise ValueError("価格は 0 以上にしてください")
        self._price = value

p = Product(1000)
p.price = 1200        # setter が呼ばれる
print(p.price)        # 1200
p.price = -1          # ValueError

よく使う特殊メソッド

class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y

    def __repr__(self):                # デバッグ表示。print やログで役立つ
        return f"Point({self.x}, {self.y})"

    def __eq__(self, other):           # == の比較方法
        return (self.x, self.y) == (other.x, other.y)

    def __len__(self):                 # len() が使えるようになる
        return 2

print(Point(1, 2))                     # Point(1, 2)
print(Point(1, 2) == Point(1, 2))      # True

単純なデータの入れ物なら @dataclass を使うと __init____repr____eq__ が自動生成されます。

from dataclasses import dataclass, field

@dataclass
class Point:
    x: int
    y: int = 0
    tags: list[str] = field(default_factory=list)   # 可変な既定値はこう書く

print(Point(1, 2))          # Point(x=1, y=2, tags=[])

self を書き忘れたときの症状

class User:
    def __init__(self, name):
        self.name = name

    def greet():                      # self が無い
        return "hello"

    def rename(self, name):
        name = name                   # self を付け忘れ → ローカル変数を書き換えただけ

u = User("田中")
u.greet()        # TypeError: greet() takes 0 positional arguments but 1 was given
u.rename("鈴木")
print(u.name)    # 田中   ← 変わっていない

2 つ目はエラーにならないぶん厄介です。「代入したのに反映されない」ときは、self. が付いているかを最初に確認してください。

属性は後から足せる

class User:
    def __init__(self, name):
        self.name = name

u = User("田中")
u.age = 30                  # 定義していない属性を足せてしまう
print(u.__dict__)           # {'name': '田中', 'age': 30}

print(getattr(u, "email", "未設定"))    # 未設定   無い属性を安全に読む
print(hasattr(u, "age"))                # True

# 属性を固定したいなら __slots__
class Point:
    __slots__ = ("x", "y")
    def __init__(self, x, y):
        self.x, self.y = x, y

p = Point(1, 2)
p.z = 3         # AttributeError: 'Point' object has no attribute 'z'

タイプミスした属性名がそのまま新しい属性として作られるため、u.nmae = "鈴木" のような誤りに気づけません。大量に作るクラスでは __slots__ がメモリ削減にもなります。

継承と委譲の使い分け

# 継承: 「A は B の一種である」
class Admin(User):
    def __init__(self, name, level):
        super().__init__(name)
        self.level = level

# 委譲: 「A は B を持っている」
class Report:
    def __init__(self, formatter):
        self.formatter = formatter        # 差し替えられる

    def render(self, rows):
        return self.formatter.format(rows)

「は〜の一種」と言えないなら継承にしないでください。振る舞いを差し替えたいだけなら、オブジェクトを持たせる(委譲)ほうが柔軟で、テストでも差し替えが簡単になります。詳細は 継承の概念と必要性 を参照してください。

抽象クラスで実装を強制する

from abc import ABC, abstractmethod

class Formatter(ABC):
    @abstractmethod
    def format(self, rows) -> str:
        ...

class CsvFormatter(Formatter):
    def format(self, rows) -> str:
        return "\n".join(",".join(r) for r in rows)

class Broken(Formatter):
    pass

Broken()      # TypeError: Can't instantiate abstract class Broken
              # with abstract method format

@abstractmethod を付けたメソッドを実装しないクラスは、インスタンス化しようとした時点でエラーになります。実装漏れを実行時まで持ち越さずに済みます。

関連

編集
Post Share
子ページ
  1. クラスの定義
  2. initメソッド
  3. 関数の定義
  4. クラス変数とインスタンス変数
同階層のページ
  1. 基本的なルール
  2. 変数
  3. 演算子
  4. 標準ライブラリ
  5. 外部ライブラリ
  6. 制御構文
  7. リスト(配列)
  8. タプル
  9. セット
  10. 辞書(dict)
  11. クラスとメソッド
  12. 継承の概念と必要性
  13. 継承の構文
  14. コンストラクタ
  15. cookieの値の設定と取得
  16. 例外処理
  17. 例外を文字列で出力する方法
  18. httpリクエスト(curl)をする方法
  19. Responseオブジェクトの中身の確認
  20. 変数が空かどうか判定する方法
  21. タイムゾーンの設定と現在日時の取得と文字列化
  22. シングルクォーテーションとダブルクォーテーションの違い

最近更新/作成されたページ