Python/クラス
クラスとインスタンス[編集]
クラス(Classes)は、オブジェクト指向ブログラミング言語の分類の1つ「クラスベースのオブジェクト指向ブログラミング言語」[1]の対象となる概念です。 Pythonもクラスベースのオブジェクト指向ブログラミング言語です。
クラスは、データや機能の集まりを提供する手段です。 新しいクラスを定義すると、そのクラスの新しいインスタンスを作成することができるようになります。 クラスの各インスタンスは、その状態を維持するために属性をもつことができます。 クラスのインスタンスは、属性を参照したり変更したりするためのメソッドを持つこともできます。
クラスの例[編集]
クラスと属性[編集]
class MyClass: """ My first class """ pass aInstance = MyClass() aInstance.i = 123 aInstance.s = "abc" print("__doc__ =", MyClass.__doc__) print("aInstance.i =", aInstance.i) print("aInstance.s =", aInstance.s)
- 実行結果
__doc__ = My first class aInstance.i = 123 aInstance.s = abc
- クラス MyClass の定義の開始です。クラス名と : の間にスーパークラスのリストがないので、暗黙にクラス階層の頂点 object が継承されます
- クラス定義の最初に出てくる文字列リテラルは、docstring です
- 特に、定義するものがないので
pass
- 変数 aInstance をクラス
MyClass
のインスタンスで初期化 - インスタンス aInstance の属性 i に、数値 123 を代入
- 存在しない属性への代入は、新たな属性を生成します
- これは、属性のスペルをミスすると既存の属性の値を変更したつもりが、新たな属性を生成してしまう発見困難なバグの原因になりがちです
- 存在しない属性への代入は、新たな属性を生成します
- インスタンス aInstance の属性 s に、文字列 "123" を代入
- docstring を、特殊属性
__doc__
を使って参照 - インスタンス aInstance の属性 i の値を
aInstance.i
で参照 - インスタンス aInstance の属性 s の値を
aInstance.s
で参照
__init__ メソッド[編集]
- __init__メソッドの例
class Car: """ My second class """ def __init__(self, owner, colour, number): self.owner = owner self.colour = colour self.number = number a = Car("山田", "blue", 1234) b = Car(colour = "red", number = 3355, owner = "伊藤") print(f"""\ {a.colour=} {b.number=} {a.owner=} {Car.__doc__=} """)
- 実行結果
a.colour='blue' b.number=3355 a.owner='山田' Car.__doc__=' My first class '
- クラス Car の定義の開始
- docstring
- __init__ は、特殊メソッドの1つです。__init__ 限らず、メソッドの第一引数は self は対象のインスタンスです。
- __init__の仮引数 owner をインスタンスの属性 owner に代入します。"山田"や"伊藤"などの車両の所有者です。
- __init__の仮引数 colour をインスタンスの属性 colour に代入します。"blue"や"red"などの車体の色です。
- __init__の仮引数 number をインスタンスの属性 number に代入します。1234や3355などの車体のナンバーです。
- "山田", "blue", 1234 を引数にクラス Car をインスタンス化し、変数aを初期化。
- colour = "red", number = 3355, owner = "伊藤" を引数にクラス Car をインスタンス化し、変数bを初期化。
- 後者は、キーワード引数を使っており、ドキュメント性が高いとともに、引数が増えた場合にも対応できます。
- Fヒアドキュメント と { 式 =} の組合わせは動作を確認するために役立ちます。
- インスタンスaのデータ属性colourには a.colour で参照できます。
- インスタンスbのデータ属性numberには b.number で参照できます。
- インスタンスaのデータ属性ownerには a.owner で参照できます。
- クラスCarのdocstringには Car.__doc__ で参照できます。
メソッド[編集]
クラスにはインスタンスの持つ属性として関数も持つことができます。この関数属性のことをメソッドと言います。
メソッドを定義するには、関数を定義したようにクラス定義中にdef
で定義します。メソッドの第一引数はself
で、インスタンス自身です。
- コード例
class Car: """ My first class """ def __init__(self, owner, colour, number): self.owner = owner self.colour = colour self.number = number def get_colour(self): return self.colour def set_colour(self, new_colour): self.colour = new_colour a = Car("山田", "blue", 1234) print(f"{a.get_colour()=}") a.set_colour("yellow") print(f"{a.get_colour()=}")
- 実行結果
a.colour='blue' a.colour='yellow'
- __init__ もメソッドですが、インスタンス化のときに暗黙に呼ばれることが特別です。
- get_colour 色を返すメソッド
- set_colour 色を変えるメソッド
ゲッター・セッターとプライベート属性[編集]
前節の Car.get_colour() のようなクラスの属性の値を返す関数をゲッター、Car.set_colour() のようなクラスの属性の値を返す関数をセッターと呼びます。
プライベート属性[編集]
ゲッター・セッターは、クラスの実装の内部構造隠蔽を目的としていますが、上記のコードでは、
- データ属性を直接参照
a = Car("山田", "blue", 1234) print(f"{a.colour=}") a.colour = "yellow" print(f"{a.colour=}")
- 実行結果
a.colour='blue' a.colour='yellow'
- の様にデータ属性を直接参照できてしまい、内部構造の隠蔽は果たせていません。
このような場合はクラスプライベートな識別子を使います。 クラスプライベートな識別子は特別な構文があるわけではなく、属性の最初の2文字を '__' にするだけです。
- プライベートの使用例
class Car: """ My first class """ def __init__(self, owner, colour, number): self.owner = owner self.__colour = colour self.number = number def get_colour(self): return self.__colour def set_colour(self, new_colour): self.__colour = new_colour a = Car("山田", "blue", 1234) print(f"{a.get_colour()=}") a.set_colour("yellow") print(f"{a.get_colour()=}") print(f"{a.owner=}") # print(f"{a.__colour=}") """ Traceback (most recent call last): File "Main.py", line 20, in <module> print(f"{a.__colour=}") AttributeError: 'Car' object has no attribute '__colour' """
- 実行結果
a.get_colour()='blue' a.get_colour()='yellow' a.owner='山田'
これで、ゲッター・セッターを経由しなければ、__colour を参照できなくなりました。 ゲッター・セッターには、値の変換や型や範囲のチェックなど、追加の価値を付加することができます。
@property と @*.setter[編集]
ゲッター・セッターで、内部構造の隠蔽は果たせましたが、セッター・ゲッターはメソッドなので値の参照や変更(代入など)のたびにメソッド呼出しが必要で、コードの量が増えると負担に感じてきます。 そこで、@property と @*.setter を適用すると、通常の参照や変更の構文を使えるようになります。
- @property と @*.setterの使用例
class Car: """ My first class """ def __init__(self, owner, colour, number): self.owner = owner self.__colour = colour self.number = number @property def colour(self): return self.__colour @colour.setter def colour(self, new_colour): self.__colour = new_colour a = Car("山田", "blue", 1234) print(f"{a.colour=}") a.colour = "yellow" print(f"{a.colour=}")
- 実行結果
aa.colour='blue' a.colour='yellow'
- 一見すると、.colour と言うデータ属性を参照しているようですが、実際は(隠れた)セッター・ゲッターを経由してデータ属性 .__colour を参照しています。
@property や @colour.setter はPythonのキーワードではなく、デコレーターという仕組みを使っています。
特殊メソッド[編集]
特殊メソッド(special method;マジックメソッド magic method とも)は、加算などの型に対する特定の操作を実行するためにPythonから暗黙的に呼び出されるメソッド。 このようなメソッドは、ダブルアンダースコアで始まり、ダブルアンダースコアで終わる名前を持っています。
- コード例
class Car: """ My first class """ def __init__(self, owner, colour, number): self.owner = owner self.colour = colour self.number = number def __str__(self): return ",".join([f"{k}:{repr(v)}" for k, v in self.__dict__.items()]) a = Car("山田", "blue", 1234) print(a)
- 実行結果
owner:'山田',colour:'blue',number:1234
- ここでは __str__ を定義して str() をオーバーライドしていますが、明示的に str() を呼び出しているのではなく、組込み関数 print() のディフォルト変換にキックされています。
メソッド・チェイン[編集]
Pythonのコミュニティは、メソッドがNoneを返すことを推すなどメソッド・チェインには否定的ですが[2]、クラス×メソッドの仕組みはメソッド・チェインと親和性が高いです。
- メソッド・チェイン対応クラス
from itertools import tee class Chainer(object): def __init__(self, iterator): self.iterator = iterator def map(self, func): return Chainer((func(x) for x in self.iterator)) def filter(self, func): return Chainer((x for x in self.iterator if func(x))) def list(self): return list(self.__iter__()) def tuple(self): return tuple(self.__iter__()) def __iter__(self): x, self.iterator = tee(self.iterator) return x a = (Chainer([2,1,15,-4,-5,-12]) .map(lambda x: x * 2) .filter(lambda x: x > 0)) print(list(a)) print(tuple(a)) print(a.list()) print(a.tuple()) def reduceFunc(self, *args): func = args[0] value = 0 if (len(args) >= 2) : value = args[1] for i in self.iterator : value = func(value, i) return value Chainer.reduce = reduceFunc print(a.reduce(lambda x,y: x+y))
- 実行結果
[4, 2, 30] (4, 2, 30) [4, 2, 30] (4, 2, 30) 36
- __iter__ は、iter() イテレータをオーバーライドします。
- メソッド・チェインは素のままでは改行できませんが、括弧 ( ) で括れば複数行に渡って記述することができます。
Ruby からの移植[編集]
Ruby#ユーザー定義クラスをPython3に移植しました。
- Ruby からの移植
import math class GeoCoord(object): def __init__(self, longitude, latitude): self.longitude, self.latitude = longitude, latitude def __str__(self): ew, ns = "東経", "北緯" long, lat = self.longitude, self.latitude if long < 0.0: ew = "西経" long = -long if lat < 0.0: ns = "南緯" lat = -lat return f"({ew}: {long}, {ns}: {lat})" def distance(self, other): i = math.pi / 180 r = 6371.008 return ( math.acos( math.sin(self.latitude * i) * math.sin(other.latitude * i) + math.cos(self.latitude * i) * math.cos(other.latitude * i) * math.cos(self.longitude * i - other.longitude * i) ) * r ) Sites = { "東京駅": [139.7673068, 35.6809591], "シドニー・オペラハウス": [151.215278, -33.856778], "グリニッジ天文台": [-0.0014, 51.4778], } for name in Sites: Sites[name] = GeoCoord(*Sites[name]) for name in Sites: print(f"{name}: {Sites[name]}") keys, len = tuple(Sites.keys()), len(Sites) for i in range(len): x, y = keys[i], keys[(i + 1) % len] print(f"{x} - {y}: {Sites[x].distance(Sites[y])} [km]")
- 実行結果
東京駅: (東経: 139.7673068, 北緯: 35.6809591) シドニー・オペラハウス: (東経: 151.215278, 南緯: 33.856778) グリニッジ天文台: (西経: 0.0014, 北緯: 51.4778) 東京駅 - シドニー・オペラハウス: 7823.269299386704 [km] シドニー・オペラハウス - グリニッジ天文台: 16987.2708377249 [km] グリニッジ天文台 - 東京駅: 9560.546566490015 [km]
- 内包表記などを使えば、もう少し「Pythonぽい」プログラムに出来たと思いますが、なるべく一対一に移植元と対応するよう実装しました。
- end が要らないのは簡便な反面、どこで定義が終わっているか判りづらいという問題もあり、__doc__ を充実させるなど工夫が必要そうです。
- 型アノテーションは付けていません。
クラス変数[編集]
ここまでは、インスタンスの属性だけ扱って来ましたが、インスタンスではなくクラスに属する属性があります。 これをクラス変数と呼びます。
class MyClass: count = 0 a = MyClass() b = MyClass() # print(type(a)) print( f"""\ {MyClass.count=} {a.count=} {b.count=} """ ) MyClass.count = 1 print( f"""MyClass.count = 1 {MyClass.count=} {a.count=} {b.count=} """ ) a.count = 10 print( f"""a.count = 10 {MyClass.count=} {a.count=} {b.count=} """ ) a.__class__.count = 20 print( f"""a.__class__.count = 20 {MyClass.count=} {a.count=} {b.count=} """ ) del a.count print( f"""del(a.count) {MyClass.count=} {a.count=} {b.count=} """ )
- 実行結果
出力 入力 コメント 0 MyClass.count=0 a.count=0 b.count=0 MyClass.count = 1 MyClass.count=1 a.count=1 b.count=1 a.count = 10 MyClass.count=1 a.count=10 b.count=1 a.__class__.count = 20 MyClass.count=20 a.count=10 b.count=20 del(a.count) MyClass.count=20 a.count=20 b.count=20
- クラス変数 count だけ属性にしたクラス MyClass を定義
- クラス変数 count をゼロで初期化
- クラス MyClass のインスタンスで変数 a を初期化
- クラス MyClass のインスタンスで変数 b を初期化
- 初期状態では、MyClass.count も a.count も b.count もゼロ
MyClass.count += 1
の後は、MyClass.count も a.count も b.count も 1a.count += 1
の後は、a.count だけが 10、 あとの MyClass.count と b.count も 1 のまま- インスタンス a に、属性 count(インスタンス変数)が生えてきて、MyClassの属性 count(クラス変数)を隠してしまった。
a.__class__.count = 20
の後は、a.count だけが 10、 あとの MyClass.count と b.count は 20 のまま- インスタンス a をつかって、__class__特殊属性を使って count(クラス変数)を変更しても、インスタンス変数がクラス変数を隠してている状況は変わらず。
- 組込み関数
del(a.count)
の後は、MyClass.count も a.count も b.count も 20- インスタンス a に、属性 count(インスタンス変数)を削除した結果、隠れていた MyClassの属性 count(クラス変数)を、a.count で参照できるようになった。
このように、クラス変数は参照と同じ構文で変更(左辺値化)すると、新しくインスタンス変数は生えてきてしまい、発見困難なバグの原因になります。
では、毎回 クラス名 . 属性
の構文を使えば良いようですが、クラスが継承された場合に基底クラスの属性か派生クラスの属性かで齟齬が生じるので、(特にメソッドでは)__class__ を使って参照・変更を行うべきです。
- まとめ
- クラス変数は
クラス . 属性
インスタンス . 属性
- の2つの参照方法があります。
インスタンス . 属性
の構文で、もしクラス変数とインスタンス変数の名前が重複していた場合、インスタンス変数が優先だれます。名前が重複した場合にクラス変数にアクセスしたい時は、インスタンス . __class__ .属性
の様に、特殊属性__class__
を使います。
脚註[編集]
- ^ クラスベースのオブジェクト指向ブログラミング言語の他に、プロトタイプベースのオブジェクト指向ブログラミング言語があり、Self、JavaScript、Luaが代表的なものです。
- ^ “5.1. More on Lists” (2021年11月29日). 2021年11月29日閲覧。