6-3. クラス

Pythonにおけるオブジェクト指向プログラミングのうち、クラスを定義する方法について簡単に説明します。

参考

クラス定義

Pythonでは全てのデータはオブジェクトなのですが、 以下では特に、クラス定義によって作成されたクラスを型とするデータを扱います。 このようなデータは、オブジェクト指向プログラミングにおける典型的なオブジェクトです。 そこで以下では、オブジェクトという用語をもっぱら使います。

4-1で見たように、ファイルオブジェクトに対して readline() というメソッドを呼び出すと、 ファイルの行が文字列として次々と返されます。 ここでは、ファイルオブジェクトのようなオブジェクトで、 readline() というメソッドが呼び出されると、常に 'Hello.\n' という文字列を返すようなものを作ってみましょう。

そのためには、新しいクラスを定義します。 クラスとは、オブジェクトの種類を意味します。 新しいクラスを定義すると、そのクラスに属するオブジェクトを作ることができるようになります。 それらのオブジェクトの型は、その新しいクラスになります。

ここでは、ずっと 'Hello.\n' を返し続けるので、 HelloForEver という名前を持つクラスを定義しましょう。 そして、HelloForEver というクラスを型とするオブジェクトを作ります。

[1]:
class HelloForEver:
    def readline(self):
        return 'Hello.\n'

一般にクラス定義は、以下のような形をしています。

class クラス名:
    def メソッド名(self, 引数, ...):
        実行文
    def メソッド名(self, 引数, ...):
        実行文
    ...

メソッド定義は関数定義と同じ形をしていますが、 クラス定義の中に入っています。 メソッド定義において、その最初の引数には慣例として self という名前を付けます。 この引数には、メソッドが呼び出されたオブジェクト自身が渡されます。

上の例では、readline というメソッドが1つ定義されています。

以下のようにして、このクラスのオブジェクトを作ることができます。

[2]:
f = HelloForEver()

HelloForEver を型とする新しいオブジェクトが作られて変数 f の値となります。

一般に、オブジェクトの生成は、

クラス名(式, ...)

という式で行います。このようにオブジェクトを生成する式はコンストラクタと呼ばれます。 なお、上の例では、括弧の中に式は1つもありません。

このようにして作ったオブジェクトの型を確認してください。

[3]:
type(f)
[3]:
__main__.HelloForEver

__main__.HelloForEver と表示されたでしょう。 __main__ は、ノートブックの式が評価されているモジュールを指すので、 このオブジェクトの型が、上で定義した HelloForEver クラスであることがわかります。 クラスのコンストラクタによって生成されたオブジェクトを、そのクラスのインスタンスと言います。 上のオブジェクトは HelloForEver クラスのインスタンスです。

オブジェクトそのものは以下のように表示されます。

[4]:
f
[4]:
<__main__.HelloForEver at 0x10f459820>

このオブジェクトに対して、readline というメソッドを呼び出すことができます。

[5]:
f.readline()
[5]:
'Hello.\n'

この例では、f という変数に入っているオブジェクトが self という引数に渡されて、 readline の本体である以下の文が実行されました。

return 'Hello.\n'

(この例では self は参照されていません。)

何回やっても同じです。

[6]:
f.readline()
[6]:
'Hello.\n'
[7]:
f.readline()
[7]:
'Hello.\n'

初期化と属性

以下の例では、初期化のメソッドが定義され、オブジェクトに属性が与えられます。

初期化のメソッドは __init__ という名前を持ち、 オブジェクトが作られたときに自動的に呼び出されます。 __init__ の引数は、オブジェクト自身と、クラス名の後に与えられる式の値です。

[8]:
class HelloFile:
    def __init__(self, n):
        self.n = n
    def readline(self):
        if self.n == 0:
            return ''
        self.n = self.n - 1
        return 'Hello.\n'

この例では、以下のようにしてオブジェクトが作られます。

[9]:
f = HelloFile(3)

すると、HelloFile を型とする新しいオブジェクトが作られて、 そのオブジェクト自身が self に、3n に渡されて、 self.n = n という文が実行されます。

