Python – OOP Part 2. クラスとインスタンス(Class and Instance)

読了 9分

今回の講座ではクラスとインスタンスについて学んでいきます。

「Pythonはオブジェクト指向プログラミング言語です。Pythonのすべてはオブジェクトです。文字列、リスト、関数、さらにはモジュールまでもオブジェクトです…」という話は、耳が痛くなるほど聞いてこられたと思います。ところで、いったいオブジェクトとは何でしょうか?オブジェクトとは、属性のようなさまざまなデータと関数(オブジェクトの中ではメソッドと呼びます。)を含んだ一つのデータ構造を指します。また、Pythonにおいてこれらのオブジェクトは変数に代入することもでき、関数の引数として渡すこともできるファーストクラスオブジェクトです。ファーストクラスオブジェクトについては、以前の講座であるファーストクラス関数を参照してください。

オブジェクトとは?

オブジェクトとは、データをもう少し簡単に扱うために「名前空間」というものを利用して作った論理的な集合です。学校で生徒を管理するために学年やクラスに分けるようなものです。Python辞書を作ることや、クラスとモジュールでデータの集合を作ることはすべて、データを簡単に保存・変更・アクセスできるようにオブジェクトを作ることです。

名前空間については、モジュールを扱うときに詳しく説明します。

次のコードは、形式が違うだけで、すべて論理的なデータの集合であるオブジェクトを作って必要なデータにアクセスする方法を示しています。

お好きなディレクトリに oop_2.py という名前のPythonファイルを作って、次のコードを保存してください。

辞書を使う場合

oop_2.py
student = {'name': '李相熙', 'year': 2, 'class': 3, 'student_id': 35}

print('{}, {}{}{}番'.format(student['name'], student['year'], student['class'], student['student_id']))

ターミナルやコマンドウィンドウを開いて oop_2.py が保存されたディレクトリに移動した後、プログラムを実行してください。

実行結果
李相熙, 2年 3組 35番

クラスを使う場合

oop_2.py
class Student:
def __init__(self, name, year, class_num, student_id):  # Pythonキーワードのclassは仮引数名として使えません。
self.name = name
self.year = year
self.class_num = class_num
self.student_id = student_id

    def introduce_myself(self):
        return '{}, {}{}{}番'.format(self.name, self.year, self.class_num, self.student_id)


student = Student('李相熙', 2, 3, 35)
print(student.introduce_myself())
実行結果
李相熙, 2年 3組 35番

モジュールを使う場合 同じフォルダの中に student.py ファイルを一つ作成し、下のコードを入力します。

student.py
name = '李相熙'
year = 2
class_id = 3
student_id = 35
oop_2.py
import student

print('{}, {}{}{}番'.format(student.name, student.year, student.class_id, student.student_id))
実行結果
李相熙, 2年 3組 35番

上の3つの例はすべて、形式と方法が少し違うだけで、オブジェクトという論理的な集合を使って同じデータを出力していることが分かります。

それでは今度は、Python のすべてのものが本当にオブジェクトなのか、そしてそのオブジェクトの中には何があるのか確認してみましょうか?まずは文字列が本当にオブジェクトなのか確認してみます。

oop_2.py
text = 'string'
print(dir(text))

覚えておきましょう~!

dir() は Python の標準組み込み関数です。この関数は、引数がない場合はモジュールレベルのローカル変数を、引数がある場合は引数(オブジェクト)のすべての属性とメソッドを表示します。この関数はデバッグをするときにとてもよく使われる重要な関数です。

実行結果
['__add__', '__class__', '__contains__', '__delattr__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__getslice__', '__gt__', '__hash__', '__init__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '_formatter_field_name_split', '_formatter_parser', 'capitalize', 'center', 'count', 'decode', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'index', 'isalnum', 'isalpha', 'isdigit', 'islower', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']

text という変数に「string」という6文字を代入しただけなのに、なぜこんなにたくさん出力されるのでしょうか?!?

その理由はこうです。textstr というデータ型が作り出したオブジェクトであり、str データ型に定義されたすべての属性とメソッドを継承しているからです。

例題を通していくつかだけ確認してみます。

oop_2.py
text = 'string'

