[Python][Python3][オブジェクト]Python3におけるオブジェクトとクラスについて

incandescent light bulb python
Photo by Esther on Pexels.com

オブジェクトとクラス

やはりpythonのselfについて自分がよくわかっていないと思い、まとめてみる。手持ちのこちらの書籍から抜粋。2版になってずいぶん値段があがったな・・・。

入門 Python 3 第2版
データサイエンスやウェブ開発、セキュリティなど、さまざまな分野で人気を獲得してきているPython。本書は、ベストセラー『入門 Python 3』の6年ぶりの改訂版で、プログラミング初級者を対象としたPythonの入門書です。プログラミングおよびPythonの基礎から、ウェブ、データベース、ネットワーク、並行処理といっ...

オブジェクトとは何か

そもそもpythonで普段扱っている

n = 2024
my_list = ["Tokyo", "Kyoto", "Osaka"]

などはすべてオブジェクトである。上のオブジェクトは値が\(2024\)の整数型オブジェクトで、下はstr型リストのオブジェクトである。普段中身を意識することはなく、中身を調査する必要があるのは、独自のオブジェクトを作成するときか、既存のオブジェクトの動作を変更するときである。

オブジェクトにはデータ(属性と呼ばれる)とコード(メソッドと呼ばれる)が含まれている。例えば、”n = 7″の属性は数値(整数クラス)であり、”my_list”には

capitalize()
replace()

といったメソッドが含まれている。

誰も作ったことのない新しいオブジェクトを作成するときは、その内容を示すクラスを作成する必要がある。

クラス

上でさらっとでてきた「クラス」とはなんだろうか。例えば、Pythonには文字列クラスStringがある。これに含まれるのは、”penguin”や”lion”などの文字列オブジェクトである。その他にも、数値、リスト、辞書など標準データ型を作成するための組み込みのクラスが多数用意されている。カスタムオブジェクトを作成するためには、”class”キーワードを用いてクラスを定義しなくてはいけない。

例を見てみる。人間を表現するためのオブジェクトを作成したい。この鋳型(テンプレート)として、Personというクラスを定義する。なお、Pythonのクラス名は先頭を大文字にする。

class Person():
    pass

このクラスが空だということを表現するために、”pass”は必要である。このクラスを利用して(呼び出して)、オブジェクトを作成することができる。

someone = Person()

Personオブジェクトは空なので、このsomeoneオブジェクトは何の働きもすることはできない。そこで、特殊メソッド”__init__”を用いて、Pythonオブジェクトの初期化を行う。

class Persion():
    def __init__(self):
        pass

“__init__”はクラスの定義から、個々のオブジェクトを作成するときに、それを初期化するメソッドにつけられた特殊な名前である。”self”という引数は、作成されるオブジェクト自身を参照することを表している。

ここで、”__init__”を定義するときは、第一引数は必ず”self”でなければならない。これは予約語ではないが、Pythonの慣習として広く受け入れられているし、遵守すべきである。

上の二番目のクラスでも、まだオブジェクトは何の働きをすることもできない。次の定義でようやく、単純な働きをこなすようになる。

class Person():
    def __init__(self, name):
        self.name = name

name引数として、文字列を渡すと、Personクラスから次のようにオブジェクトを作り、代入することができるようになる。

Tom = Person("Thomas Pryce")

上記コードでは、

  • Personクラスの定義を探し、
  • メモリ内に新しいオブジェクトのインスタンスを作成し、
  • 新しく作ったオブジェクトをself、もう一つの引数”Thomas Pryce”をnameとして渡して、オブジェクトの”__init__”メソッドを呼び出す。
  • “name”の値をオブジェクトに格納する。
  • その新しいオブジェクトを返却する。
  • オブジェクトに”Tom”という名前を与える。

という動作を行っている。この新しいオブジェクトはリストやtupple、辞書や集合の要素として扱うことができるし、他の関数に引数として渡したり、関数から結果として返却することもできる。渡した”name”の値は、属性としてオブジェクトに付与される。

print(Tom.name)

ただし、classの内部では、”self.name”という形でアクセスする。すべてのクラス定義が”__init__”を要するわけではない。同じクラスから作られた他のオブジェクトから、オブジェクトを区別する必要があるときにこの処理を行う。

継承

Pythonでコーディングを行っていると、既存のクラスをほんの少し変更するだけでオブジェクトが作成できそうだ、という場面が多々ある。このとき、既存のクラスを修正することで、壊してしまう可能性や、コピー&ペーストして新しいクラスを作ることで保守性が低下するという問題が生じる。この問題を驚くほどスムーズに解決する方法が継承である。既存のクラスを指定して、追加や変更する部分のみを定義する新しいクラスを作成する。

新しいクラスで追加や変更したい部分だけを定義すると、古いクラスの定義は利用されない。これをオーバーライドという。元のクラスは親(スーパークラス、基底クラス)と言われ、新しいクラスは子(サブクラス、派生クラス)などと言われる。

実際の継承の例を作成してみよう。Carというクラスを作成し、Toyotaというサブクラスを定義する。

class Car():
    pass

class Toyota(Car):
    pass

give_me_a_car = Car()
gime_me_a_toyota = Toyota()

子クラスは親クラスを専門特化したものである。この例ではオブジェクトが何の働きをすることもできない。以下で改良をしていく。

class Car():
    def exclaim(self):
        print("I am a car.")

class Toyota(Car):
    pass

give_me_a_car = Car()
give_me_a_toyota = Toyota()

give_me_a_car.exclaim()
# I am a car.
give_me_a_toyota.exclaim()
# I am a car.

この例ではToyotaがCarのメソッドであるexclaimを継承していることがわかる。しかし、これではアイデンティティの危機に陥りかねない。そこで、次でメソッドのオーバーライドをみていく。

メソッドのオーバーライド

ToyotaクラスはCarに内包されているが、異なる部分がある筈である。その部分を上書き(オーバーライド)していく。

class Car():
    def exclaim(self):
        print("I am a car.")

class Toyota():
    def exclaim(self):
        print("I am a Toyota. Muchi like a Car, but more Toyota-ish.")

give_me_a_car = Car()
give_me_a_Toyota = Toyota()

give_me_a_car.exclaim()
# I am a car.
give_me_a_toyota.exclaim()
# I am a Toyota. Muchi like a Car, but more Toyota-ish.

この例ではexclaimメソッドをオーバーライドしているが、”__init__”を含むどんなメソッドでもオーバーライド可能である。上記のPersonクラスを用いて、医者(MDPerson)および弁護士(JDPerson)を作成してみよう。

class Person():
    def __init__(self, name):
        self.name = name

class MDPerson():
    def __init__(self, name):
        self.name = "Doctor " + name

class JDPerson():
    def __init__(self, name):
        self.name = name + ", Esquire"

person = Person("Tom")
doctor = MDPerson("Tom")
lawyer = JDPerson("Tom")

print(person.name)
# Tom
print(doctor.name)
# Doctor Tom
print(lawyer.name)
#Tom, Esquire

メソッドの追加

もちろん親クラスにないメソッドを子クラスに追加することもできる。再度Carクラスの例に戻ろう。Toyoyaクラスにだけ、”need_a_push()”メソッドを追加してみよう。

class Car():
    def exclaim(self):
        print("I am a car.")

class Toyota(Car):
    def exclaim(self):
        print("I am a Toyota. Muchi like a Car, but more Toyota-ish.")
    def need_a_push(self):
        print("A little help here?")

give_me_a_car = Car()
give_me_a_toyota = Toyota()

上記のToyotaオブジェクトは、need_a_push()メソッドに対応可能である。

give_me_a_toyota.need_a_push()
# A little help here?

しかし、親クラスは対応できない。

give_me_a_car.need_a_push()

# ---------------------------------------------------------------------------
# AttributeError                            Traceback (most recent call last)
# Cell In[1225], line 1
# ----> 1 give_me_a_car.need_a_push()
# 
# AttributeError: 'Car' object has no attribute 'need_a_push'

superによる親クラスへの支援要請

上書きや追加ではなく、小クラスから親クラスのメソッドを単純に呼び出す方法について述べる。ここでも本の例に習う。電子メールアドレスを有するPersonを表現するEmailPersonという新クラスを作成する。まずは親クラスの定義から。

class Person():
    def __init__(self, name):
        self.name = name

次の子クラスで、”__init__()”の呼び出しで”email”変数が追加されていることに注意する。

class EmailPerson(Person):
    def __init__(self, name, email):
        super().__init__(name)
        self.email = email

