他言語経験者用 Python 入門④ ~クラス~

オブジェクト

クラスを定義する前に、オブジェクトについて簡単にまとめておく。

Pythonのオブジェクトには整数値、文字列、リスト、タプルなどの「種類」があり、これを「データ型」あるいは単純に「型」と呼ぶ。

オブジェクトの型は、type 関数を使うことで調べられる。

type(obj)

オブジェクトの id を調べることができる。

id(obj)

オブジェクトの比較

オブジェクトの比較には、「同一性」と「等価性」によるものがある。

 ・同一性:同じ id を持っているか
 ・等価性:オブジェクトの値が等しいかどうか

同一性の比較には、is 演算子と is not 演算子を使う。
等価性の比較には、== 演算子と != 演算子を使う。

オブジェクトの文字列化

オブジェクトを文字列化するには、str 関数と repr 関数がある。
違いをサンプルで示す。

msg = '123'

print(str(msg))
print(repr(msg))
実行結果:
 123
 '123'

クラスの定義とインスタンス化

class クラス名:
    クラス定義の本体(クラスが持つ属性を定義する)

何も処理しないクラスを作成してみる。

class Test:
 pass
実行結果: なし

上記のクラスをインスタンス化する。
Python のインスタンス化では、Java などのように new を使用しない。

test = Test()
print(type(test))
実行結果: <class '__main__.Test'>

Test のインスタンスが持つ機能を調べる。

print(dir(test))
実行結果:
['__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__']

中身が pass しかない空のクラスなのに、上記のとおり色々な機能がある。
その理由は、Python のすべての class は object クラスが基底となっているから。

メンバ変数(インスタンス変数)の定義

人間(Person)クラスを作成し、名前と年齢をメンバ変数として定義する。

class Person:
 pass

p = Person()
p.name = 'Taro'
p.age = 20

上記のとおり、クラス定義には書かれていなくても、Javascript のようにインスタンス後にメンバ変数を追加することができる。

しかし、この方法では、インスタンス化のタイミングでメンバに情報を与えることができない。

「__init__」というメソッドを使用することで、インスタンス化のタイミングでメンバ変数に初期値を設定できる。
多言語でいうコンストラクタと言える。

# class 定義
class Person:
 # コンストラクタ
 def __init__(self, name='Taro', age='20'):
   self.name = name
   self.age = age

p = Person('Jiro', 30)    # インスタンス化

print(p.name, p.age)
実行結果: Jiro 30

インスタンスメソッドの定義

では次に、メンバ変数の name と age を使い、自己紹介するメソッド「hello」を定義してみる。

※ ここでは、メンバ変数をプライベート化するため、アンダーバー2つを変数の先頭に付与している。

class Person:
 def __init__(self, name, age):
   self.__name = name
   self.__age = age
 
 def hello(self):    # インスタンスメソッドには、「self」を第1引数に設定する
   print(f'Hello! My name is {self.__name}. {self.__age} years old.')

# Person クラスの使用コード
p = Person('taro', 20)
p.hello()
実行結果: Hello! My name is taro. 20 years old.

クラスの属性

クラスの属性は、これまでに見てきたメンバ変数(インスタンス変数)とインスタンスメソッド以外にもある。

・インスタンス変数
・インスタンスメソッド
・クラス変数
 ⇒ クラスに結び付けられた変数
・クラスメソッド
 ⇒ クラスに結び付けられたメソッド
・スタティックメソッド
 ⇒ クラスを名前空間として、その中に定義されたメソッド

それぞれの違いを表に示す。

画像1

クラス変数

class Person:
 male = 1
 female = 2

上記のクラス変数には、次のようにしてアクセスできる。

# class から直接アクセスしている例
print(Person.male)
print(Person.female)

# インスタンスからアクセスしている例
p = Person()
print(p.male)
print(p.female)
実行結果:
 1
 2
 1
 2

クラスメソッド

クラスメソッドはインスタンスではなく「クラス」と関連付けられている。
Javaでいうスタティックメソッドの概念に近い。
クラスメソッドの第1引数の名前は「cls」とすることが推奨されている。
クラスメソッド内部からクラス変数にアクセスするには、この「cls」を使う。

定義方法は以下の種類がある。

・ @classmethod デコレーターを使用する
classmethod 組込み関数を使用する

上記2種類の定義方法で作成したSampleクラスと、呼び出し方法のコードを掲載する。