print('# text のクラス確認')
print(text.__class__)  # <type 'str'>

print('\n# text が str のインスタンスオブジェクトかどうか確認')
print(isinstance(text, str))  # True

print('\n# str オブジェクトのメソッド確認')
print(text.upper())  # STRING
実行結果
# text のクラス確認
<class 'str'>

# text が str のインスタンスオブジェクトかどうか確認
True

# メソッド確認
STRING

text のクラスも確認してみて、メソッドも呼び出してみました。関数やモジュールもオブジェクトと言っていましたが、本当かどうか確認してみましょう。

oop_2.py
def my_function():
'''my_function についての説明です~!'''
pass

print('# my_function の属性確認')
print(dir(my_function), '\n')

print('# my_function の docstring 出力')
print(my_function.__doc__, '\n')

print('# my_function に新しい属性を追加\n')
my_function.new_variable = '新しい変数です。'

print('# 追加された属性の確認')
print(dir(my_function), '\n')

print('# 追加した属性値の出力')
print(my_function.new_variable, '\n')
実行結果
# my_function の属性確認
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__ha
sh__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subcl
asshook__']

# my_function の docstring 出力
my_function についての説明です~!

# my_function に新しい属性を追加

# 追加された属性の確認
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__ha
sh__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subcl
asshook__', 'new_variable']

# 追加した属性値の出力
新しい変数です。

本当に関数も多くの属性を持っていること、そして任意に属性を追加することもできるということまで確認しました。

ここである程度感覚を掴まれたと思いますので、クラスを使って新しいデータ型を作り、そのデータ型のインスタンスオブジェクトを作ってみましょう。

会社で従業員の人事データを管理するためのクラスを作ってみます。

oop_2.py
class Employee:
pass

emp_1 = Employee()
emp_2 = Employee()

print('# emp_1 と emp_2 は異なるメモリアドレス値を持つ別々のオブジェクトです。')
print(id(emp_1))
print(id(emp_2))
print()

print('# emp_1 と emp_2 が同じクラスのインスタンスであることを確認します。')
class_of_emp_1 = emp_1.__class__
class_of_emp_2 = emp_2.__class__
print(id(class_of_emp_1))
print(id(class_of_emp_2))
実行結果
# emp_1 と emp_2 は異なるメモリアドレス値を持つ別々のオブジェクトです。
3095355928096
3095355928048

# emp_1 と emp_2 が同じクラスのインスタンスであることを確認します。
3095348371152
3095348371152

Employeeというクラスを定義し、emp_1、emp_2というインスタンスを作りました。そしてid()関数を利用してemp_1とemp_2が異なるメモリアドレス値を持つ別々のオブジェクトであることを確認しました。そして両方とも同じクラスのインスタンスであることも確認しました。

今度はemp_1、emp_2インスタンスに変数を追加してデータを保存してみます。

oop_2.py
class Employee:
pass

emp_1 = Employee()
emp_2 = Employee()

# インスタンス変数にデータを保存
emp_1.first = 'Sanghee'
emp_1.last = 'Lee'
emp_1.email = 'sanghee.lee@schoolofweb.net'
emp_1.pay = 50000

emp_2.first = 'Minjung'
emp_2.last = 'Kim'
emp_2.email = 'minjung.kim@schoolofweb.net'
emp_2.pay = 60000

# インスタンス変数のデータにアクセス
print(emp_1.email)
print(emp_2.email)
実行結果
sanghee.lee@schoolofweb.net
minjung.kim@schoolofweb.net

インスタンスにデータを保存してアクセスしてみました。しかし上のコードは間違ったコードです。上のコードのようにインスタンス変数を一つ一つ手動で代入するのであれば、クラスを使う意味がありません。init メソッドを使って、インスタンスを生成するときに必要なデータを代入してみましょう。

ノート

init メソッドは「イニシャライザ」とも呼ばれ、他の言語では「コンストラクタ」とも呼ばれます。このメソッドはインスタンスが生成されるときに自動的に呼び出され、呼び出される瞬間に自動的にインスタンスオブジェクトを self という引数として受け取ります。そして、このイニシャライザを使って、インスタンス生成時にさまざまなデータを引数として渡し、オブジェクトの中に初期データとして保存することができます。