ここでは”__init__()”を再定義しているので、親クラスの”__init__()”メソッドはもはや呼び出されない。ここで行われていることは、

  • super()が親クラスのPersonの定義を取り出す。
  • super().__init__()メソッド呼び出しは、Person.__init__()メソッドを呼び出す。このとき、self引数の親クラスへの受け渡しはPythonが処理してくれるので、コーディングで気を配るのは引数を適切に渡すことだけである。Person()が受け付ける引数はnameだけである。
  • self.email = emailはEmailPersonクラスで追加された新しいオブジェクトである。

ここで、EmailPersonクラスのオブジェクトを作成してみる。

Tom = EmailPerson("Thomas Pryce", "tom@mailadress.com")

nameiとemail両方の属性をもつオブジェクトが作成される。

Tom.name
# 'Thomas Pryce'
Tom.email
# 'tom@mailadress.com'

ここで重要なポイントがある。なぜ以下のように、子クラスを定義しなかったのかということである。

class EmailPerson(Person):
    def __init__(self, name, email):
        self.name = name
        self.email = email

もちろこのコードは正しい。が継承を利用する意味がなくなってしまう。先のコードではPersonにただのPersonオブジェクトとしての働きを与えた。こうしておけば、将来的にPersonクラスの定義を変更しても、super()を使っておけばEmailPersonがPersonから継承した属性・メソッドをそのまま変更した形で利用できる。コードの保守性という観点からも、これは非常に重要である。

selfの自己弁護

Pythonのインスタンスメソッドに何故self引数を、しかも第一の引数として組み込まなければならないのか?Pythonはこの引数を利用することで、適切なオブジェクトの属性とメソッドを発見することができる。具体例でみてみる。再三になるが、Carクラスを利用する。

car = Car()
car.exclaim()
# I am a car.

このコードの背部で、Pythonは

  • carオブジェクトクラスのCarクラスを探す。
  • Carクラスのexclaim()メソッドにself引数としてcarオブジェクトを渡す。

という動作を行っている。これは、以下のコードと同一であるが、このコードを利用すべき理由はない。

Car.excalim(car)
# I am a car.

プロパティによる属性値の取得や設定

PythonではClass内のすべての属性とメソッドは公開されているが、アクセスする場合にはプロパティを用いる。以下の例では、属性としてhidden_nameをもつDuckクラスを定義する。

class Duck():
    def __init__(self, input_name):
        self.hidden_name = input_name
    def get_name(self):
        print("inside the getter")
        return self.hidden_name
    def set_name(self, input_name):
        print("inside the setter")
        self.hidden_name = input_name
    name = property(get_name, set_name)

新メソッドは最後の行がなければ通常のゲッター、セッターとして機能するが、最後の行は二つのメソッドをnameというプロパティのセッター、ゲッターとして定義している。perperty()の第一引数はgetterメソッド、第二引数はsetterメソッドである。これによって、Duckオブジェクトのnameを参照すると、実際にはget_name()メソッドが呼び出されるようになる。

fowl = Duck("Howard")
fowl.name
# inside the getter
# "Howard"

それでも通常のgetterメソッドのように、get_name()を直接呼び出すことができる。

fowl.get_name()
# inside the getter
# "Howard"

一方、nameプロパティに値を代入すると、set_nameメソッドが呼び出される。

fowl.name = "Daffy"
# inside the setter
fowl.name
# inside the getter
# "Daffy"

こちらの場合も、set_name()メソッドを直接呼び出すことが可能である。

fowl.set_name("Daffy")
# inside the setter
fowl.name
# inside the getter
# "Daffy"

プロパティを定義する別の方法は、デコレータである。次のサンプルでは、同じname()という名前を持つが、前につくデコレータが異なる二つのメソッドを定義している。

class Duck():
    def __init__(self, input_name):
        self.hidden_name = input_name
    @property
    def name(self):
        print("inside the getter")
        return self.hidden_name
    @name.setter
    def name(self, input_name):
        print("inside the setter")
        self.hidden_name = input_name

このようにすることで、属性であるかのようにnameにアクセスはできるが、目に見える形でのget_name()あるいはset_name()メソッドはない。

fowl = Duck("Howard")
fowl.name
# inside the getter
# "Howard"
fowl.name = "Donald"
# inside the setter
fowl.name
# inside the getter
# "Donald"

上の例ではオブジェクト内に格納された単一の属性であるhidden_nameを参照するために、nameプロパティを使った。プロパティは計算された値を参照することもできる。以下の例を見てみよう。