self.n という式は、このオブジェクトの n という名前の属性を表します。

一般に、class の構文によって定義されたクラスを型とするオブジェクトは、 属性を持つことができます。 属性とは、個々のオブジェクトごとに記録される値であり、 オブジェクト内の変数と考えられます。 オブジェクトの属性は、オブジェクトに対してその属性名を指定して、参照したり設定したりできます。 オブジェクトの属性は、self.属性名 という式で参照されます。 self.属性名 を代入文の左辺に書けば、属性を設定することができます。

self.n = n のうち、self. の次の n は属性を表し、 右辺の n は、__init__ メソッドの引数を表していますので、 混同しないようにしてください。

この例では、新しく作られたオブジェクトの n という属性が、引数 n の値である 3 に設定されます。

readline メソッドは以下のように定義されています。

def readline(self):
    if self.n == 0:
        return ''
    self.n = self.n - 1
    return 'Hello.\n'

オブジェクトの属性 n を参照して、それが 0 ならば空文字列を返します。 そうでなければ、属性 n1 減らしてから文字列 'Hello.\n' を返します。

[10]:
f.readline()
[10]:
'Hello.\n'
[11]:
f.readline()
[11]:
'Hello.\n'
[12]:
f.readline()
[12]:
'Hello.\n'
[13]:
f.readline()
[13]:
''

変数 f の値であるオブジェクトの属性 n は、f.n という式によって参照できます。

[14]:
f.n
[14]:
0

ここでは詳しく説明しませんが、オブジェクトのメソッドも属性の一種です。

継承

継承は、既存のクラスをもとにして、変更部分だけを与えることにより、 新たなクラスを定義する機能です。

以下の例では、HelloForEver をもとにして HelloFile を定義しています。 一般に、新しく定義されるクラスを子クラス、そのもとになるクラスを親クラスと言います。

[15]:
class HelloFile(HelloForEver):
    def __init__(self, n):
        self.n = n
    def readline(self):
        if self.n == 0:
            return ''
        self.n = self.n - 1
        return super().readline()

ここでは、__init__readline を新たに定義しています。

HelloForEver にも readline があります。 こちらの readline は、super().readline() という式で呼び出すことができます。 super() は、子クラスのオブジェクトに対して親クラスのメソッドを呼び出すための構文です。 実際に、HelloFilereadline の中で、 HelloForEverreadline を呼び出しています。

[16]:
f = HelloFile(3)
[17]:
f.readline()
[17]:
'Hello.\n'

特殊メソッド

Pythonでは、特殊メソッドと呼ばれるメソッドが多数あります。 これらのメソッドの名前は __ で始まり __ で終わります。

クラス定義の中で特殊メソッドを定義すると、そのクラスのオブジェクトに対して、 その特殊メソッドに対応する機能が付与されます。 初期化メソッド __init__ も特殊メソッドですが、 以下のクラス HelloFileIterator では、__iter____next__ という特殊メソッドが定義されています。 このクラスは、HelloFile を継承して定義されています。

__iter__ メソッドは、オブジェクトに対して関数 iter が適用されたときに呼び出されます。 __iter__ メソッドの値が関数 iter の値となります。 以下の例では、__iter__ はオブジェクト自身を返しています。 したがって、オブジェクトに iter が適用されると、オブジェクト自身が返ります。

[18]:
class HelloFileIterator(HelloFile):
    def __iter__(self):
        return self
    def __next__(self):
        line = self.readline()
        if line == '':
            raise StopIteration
        return line
[19]:
f = HelloFileIterator(3)
[20]:
print(f is iter(f))
True

上の例で、iter(f) として関数 iter を呼び出すと、 f.__iter__() としてメソッド __iter__f に対して呼び出され、 その結果が iter(f) の値となります。 したがって、iter(f)f と同じ値を返します。

__next__ メソッドも、オブジェクトに対して関数 next が適用されたときに呼び出されます。 __next__ メソッドの値が next の値となります。

上の例では、self.readline() として、オブジェクト自身に対してメソッド readline を呼び出しています。 その値が空文字列ならば、