oop_2.py

class Employee:
def __init__(self, first, last, pay):
self.first = first
self.last = last
self.pay = pay
self.email = first.lower() + '.' + last.lower() + '@schoolofweb.net'


emp_1 = Employee('Sanghee', 'Lee', 50000)
emp_2 = Employee('Minjung', 'Kim', 60000)

print(emp_1.email)
print(emp_2.email)

# emp_1 のフルネームを出力
print('{} {}'.format(emp_1.first, emp_1.last))
実行結果
sanghee.lee@schoolofweb.net
minjung.kim@schoolofweb.net
Sanghee Lee
クラスの利点を活かして簡潔なコードができました。

ところで最後の行のフルネームを出力するコードを見てください。このコードは会社である従業員の名前を聞くときに、「お名前は何ですか?ファーストネーム、ラストネームの順で言ってください。」とお願いするのと同じです。100人の従業員の名前を聞いたら口が痛くなりそうです。笑 すべての従業員が「お名前は?」と聞かれたときにどんな形式で答えるべきか分かっているといいですね。そうなるように作ってみましょう。
oop_2.py
class Employee:
def __init__(self, first, last, pay):
self.first = first
self.last = last
self.pay = pay
self.email = first.lower() + '.' + last.lower() + '@schoolofweb.net'

    def full_name(self):
        return '{} {}'.format(self.first, self.last)


emp_1 = Employee('Sanghee', 'Lee', 50000)
emp_2 = Employee('Minjung', 'Kim', 60000)

# emp_1 のフルネームを出力
print(emp_1.full_name())
実行結果
Sanghee Lee

初めてクラスを使う方々がとてもよくする間違いがあります。それは何かというと、メソッドを定義するときに self 引数を忘れてしまうことです。すると、どうなるでしょうか?見てみましょう。

oop_2.py
class Employee:
def __init__(self, first, last, pay):
self.first = first
self.last = last
self.pay = pay
self.email = first.lower() + '.' + last.lower() + '@schoolofweb.net'

    def full_name():  # <--- self がありません。
        return '{} {}'.format(self.first, self.last)


emp_1 = Employee('Sanghee', 'Lee', 50000)
emp_2 = Employee('Minjung', 'Kim', 60000)

# emp_1 のフルネームを出力
print(emp_1.full_name())
実行結果
Traceback (most recent call last):
File "C:\Users\CURTIS\Dev\Python\oop_2.py", line 16, in <module>
print(emp_1.full_name())
TypeError: full_name() takes 0 positional arguments but 1 was given
“TypeError: full_name() takes 0 positional arguments but 1 was given”

おっと… Pythonインタプリタが何やら分からないことを言っていますが、どういう意味でしょうか? full_name メソッドは引数を受け取らないのに、なぜ1つ渡されたのかと言っています。emp_1.full_name() のように何の引数もなしに呼び出したのに、ですよ。何でしょう?!? それは上でも説明したように、インスタンスのメソッドを呼び出すとインスタンス自身である self が最初の引数として自動的に渡されるからです。次の例を見ていただくと、少し理解しやすいでしょう。

oop-2.py
class Employee:
def __init__(self, first, last, pay):
self.first = first
self.last = last
self.pay = pay
self.email = first.lower() + '.' + last.lower() + '@schoolofweb.net'

    def full_name(self):
        return '{} {}'.format(self.first, self.last)


emp_1 = Employee('Sanghee', 'Lee', 50000)
emp_2 = Employee('Minjung', 'Kim', 60000)

# クラスを通じて full_name メソッドを呼び出し
print(Employee.full_name(emp_1))
実行結果
Sanghee Lee

最後の行のコードを見ると、クラスを通じてメソッドを実行しましたが、このような場合にはクラスはどのインスタンスのメソッドを呼び出すべきかが分からないため、対象となるインスタンスを引数として渡さなければなりません。実は emp_1.full_name() を実行すると、バックグラウンドでは Employee.full_name(emp_1) が実行されているのです。

今回の講座はここで終わりにし、次の講座では「クラス変数」を学びながら、クラスとインスタンスの違いについてさらに学んでいきましょう。

X