class Sample:
 count = 0
 
 @classmethod
 def get_count(cls):
   print(f'Count is {cls.count}')
 
 get_increased_count = classmethod(
   lambda cls: print(f'Increased count is {cls.count + 1}'))
   
# @classmethod デコレーターで定義したクラスメソッドを呼び出す
Sample.get_count()

# classmethod 関数で定義したクラスメソッドを呼び出す
Sample.get_increased_count()
実行結果:
 Count is 0
 Increased count is 1

スタティックメソッド

スタティックメソッドはインスタンスともクラスとも関連付けられていないメソッド。
よって、スタティックメソッドの第1引数には「self」も「cls」も不要。

定義方法は以下の種類がある。

・ @staticmethod デコレーターを使用する
staticmethod 組込み関数を使用する

前述のとおり、スタティックメソッドは、クラスにもインスタンスにも関連付けられていない。
使いどころとしては、『特定のクラスや、そのクラスから生成されたインスタンスと直接関連はないが、クラスの属性にまとめておきたい処理』をメソッドとして記述するためにあると考えて良い。
クラスやインスタンスとは無関係な処理を行うプライベートヘルパーなどが考えられるが、使用頻度は低そう。

プライベートな属性

プライベートな属性は、以下の方法で作成できる。

・変数名をアンダーバーから始めることで、プライベート変数であることを使用者に伝えるが、強制力はない。
・変数名をアンダーバー2つから始めることで、プライベート変数として宣言する。

プライベートな属性には、Getter / Setterを介してアクセスするが、property 関数を使うことで、属性に直接アクセスするようにコーディングできる。

property 関数
property(fget=None, fset=None, fdel=None, doc=None)

fget: プロパティの値を取得するのに使用するメソッド
fset: プロパティの値を設定するのに使用するメソッド
fdel: プロパティを値を削除するのに使用するメソッド
doc: プロパティのdocstring

Personクラスの private属性 (__age) をproperty 関数を使い設定・取得してみる。

class Person:
 def __init__(self, name, age):
   self.__name = name
   self.__age = age
 
 def hello(self):
   print(f'Hello! My name is {self.__name}. {self.__age} years old.')
 
 def get_age(self):
   return self.__age
 
 def set_age(self, age):
   self.__age = age
 
 age = property(get_age, set_age)


# Person クラスの使用コード
p = Person('taro', 20)
p.age = 30
print(p.age)
実行結果: 30

クラスの継承

基底クラス(Person)

簡単にするため、メンバ変数は name 1つだけにし、プライベート化していない。

class Person:
 def __init__(self, name):
   self.name = name
 
 def hello(self):
   print(f'Hello! My name is {self.name}.')

サブクラス(Student)

継承するには、クラス名の後ろに丸カッコを付け、その中に継承する親クラスを指定する。

from person import Person

# Person クラスを継承する Student
class Student(Person):
 def __init__(self, name, school):
   super().__init__(name)    # 親クラスの __init__ により名前をセット
   self.school = school
 
 # hello メソッドをオーバーライド
 def hello(self):
   super().hello()    # 親クラスの hello メソッドを呼び出し
   print(f'Hello! My school is {self.school}.')

動作確認用のコード

from student import Student

s = Student('taro', 20, 'Test School')
s.hello()
実行結果:
 Hello! My name is taro.
 Hello! My school is Test Shcool.

多重継承

最近のプログラミング言語では多重継承をサポートするものは C++ など、それほど多くはないが、Pythonは多重継承をサポートしている。
多重継承を行うには、クラス定義で基底クラスを複数記述する。

#extend_sample.py

class A:
 def hello(self):
   print('Hello A')
   
class B(A):
 pass
 
class C(A):
 def hello(self):
   print('Hello C')
   
class Main(B, C):
 pass

動作確認用のコード

import extend_sample as ex

main = ex.Main()
main.hello()
実行結果: Hello C

上記例の場合、Mainクラスのインスタンスから hello を呼び出しているが、
Mainには hello が存在せず、Bを探しにいく。
そこにも hello がないので C を探し、そこで hello を見付けていることになる。
よって、Aの hello は探されずスルーされている。

Main -> B -> C ( ここで hello を発見 )

では次に、Cクラスから継承を取り去ってみる。

class A:
 def hello(self):
   print('Hello A')
   
class B(A):
 pass
 
