13. クラス#

クラスは変数(メンバ変数)と関数(メソッド)をまとめて管理できるオブジェクトの型を定義する。

13.1. インスタンス化#

以下の例では2次元上の点を表すクラスPointを定義している。Pointクラスのインスタンスのメンバ変数としてxyを定義し、それぞれ0.に初期化している。

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

クラスをインスタンス化してPointオブジェクトを作成する。このとき、作成されたオブジェクトを引数として__init__関数が呼び出される。__init__関数を定義するときに、最初の引数の変数名をselfにするのはお約束。そのオブジェクト(self変数で参照される)に対して.演算子を適用することで、メンバ変数xy0.に初期化している。

p = Point()

pの型はPoint型である。

p
<__main__.Point at 0x7ff5083c1250>
type(p)
__main__.Point

インスタンスのメンバ変数には.演算子でアクセスできる。

p.x
0.0
p.y
0.0

メンバ変数の値をクラスの外からでも変更できる(基本的にPythonのメンバ変数はpublicである)。

p.x = 2.
p.x
2.0

インスタンスのメンバ変数をクラスの外からでも宣言・代入できる。

p.a = 3
p.a
3

(当たり前ではあるが)Pointクラスを新しくインスタンス化して別のオブジェクトを作成しても、既に作成済みのオブジェクトに影響を与えることはない。

q = Point()
p.x
2.0
q.x
0.0

__init__関数で引数を受け取り、メンバ変数を初期化することも可能。

class Point:
    def __init__(self, x=0., y=0.):
        self.x = x
        self.y = y
p = Point(1, 2)
p.x
1
p.y
2

__init__関数の引数にデフォルト値を設定しておいたので、引数を省略しインスタンスを作成しても構わない。

q = Point()
q.x
0.0

13.2. メソッド#

インスタンスのメンバ変数に対する操作などをメンバ関数(メソッド)として記述できる。メンバ関数の第1引数としてインスタンスが渡されるため、第1引数をselfという変数名にするのが慣例。

class Point:
    def __init__(self, x=0., y=0.):
        self.x = x
        self.y = y
        
    def set(self, x, y):
        self.x = x
        self.y = y
        
    def transpose(self):
        self.x, self.y = self.y, self.x
        
    def hamming(self):
        return self.x + self.y
    
    def dot(self, other):
        return self.x * other.x + self.y * other.y

Pointクラスをインスタンス化

p = Point()
p.x
0.0
p.y
0.0

Pointクラスのインスタンスpのメンバ関数を呼び出す。

p.set(1, 2)
p.x
1
p.y
2

上のメンバ関数の呼び出しは、以下の呼び出しと等価(第1引数がselfとなる理由が分かる)。

Point.set(p, 2, 3)
p.x
2
p.y
3

メンバ関数の中でメンバ変数の値を変更する例。

p.transpose()
p.x
3
p.y
2

メンバ関数が値を返す例。

p.hamming()
5

メンバ関数の引数として別のオブジェクトを渡す例。

p = Point(2, 3)
q = Point(5, 4)
p.dot(q)
22

あるオブジェクトがどのようなメンバ変数・関数を持っているのか確認するには、dir関数を用いる。

dir(p)
['__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__le__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__',
 'dot',
 'hamming',
 'set',
 'transpose',
 'x',
 'y']

13.3. 継承#

あるクラスをベースとしてメンバ変数・関数を追加し、新しいクラスを定義することを継承と呼ぶ。2次元の点を表すクラスPointをベースに、3次元の点を表すクラスPoint3Dを作成する。このとき、Pointクラスは親クラスと呼ばれる。以下の例におけるsuper().__init__は、Point3Dクラスから見て親クラスであるPointクラスの__init__関数を呼び出している。

class Point3D(Point):
    def __init__(self, x=0., y=0., z=0.):
        super().__init__(x, y)
        self.z = z

3次元の点を表すインスタンスを作成する。z軸の値を格納できるようになっている。

p = Point3D(3, 4, 2)
p.x
3
p.y
4
p.z
2

ところが、hammingメンバ関数を呼び出しても、期待する結果(9)が得られない。

p.hamming()
7

これは、Point3Dクラスではhammingメンバ関数が明示的には実装されていないため、Pointクラスのhammingメンバ関数が呼び出され、x軸とy軸の値しか使われていないためである。

