Pythonのクラス構文
Javaのクラス構文との対比
PythonとJavaでは、クラスを定義する際の文法やオブジェクトの構造が大きく異なります。
Javaではクラスの中にメンバー変数をあらかじめ宣言する必要があります。
これに対し、Pythonではクラスの内部でメンバー変数を事前に宣言する必要はありません。
Pythonでは、コンストラクタである __init__ メソッドなどの内部で、self.属性名 に値を代入することで、動的にメンバー変数が追加されます。
また、Javaで「フィールド」と呼ばれるものは、Pythonでは属性と呼ぶのが一般的です。
同様に、Javaの「メソッド」はPythonでもメソッドと呼ばれます。
JavaとPythonのクラス定義の基本的な違いを比較するために、二次元の座標を表す Point クラスを考えましょう。
Javaで Point クラスを定義するプログラムは以下のようになります。
Point.java
public class Point {
public int x;
public int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
これと同様のクラスをPythonで定義するプログラムは以下のようになります。
point.py
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
point = Point(10, 20)
print(point.x)
print(point.y)
プログラムの実行結果は以下のようになります。
実行結果
10
20
Pythonでは self という特殊な引数を用いて、インスタンス自身を指し示します。
コンストラクタである __init__ の中で self.x や self.y に値を代入した時点で、初めてオブジェクトに属性が追加されます。
メソッドの定義とクラスの継承
Pythonでクラスの中にメソッドを定義する際は、関数と同様に def キーワードを使用します。
インスタンスメソッドを定義する場合、第一引数には必ず自分自身を指す self を受け取る必要があります。
オブジェクトを生成した後にメソッドを呼び出すときは、第一引数の self にはインスタンス自身が自動的に渡されます。
そのため、呼び出し側の引数には self に対応する値を明示的に記述する必要はありません。
また、既存のクラスを基にして新しいクラスを作成する継承を行うには、クラス名の後ろの括弧の中に親クラスの名前を指定します。
これはJavaにおける extends キーワードを用いたクラスの継承に相当します。
メソッドの定義とクラスの継承を組み合わせたプログラムは以下のようになります。
inheritance.py
class Animal:
def __init__(self, name):
self.name = name
def make_sound(self):
print("音を出します")
class Dog(Animal):
def make_sound(self):
print(f"{self.name}がワンワンと鳴きます")
dog = Dog("ポチ")
dog.make_sound()
プログラムの実行結果は以下のようになります。
実行結果
ポチがワンワンと鳴きます
このように、class Dog(Animal): と記述することで Animal クラスを継承できます。
また、親クラスと同じ名前のメソッドを子クラスで定義し直すことで、メソッドの挙動を上書きするオーバーライドが実現できます。
ネームマングリングとカプセル化
Javaでは private 修飾子を用いることで、クラスの外部からフィールドへの直接アクセスを厳密に禁止できます。
しかし、Pythonには外部からのアクセスを厳密に制限するようなアクセス修飾子は存在しません。
Pythonでは、すべての属性やメソッドが原則として外部からアクセス可能な公開された状態となります。
外部から直接アクセスされたくない属性がある場合、Pythonではアンダースコア1つ(_)で始まる名前を付けるのが慣例となっています。
これは開発者間の「直接アクセスしない」という合意に基づくマナーであり、言語機能としてアクセスを制限するものではありません。
一方で、アンダースコア2つ(__)で始まる名前を付けた属性には、ネームマングリングと呼ばれる仕組みが適用されます。
ネームマングリングが適用されると、その属性名は自動的に _クラス名__属性名 という名前に変換されます。
ネームマングリングの仕組みを確認するプログラムは以下のようになります。
mangling.py
class Point:
def __init__(self, x, y):
self.__x = x
self.__y = y
point = Point(10, 20)
try:
print(point.__x)
except AttributeError as e:
print(e)
print(point._Point__x)
プログラムの実行結果は以下のようになります。
実行結果
'Point' object has no attribute '__x'
10
このように、point.__x という名前で直接アクセスしようとするとエラーが発生します。
しかし、変換後の名前である point._Point__x を指定すると、外部からでも値を取得できてしまいます。
このことから、PythonではJavaのような厳密なカプセル化は不可能であることがわかります。
Pythonではカプセル化を言語の強制力に頼るのではなく、開発者がルールを尊重し合うことで実現しています。
propertyによるカプセル化
PythonではJavaのようにゲッターやセッターメソッドを手動で定義することもできます。
しかし、すべての属性に対して手動でゲッターやセッターを定義すると、コードが冗長になり読みづらくなります。
Pythonでは、@property デコレータを用いることで、属性アクセスの簡潔な見た目を保ちながら、値のバリデーションや読み取り専用といったカプセル化を実現できます。
ゲッターとセッターを手動で定義した素朴なプログラムは以下のようになります。
point_getter_setter.py
class Point:
def __init__(self, x, y):
self._x = x
self._y = y
def get_x(self):
return self._x
def set_x(self, x):
if x < 0:
raise ValueError("座標は0以上でなければなりません")
self._x = x
point = Point(10, 20)
point.set_x(15)
print(point.get_x())
プログラムの実行結果は以下のようになります。
実行結果
15
このコードは動作しますが、値の取得や設定に毎回メソッド呼び出しの括弧を書く必要があり、Pythonらしい簡潔さに欠けます。
@property を用いて、属性のようにアクセスしつつバリデーションを行うように改善したプログラムは以下のようになります。
point_property.py
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
@property
def x(self):
return self._x
@x.setter
def x(self, value):
if value < 0:
raise ValueError("座標は0以上でなければなりません")
self._x = value
@property
def y(self):
return self._y
@y.setter
def y(self, value):
if value < 0:
raise ValueError("座標は0以上でなければなりません")
self._y = value
point = Point(10, 20)
point.x = 15
print(point.x)
try:
point.x = -5
except ValueError as e:
print(e)
プログラムの実行結果は以下のようになります。
実行結果
15
座標は0以上でなければなりません
このように、@property を使用すると、外部からは point.x という通常の属性アクセスと同じ見た目で、ゲッターやセッターのロジックを実行できます。
また、コンストラクタの内部でも self.x = x とすることで、初期化時にも自動的にセッターのバリデーションが適用されます。
演習問題
演習1
HTTPレスポンスを表現する Response クラスを定義しなさい。
コンストラクタ __init__(self, status_code, body) はステータスコードとレスポンスボディを受け取り、それぞれを属性に設定するものとします。
また、ステータスコードとボディを表示する print_info(self) メソッドを実装しなさい。
演習2
Response クラスの status_code 属性を @property デコレータを使用してカプセル化しなさい。
セッターメソッドにおいて、値が100から599の範囲外である場合は ValueError を発生させるバリデーションを実装すること。
また、ステータスコードが200以上300未満の場合に True、それ以外の場合に False を返す読み取り専用の is_success プロパティを追加しなさい。
演習3
Response クラスを継承した JsonResponse クラスを作成しなさい。
コンストラクタでステータスコードとJSON形式の文字列を受け取り、親クラスのコンストラクタを呼び出すものとします。
また、to_dict(self) メソッドを定義し、標準ライブラリの json モジュールを使用してレスポンスボディをデコードし、辞書型として返すようにしなさい。
解答例
演習問題のプログラムの実装例は以下の通りである。
実装の方針として、まず @property を用いて Response クラスのステータスコード属性に対するバリデーションおよび成功判定のロジックをカプセル化する。
次に Response クラスを継承した JsonResponse クラスを定義し、親クラスの初期化処理を呼び出すとともに、JSON文字列のデコード処理を追加する。
最後にそれぞれのクラスをインスタンス化し、正常な値での初期化や例外の発生、およびJSONの辞書変換が正しく機能することを確認する。
http_response.py
import json
class Response:
def __init__(self, status_code, body):
self.status_code = status_code
self.body = body
@property
def status_code(self):
return self._status_code
@status_code.setter
def status_code(self, value):
if not (100 <= value <= 599):
raise ValueError("ステータスコードは100から599の間でなければなりません")
self._status_code = value
@property
def is_success(self):
return 200 <= self.status_code < 300
def print_info(self):
print(f"Status: {self.status_code}, Body: {self.body}")
class JsonResponse(Response):
def __init__(self, status_code, json_string):
super().__init__(status_code, json_string)
def to_dict(self):
return json.loads(self.body)
# 動作検証
response = Response(200, "OK")
response.print_info()
print(f"Success: {response.is_success}")
json_response = JsonResponse(201, '{"message": "Created", "id": 1}')
json_response.print_info()
print(f"Success: {json_response.is_success}")
print(f"Data: {json_response.to_dict()}")
try:
invalid_response = Response(99, "Invalid")
except ValueError as e:
print(e)
プログラムの実行結果は以下のようになります。
実行結果
Status: 200, Body: OK
Success: True
Status: 201, Body: {"message": "Created", "id": 1}
Success: True
Data: {'message': 'Created', 'id': 1}
ステータスコードは100から599の間でなければなりません