class C:    # A を継承しないよう変更
 def hello(self):
   print('Hello C')
   
class Main(B, C):
 pass

動作確認用のコード

import extend_sample as ex

main = ex.Main()
main.hello()
実行結果: Hello A

上記例の場合、Mainクラスのインスタンスから hello を呼び出しているが、
Mainには hello が存在せず、Bを探しにいく。
そこにも hello がない、とここまでは前回と同様だが、その後 C を探しにいかず、継承している A を探しにいきそこで hello を見付けていることになる。
よって、C の hello は探されずスルーされている。

Main -> B -> A ( ここで hello を発見 )

Method Resolution Order ( メソッド解決順 )

前述の例のとおり、メソッド解決順は非常に複雑なため、完全に理解・記憶するのが困難だが、__mro__ というクラスの特殊属性を使い確認できる。
※ mro とは Method Resolution Order の略

from extend_sample import Main

print(Main.__mro__)
実行結果:
(<class 'extend_sample.Main'>, <class 'extend_sample.B'>, <class 'extend_sample.A'>, <class 'extend_sample.C'>, <class 'object'>)

mixin

簡単に言うと、実装の継承。
Javaで言えばInterfaceやAbstract classを使ったポリモーフィズムに近い。

属性を持たず、機能のみを持つクラスのサンプルを以下に掲載する。

# util.py
from sys import _getframe as frame    # 自身のメソッド名を取得するために使用する

class Util:
 def template_method(self):
   self.utility()    # utility を呼び出している
   
 def utility(self):
   raise NotImplementedError(
     f"Method '{frame().f_code.co_name}' not implemented")

動作確認用のコード

from util import Util

u = Util()
u.template_method()
実行結果: NotImplementedError: Method 'utility' not implemented

上記のとおり、エラーになる。

Utilクラスの utility メソッドは、エラーを raise するだけなので、継承先クラスでオーバーライドし、機能を実装することを前提としている。
※ raise については「例外」の項で後述。

では次に、Utilクラスを継承したUserUtilクラスを作成する。

from util import Util

class UseUtil(Util):
 def utility(self):
   print('This is mixin')

動作確認用のコード

from use_util import UseUtil

u = UseUtil()
u.utility()
実行結果: This is mixin

多重継承には、インスタンス変数を持つ複数クラスを継承した際に問題があるため、mixin を前提とした運用が望ましそう。

例外

Pythonの例外処理も、Javaなどと同様「try」を使用する。
発生した例外は except で捕捉する。

基本的な構文は以下のとおり。
※ [ ] 内は省略可能

try :
  例外の可能性がある処理
except [例外クラス] [as 別名] :
  例外処理のコード

シンプルな数宛てゲーム

import random
ans = random.randint(1, 100)

while True:
 n = input('Input number 1 - 100. 0 to quit.\n:')
 
 try:
   n = int(n)    # n に文字が入力された場合にエラーが想定される
 except ValueError as e:
   print('Input number only!')
   continue
 
 if n == 0:    # 0 が入力されたらループを抜ける
   break;
 
 # 正解判定の分岐
 if n == ans:
   print('Good!')
 else:
   print('No good...')

except は複数書くことができる。

try :
  例外の可能性がある処理
except [例外クラス1] [as 別名] :
  例外処理のコード
except [例外クラス2] [as 別名] :
  例外処理のコード
else:
  例外が発生しなかった場合に実行するコード
finally:
  例外が発生してもしなくても最後に実行するコード

Exceptionクラスは、その他の例外を含むため、最後に記載する。

while True:
  try:
    # 省略
  except (ValueError, IndexError) as e:
    print(e.args)
  except Exception as e:
    print('Exception')
    print(e.args)
  except:
    print('Exceptionよりも上位の例外クラスに属する例外が発生 ')

raise 文

意図的に例外を発生させるには、raise文 を使う。
raise に続けて例外クラス名を入れると、引数なしで「例外クラス名()」が呼び出される。

raise 例外クラス(引数)
raise 例外クラス

※ 前回の記事で mixin のサンプルコード内で使用している。

例外クラスは自作することができる。

# Exception クラスを継承して作る
class EmptyError(Exception):
  def __init__(self, *args):
    if len(args) == 0:
      super().__init__('args is empty')
    else:
      super().__init__(args)


入門編は以上!

この記事が気に入ったらサポートをしてみませんか?