p.hamming
<bound method Point.hamming of <__main__.Point3D object at 0x7ff508321e80>>

z軸の値も考慮してハミング距離を求めるには、Point3Dクラスにおいてhamming関数を上書きする必要がある。

class Point3D(Point):
    def __init__(self, x=0., y=0., z=0.):
        super().__init__(x, y)
        self.z = z
        
    def hamming(self):
        return self.x + self.y + self.z
p = Point3D(3, 4, 2)
p.hamming()
9

以上の変更により、Point3Dクラスのhammingメンバ関数が呼び出されることが確認できる。

p.hamming
<bound method Point3D.hamming of <__main__.Point3D object at 0x7ff5083211c0>>

13.4. 特殊メソッド#

自分で定義したクラスのオブジェクトに対して、+演算子の動作やstr関数を呼び出したときの動作を特殊メソッドに記述できる。以下は2次元の点を表現するPointクラスの特殊メソッドを実装する例。

class Point:
    """ Two-dimensional vector. """
    
    def __init__(self, x=0., y=0.):
        self.x = x
        self.y = y
    
    def __str__(self):
        """ str(p) """
        return f'({self.x}, {self.y})'

    def __repr__(self):
        """ repr(p) """
        return f'Point({self.x}, {self.y})'
    
    def __add__(self, other):
        """ self + other """
        return Point(self.x + other.x, self.y + other.y)
    
    def __sub__(self, other):
        """ self - other """
        return Point(self.x - other.x, self.y - other.y)
    
    def __mul__(self, other):
        """ self * other """
        return Point(self.x * other.x, self.y * other.y)

    def __rmul__(self, other):
        """ other * self """
        return Point(self.x * other, self.y * other)

    def __truediv__(self, other):
        """ self / other """
        return Point(self.x / other.x, self.y / other.y)

    def __floordiv__(self, other):
        """ self // other """
        return Point(self.x // other.x, self.y // other.y)
    
    def __eq__(self, other):
        """ self == other """
        return self.x == other.x and self.y == other.y

    def __ne__(self, other):
        """ self != other """
        return self.x != other.x or self.y != other.y
    
    def __lt__(self, other):
        """ self < other """
        return self.x < other.x and self.y < other.y
    
    def __le__(self, other):
        """ self <= other """
        return self.x <= other.x and self.y <= other.y
    
    def __gt__(self, other):
        """ self > other """
        return self.x > other.x and self.y > other.y
    
    def __ge__(self, other):
        """ self >= other """
        return self.x >= other.x and self.y >= other.y

    def set(self, x, y):
        self.x = x
        self.y = y

    def transpose(self):
        self.x, self.y = self.y, self.x
        
    def hamming(self):
        return self.x + self.y
    
    def dot(self, other):
        return self.x * other.x + self.y * other.y

Pointクラスのインスタンスp1p2を作成。

p1 = Point(2, 3)
p2 = Point(5, 6)

+, -, *, /演算子を使うと、__add__, __sub__, __mul__または__rmul__, __truediv__が呼び出される。

p1 + p2
Point(7, 9)
p2 - p1
Point(3, 3)
p1 * p2
Point(10, 18)
3 * p1
Point(6, 9)
p2 / p1
Point(2.5, 2.0)

==, !=, <, <=, >, >=演算子を使うと、__eq__, __ne__, __lt__, __le__, __gt__, __ge__が呼び出される。

p1 == p2
False
p1 != p2
True
p1 < p2
True
p1 <= p2
True
p1 > p2
False
p1 >= p2
False

str関数はオブジェクトを文字列として読みやすい形式で表現する(ことを想定しているため、そのようになるように__str__関数を実装しておいた)。

str(p1)
'(2, 3)'

repr関数はオブジェクトをプログラムとして実行可能な表現に変換する(ことを想定しているため、そのようになるように__repr__関数を実装しておいた)。

repr(p1)
'Point(2, 3)'

オブジェクトを単に評価させたときにはrepr関数が呼び出される。

p1
Point(2, 3)

repr関数が返す文字列はそのままPythonのプログラムとして評価できることを想定しているため、例えばeval関数でプログラムとして実行できるようにしておくとよい。

eval(repr(p1)) + p2
Point(7, 9)