raise StopIteration

という文を実行して、StopIteration というエラーを投げます。 実は、このエラーは、for文が捕まえて繰り返しを止める効果を持ちます。 なお、raise は強制的にエラーを発生させる構文です。

[21]:
for line in f:
    print(line)
Hello.

Hello.

Hello.

4-2で説明したように、上のfor文では、 まず f のオブジェクトに対して関数 iter が適用されます。 すると f のオブジェクト自身が返ります。 そして、このオブジェクトに対して関数 next が繰り返し適用されて、 その結果が変数 line の値となります。 StopIteration のエラーが検知されると、for文が終了します。

継承による振舞いの改変

上で示された、HelloFileIterator__next__ メソッドでは、self.readline() というメソッド呼び出しがありました。 上の例の振舞いから、そのメソッド呼び出しは、HelloFileIterator には readline メソッドが定義されていないので、親の HelloFile を見に行って、そこで定義された readline メソッドが使われたように見えます。 しかし、それは正確ではありません。

self.readline() では、その呼び出し場所がどこであるかに関わらず、常にオブジェクト self の中のメソッドを探索します。 そして、継承があるために、__next__(self) における self が、HelloFileIterator のインスタンスであるとも限りません。 次を見てみましょう。

[22]:
class EmptyFile(HelloFileIterator):
    def readline(self):
        return ''

f = EmptyFile(3)
next(f)
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
Cell In[22], line 6
      3         return ''
      5 f = EmptyFile(3)
----> 6 next(f)

Cell In[18], line 7, in HelloFileIterator.__next__(self)
      5 line = self.readline()
      6 if line == '':
----> 7     raise StopIteration
      8 return line

StopIteration:

コンストラクタに 3 を与えているので、HelloFileIterator と同様に next を3回適用できてもよさそうですが、即座に StopIteration が生じました。 これは、__next__(self) における self が、EmptyFile のインスタンスであり、self.readline() が常に '' を返すからです。

このように、継承は、メソッドの部分的な再定義を通じて、再定義されたメソッドを呼び出しているメソッドの振舞いを、間接的に改変することを可能にします。

練習

'Hello.\n' ではなくて、初期時に指定された文字列を繰り返し返すように、 新たなクラス StringFileIterator を定義してください。

StringFileIteratorHelloFileIterator を継承し、 初期化メソッドには、文字列と回数を指定します。

[23]:
class StringFileIterator(HelloFileIterator):
    def __init__(self, s, n):
        ...
    ...

上のセルで解答を作成した後、以下のセルを実行し、実行結果が True になることを確認してください。

[24]:
f = StringFileIterator('abc', 3)
print(list(f) == ['abc','abc','abc'])
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[24], line 2
      1 f = StringFileIterator('abc', 3)
----> 2 print(list(f) == ['abc','abc','abc'])

Cell In[18], line 5, in HelloFileIterator.__next__(self)
      4 def __next__(self):
----> 5     line = self.readline()
      6     if line == '':
      7         raise StopIteration

Cell In[15], line 5, in HelloFile.readline(self)
      4 def readline(self):
----> 5     if self.n == 0:
      6         return ''
      7     self.n = self.n - 1

AttributeError: 'StringFileIterator' object has no attribute 'n'

▲with文への対応

ここでは詳しく説明しませんが、さらに特殊メソッドである __enter____exit__ を定義すると、 with文にも対応できます。

[25]:
class HelloFileIterator(HelloFile):
    def __enter__(self):
        return self
    def __exit__(self,exception_type,exception_value,traceback):
        pass
    def __next__(self):
        line = self.readline()
        if line == '':
            raise StopIteration
        return line
    def __iter__(self):
        return self
[26]:
with HelloFileIterator(3) as f:
    for line in f:
        print(line)
Hello.

Hello.

Hello.

練習の解答

[27]:
class StringFileIterator(HelloFileIterator):
    def __init__(self, s, n):
        self.s = s
        self.n = n
    def readline(self):
        if self.n == 0:
            return ''
        self.n = self.n - 1
        return self.s
[ ]: