オブジェクトを作るたびに属性の初期値を入れたり、設定値を整えておくには、コンストラクタである__init__の使い方を理解することが近道です。
本記事では、__init__の役割、引数設計、よくある落とし穴、実践パターンまでを初心者の方にも分かるように段階的に解説します。
実行できるサンプルコードと出力も順に示します。
__init__とは: オブジェクト生成と初期化の基本
__init__の役割と呼ばれるタイミング
__init__は、クラスからインスタンスが生成された直後に自動的に呼ばれる「初期化メソッド」です。
インスタンスの雛形となるメモリは__new__で確保され、その直後に__init__で属性へ値を入れたり、前処理をしたりします。
以下は最小の例です。
生成のタイミングでメッセージを表示し、インスタンス変数を設定します。
# __init__の呼び出しタイミングを確認する最小例
class Greeter:
def __init__(self, name): # nameは必須引数
print("[__init__] called") # いつ呼ばれるかを可視化
self.name = name # インスタンス変数の設定
def say_hello(self):
return f"Hello, {self.name}!"
g = Greeter("Alice") # インスタンス生成と同時に__init__が呼ばれる
print(g.say_hello())
[__init__] called
Hello, Alice!
- データモデル: 特殊メソッド(init / new) — Python 3.13.7 ドキュメント
- オブジェクト生成と__new__ — Python 3.13.7 ドキュメント
- 関連記事:print()の使い方と基本の表示サンプル集
- 関連記事:selfとは何か?クラスとメソッドの基本と使い方
selfの意味とインスタンス変数の設定
selfは「今まさに初期化しているインスタンス自身」を指します。
self.x = ...のようにして、インスタンスごとに固有のデータを保持します。
ローカル変数に値を入れるだけではインスタンスに保存されません。
# selfを付け忘れるとインスタンスに値が残らない例
class Counter:
def __init__(self, start):
count = start # これはローカル変数。インスタンスに保存されない
self.count = start # こちらが正しい。各インスタンスの状態となる
c1 = Counter(10)
# print(count) # NameError: ローカル変数は外から参照できない
print(c1.count) # self.countはインスタンスに保存されている
10
インスタンスの状態として残したいデータは、必ずself.属性名へ代入します。
__init__の定義とシグネチャ(引数と戻り値)
__init__(self, ...)は任意の引数を取れますが、戻り値は常にNoneでなければなりません。
値を返すとTypeErrorになります。
class Sample:
def __init__(self, x: int, y: int = 0): # 型ヒントとデフォルト値
self.x = x
self.y = y
# return 123 # これはエラー: __init__() should return None
以下に、よく使う引数の種類を整理します。
| 種類 | 例 | 説明 |
|---|---|---|
| 必須位置引数 | x | 常に渡す必要がある |
| オプション(デフォルト) | y=0 | 省略可能。指定がなければ既定値 |
| 可変長(位置) | *args | 追加の位置引数をまとめて受け取る |
| 可変長(キーワード) | **kwargs | 追加のキーワード引数を辞書で受け取る |
| 戻り値 | None | 何も返さない。返すとエラー |
__new__との違い(生成と初期化の分担)
__new__はインスタンスを「生成」し、__init__は「初期化」します。
通常は__init__のみを書けば十分です。
__new__はイミュータブル型(例: tuple)のサブクラス化など特殊なケースで使います。
# __new__と__init__の呼び順を確認する例
class Trace:
def __new__(cls, *args, **kwargs):
print("[__new__] allocate instance")
return super().__new__(cls) # インスタンスを実際に生成
def __init__(self, value):
print("[__init__] initialize instance")
self.value = value
t = Trace(42)
print(t.value)
[__new__] allocate instance
[__init__] initialize instance
42
__init__の書き方: 引数設計とデフォルト値
必須引数とオプション引数の設計
初期化時に必ず必要な情報と、後からでも設定できる情報を分けると分かりやすくなります。
「なければオブジェクトが成立しないもの」は必須引数、「なければ既定挙動でよいもの」はオプションにします。
class User:
def __init__(self, name, is_admin=False):
# nameは必須、is_adminはオプション
if not name:
raise ValueError("nameは空にできません")
self.name = name
self.is_admin = is_admin
u1 = User("Alice")
u2 = User("Bob", is_admin=True)
print(u1.name, u1.is_admin)
print(u2.name, u2.is_admin)
Alice False
Bob True
デフォルト引数と型ヒントの付け方
型ヒントは読みやすさとツール支援に有効です。
オプション値にはOptional[型]を使います。
from typing import Optional
class Article:
def __init__(self, title: str, body: str, tags: Optional[list[str]] = None):
self.title = title
self.body = body
self.tags = tags if tags is not None else [] # 後述のミュータブル注意点にも対応
a = Article("入門", "本文です")
print(a.title, a.tags)
入門 []
可変長引数(*args, **kwargs)の受け取り方
未知の追加オプションを受け付けたい場合、*argsと**kwargsで受け取って安全に保存や委譲ができます。
class Configurable:
def __init__(self, name: str, *plugins, **options):
self.name = name
self.plugins = list(plugins) # 位置引数の余りをプラグイン群として扱う
self.options = dict(options) # キーワード引数を設定辞書として保持
c = Configurable("service", "p1", "p2", timeout=3.0, retries=2)
print(c.plugins)
print(c.options)
['p1', 'p2']
{'timeout': 3.0, 'retries': 2}
ミュータブルなデフォルト引数の落とし穴(list dict)
ミュータブル(書き換え可能)なオブジェクトをデフォルト引数に直接書くのは危険です。
関数定義時に1回だけ作られ、インスタンス間で共有されてしまいます。
デフォルトはNoneにして中で新しく作るのが定石です。
# 悪い例: リストが全インスタンスで共有される
class BagBad:
def __init__(self, items=[]):
self.items = items
b1 = BagBad()
b2 = BagBad()
b1.items.append("apple")
print("bad:", b1.items, b2.items) # b2にも影響が出る
# 良い例: Noneを使って都度新しく作る
class BagGood:
def __init__(self, items=None):
self.items = list(items) if items is not None else []
g1 = BagGood()
g2 = BagGood()
g1.items.append("apple")
print("good:", g1.items, g2.items) # g2には影響しない
bad: ['apple'] ['apple']
good: ['apple'] []
初期化順序と依存関係の整理
ある属性が別の属性に依存する場合は、依存先を先に設定し、依存元は最後に計算します。
必要ならヘルパーメソッドに処理を分け、例外発生時でも一部だけ中途半端に設定されないように注意します。
class Rectangle:
def __init__(self, width: float, height: float):
self.width = self._validate_length(width)
self.height = self._validate_length(height)
self.area = self.width * self.height # 依存する計算は最後に
def _validate_length(self, v: float) -> float:
if v <= 0:
raise ValueError("widthとheightは正の数である必要があります")
return float(v)
r = Rectangle(3, 4)
print(r.area)
12.0
クラスの初期設定パターン: __init__の実践
入力値のバリデーション(ValueError TypeError)
型が違うときはTypeError、値の範囲が不正なときはValueErrorを上げるのが通例です。
class Person:
def __init__(self, name, age):
if not isinstance(name, str):
raise TypeError("nameはstrである必要があります")
if not isinstance(age, int):
raise TypeError("ageはintである必要があります")
if age < 0:
raise ValueError("ageは0以上である必要があります")
self.name = name
self.age = age
try:
Person("Alice", -1)
except Exception as e:
print(type(e).__name__, str(e))
ValueError ageは0以上である必要があります
設定値の正規化と前処理(文字列整形 数値変換)
初期化中に、表記ゆれや空白、単位の違いを揃えておくと後段が楽になります。
class Product:
def __init__(self, title: str, price):
title = title.strip() # 前後空白の削除
if not title:
raise ValueError("titleは空にできません")
self.title = title
# 価格は文字列でも数値でも受け取り、小数に正規化
try:
self.price = float(price)
except Exception as e:
raise TypeError("priceは数値に変換可能である必要があります") from e
p = Product(" Book ", "1200")
print(p.title, p.price)
Book 1200.0
計算済み属性の初期化(キャッシュ 初期計算)
高コストの計算結果をキャッシュしておくと、後のメソッド呼び出しが高速になります。
必要に応じて遅延初期化と使い分けます。
class SeriesStats:
def __init__(self, values: list[float]):
if not values:
raise ValueError("valuesは空にできません")
self.values = values
# ここでは小コストな集計だけ先に計算
self.count = len(values)
self.sum = sum(values)
self.mean = self.sum / self.count
s = SeriesStats([1, 2, 3, 4])
print(s.count, s.sum, s.mean)
4 10 2.5
外部設定の受け渡し(パス 接続情報 オプション)
外部の設定値や接続情報は、そのまま保持するだけでなく、存在確認や展開、参照しやすい形に整形しておくと実運用で堅牢になります。
from pathlib import Path
class DataStore:
def __init__(self, root_dir, **options):
self.root = Path(root_dir).expanduser().resolve()
if not self.root.exists():
raise ValueError(f"ディレクトリが存在しません: {self.root}")
# 任意オプション。既定値をマージしておく
defaults = {"readonly": False, "encoding": "utf-8"}
merged = {**defaults, **options}
self.readonly = merged["readonly"]
self.encoding = merged["encoding"]
store = DataStore("./", readonly=True)
print(store.root.is_dir(), store.readonly, store.encoding)
True True utf-8
__init__のよくあるエラーと対処法
TypeError(__init__() missing required positional argument)
必須引数を渡し忘れるとこのエラーになります。
エラーメッセージは何が欠けているかを具体的に教えてくれます。
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
try:
p = Point(10) # yを渡し忘れ
except TypeError as e:
print(e)
# 修正: 必須のyを渡す
p = Point(10, 20)
print(p.x, p.y)
Point.__init__() missing 1 required positional argument: 'y'
10 20
AttributeError(初期化漏れで属性が存在しない)
インスタンス変数を設定し忘れるとAttributeErrorになります。
例外経路でも属性が未設定にならないよう、先にデフォルトを入れてから検証するのも有効です。
class Session:
def __init__(self, token: str | None):
self.token = None # 先に存在だけ確保しておく
if not token:
raise ValueError("tokenが必要です")
self.token = token
try:
s = Session("") # 例外になるが、少なくとも属性は存在する設計
except ValueError as e:
print("ValueError:", e)
ValueError: tokenが必要です
変数のシャドーイングに注意(self.xとローカル変数)
ローカル変数と同名の属性を使うと、思わぬシャドーイングが起きます。
必ずself.を付けることを習慣化しましょう。
class Shadow:
def __init__(self, value):
value = value # 何も起きない。ローカルに代入しただけ
self.value = value # これが正しい
s = Shadow(5)
print(s.value)
5
重い初期化の回避と遅延初期化(lazy)
初期化時に重い処理(大きなファイル読み込み、ネットワーク接続など)を行うと、オブジェクト生成が遅くなります。
必要になるまで遅らせる「遅延初期化」を検討します。
class LazyLoader:
def __init__(self, path: str):
self.path = path
self._data = None # まだ読み込まない
def get_data(self):
if self._data is None:
print("[load] heavy loading happens here")
# 実際はファイルIOやネットワーク等
self._data = f"content of {self.path}"
return self._data
loader = LazyLoader("data.txt")
print("created")
print(loader.get_data()) # ここで初めて読み込み
print(loader.get_data()) # 2回目以降はキャッシュ利用
created
[load] heavy loading happens here
content of data.txt
content of data.txt
まとめ
__init__は、オブジェクトの成立を保証し扱いやすくするための「初期化ポイント」です。
必須とオプションの引数設計、ミュータブルなデフォルト引数を避けること、バリデーションと正規化、計算済み属性や遅延初期化の使い分けを押さえると、堅牢で読みやすいクラスになります。
分かりづらいエラーの多くはself.属性の付け忘れや引数の渡し忘れから生じます。
今回のパターンとサンプルを雛形として、自分のドメインに合わせた初期化ロジックを丁寧に組み立ててみてください。