class Circle():
    def __init__(self, radius):
        self.radius = radius
    @property
    def diameter(self):
        return 2*self.radius

Cicleオブジェクトを作成するときはradius属性の初期値を指定する。

c = Circle(5)
c.radius
# 5
c.diameter
# 10

これが何だと思うかも知れないが、radius属性は書き換えることができる。

c.radius = 7
c.diameter
# 14

仮に、setterを指定しないと、外部からプロパティを設定することは不可能である。これは、読み出し専用のプロパティを作成するときに便利である。

c.radius = 9
c.diameter
# 18
c.diameter = 20
# ---------------------------------------------------------------------------
# AttributeError                            Traceback (most recent call last)
# Cell In[1285], line 1
# ----> 1 c.diameter = 20

# AttributeError: can't set attribute

属性の直接アクセスよりも、プロパティの設定の何が便利かというと、プロパティの定義を書き換えた場合でも、クラス定義内のコードを書き換えるだけで済み、呼び出し元には手を加える必要がないことであろう。

非公開属性のための名前のマングリング

Duckクラスでは外部から見えないようにしたい隠し属性に対して、hidden_nameという名前をつけた。Pythonはクラス定義の外から見えないようにすべき属性の命名方法を持っている。それは、先頭に二つの”__”をつけることである。

class Duck():
    def __init__(self, input_name):
        self.__name = input_name
    @property
    def name(self):
        print("inside the getter")
        return self.__name
    @name.setter
    def name(self, input_name):
        print("inside the setter")
        self.__name = input_name

正しく動作しているか見てみる。

fowl = Duck("Howard")
fowl.name
# inside the getter
# "Howard"
fowl.name = "Donald"
# inside the setter
fowl.name
# inside the getter
# "Donald"

一方、__name属性にはアクセスできない。

fowl.__name
# ---------------------------------------------------------------------------
# AttributeError                            Traceback (most recent call last)
# Cell In[1315], line 1
# ----> 1 fowl.__name
# 
# AttributeError: 'Duck' object has no attribute '__name'

この命名方法では属性が実際に非公開になるわけではないが、Pythonは外部コードが偶然当てたりしないように名前をマングリング(ぐちゃぐちゃにする)する。

fowl._Duck__name
# "Donald"

上の例は、inside the getterが表示されていない。これは完全な保護とは言えないが、属性に対する意図せぬ調節的なアクセスをある程度まで防ぐことはできる。

メソッドのタイプ

一部の属性とメソッドはクラス自体の一部である、それ以外のデータと関数がクラスから作成されるオブジェクトの一部となっている。クラス定義の中で、メソッドの第一引数がselfになっていたら、それはインスタンスメソッドである。独自クラスを作成するときには普通このインスタンスメソッドが用いられる。

これに対してクラスメソッドはクラス全体に影響を与える。クラスに加えられた変更はすべてのオブジェクトに影響を与える。クラス定義の中で@classmethodというデコレータを挿入すると、その次の関数はクラスメソッドになり、このメソッドの第一引数はクラス自体になる。この引数はPythonの伝統でclsと呼ばれる。

非常にわかりにくいので、次の例を見てみる。

class A():
    count = 0
    def __init__(self):
        A.count += 1
    def exclaim(self):
        print("I am a A!")
    @classmethod
    def kids(cls):
        print("A has", cls.count, "little objects.")

easy_a = A()
breezy_a = A()
wheery_aa = A()
A.kids()
# A has 3 little objects.

上のコードでは、self.countではなく、A.countを用いていることに注意する。self.countだとオブジェクトインスタンスの属性になってしまう。

これ以外にも、静的メソッドと呼ばれるものがある。これは@staticmethodというデコレータを用いる。静的メソッドは第一引数としてselfやclsを取らない。

class CoyoteWeapon():
    @staticmethod
    def commercial():
        print("This CoyoteWeapon has been brought to you by Acme.")

CoyoteWeapon.commercial()
# This CoyoteWeapon has been brought to you by Acme.

静的メソッドはオブジェクトを作成することなく実行できることに注意する。

ダックタイピング

PythonはPorymorphism(多型)、すなわちクラスの種類に関わらず、異なるオブジェクトに対して同じ操作を適応することができる。以下で、同じ”__init__()”を共通する\(3\)種類のQuoteクラスを定義する。このクラスに、次の二つの関数を追加する。

  • who()は、保存されているperson大文字列を単純に返す。
  • says()は、保存されているwordsにクラスごとに異なる記号をつけて返す。
class Quote():
    def __init__(self, person, words):
        self.person = person
        self.words = words
    def who(self):
        return self.person
    def says(self):
        return self.words + "."

class QuestionQuote(Quote):
    def says(self):
        return self.words + "?"

class ExclamationQuote(Quote):
    def says(self):
        return self.words + "!"

QuestionQuoteやExclamationQuoteの初期化の方法はQuoteと変わらないので、”__init__()”メソッドのオーバーライドは行われていない。そこで、Pythonは自動的にインスタンス変数のpersonとwordの保存のため、親クラスのQuoteから”__init__()”メソッドを呼び出す。

hunter = Quote("Elmer Fudd", "I am hunting webbits")
print(hunter.who(), "says:", hunter.says())
# Elmer Fudd says: I am hunting webbits.

hunted1 = QuestionQuote("Bugs Bunny", "What's up, doc")
print(hunted1.who(), "says:", hunted1.says())
# Bugs Bunny says: What's up, doc?

hunted2 = ExclamationQuote("Daffy Duck", "It's a rabbit season")
print(hunted2.who(), "says:", hunted2.says())
# Daffy Duck says: It's a rabbit season!

異なる\(3\)種類のsays()メソッドが\(3\)つのクラスのために異なる動作を提供する。ここからさらに進んで、who()やsays()メソッドを持ちさえすれば、どのようなオブジェクトであっても共通のインターフェイスを持つオブジェクトとして扱うことができる。以下のようなクラスを定義してみよう。

class BabblingBrook():
    def who(self):
        return "Brook"
    def says(self):
        return "Babble"

brook = BabblingBrook()

こうしておいて、様々なオブジェクトのwho()およびsays()メソッドを実行してみる。

def who_says(obj):
    print(obj.who(), "says:", obj.says())

who_says(hunter)
# Elmer Fudd says: I am hunting webbits.

who_says(hunted1)
# Bugs Bunny says: What's up, doc?

who_says(hunted2)
# Daffy Duck says: It's a rabbit season!

who_says(brook)
# Brook says: Babble

これがダックタイピングと呼ばれる理由は、以下の古いことわざからである。

アヒルのように歩き、アヒルのようにクワッと鳴くなら、それはアヒルだ。

賢者

特殊なメソッド

ここでは特殊メソッドについての詳細な解説は掲載せず、一覧として表示しておく。

メソッド意味
__eq__(self, other)self == other
__ne__(self, other)self != other
__lt__(self, other)self < other
__gt__(self, other)self > other
__le__(self, other)self <= other
__ge__(self, other)self >= other
比較のための特殊メソッド
メソッド意味
__add__(self, other)self + other
__sub__(self, other)self – other
__mul__(self, other)self * pther
__floordiv__(self, other)self // other
__truediv__(self, other)self / other
__mod__(self, other)self & other
__pow__(self, other)self ** other
算術計算のためのメソッド
メソッド意味
__str__(self)str(self)
__repr__(self)repr(self)
__len__(self)len(self)
その他の特殊メソッド

コンポジション

以下の例をみるとよく分かる。継承を行うよりも、集約化することでより簡単になっている。

class Bill():
    def __init__(self, description):
        self.description = description

class Tail():
    def __init__(self, length):
        self.length = length

class Duck():
    def __init__(self, bill, tail):
        self.bill = bill
        self.tail = tail
    def about(self):
        print("This duck has a", self.bill.description, "bill and a", self.tail.length, "tail")

tail = Tail("long")
bill = Bill("wide orange")
duck = Duck(bill, tail)
duck.about()
# This duck has a wide orange bill and a long tail

モジュールとクラス・オブジェクトの使い分け

一般的なルールは以下になる。

  • メソッドは同じであるが、属性は異なる複数のインスタンスを必要とする場合は、オブジェクトを用いる。
  • クラスは継承をサポートするが、モジュールはしない。
  • 何か一つだけ必要なときはモジュールで良い。
  • 複数の値を持つ変数があり、これらを複数の関数に引数として渡せるときは、クラスとして定義した方が良いことがある。

関連記事

JSONファイルの読み込みについて
Python3で自作関数を他ファイルから参照する

コメント

タイトルとURLをコピーしました