LoginSignup
9
11

【Python】PythonのClassについて復習

Posted at

今回の記事は、PythonのClassに関するものです。
前回の記事:

でClassを作成してまとめを作ると記載しましたが、その前にPythonのClassについて復習しておきたいと考えて記事を作成。

主に、Pythonの公式ドキュメントを参考にし、その概要の紹介となります。

  1. Classとは
  2. Scopeについて
  3. Classのinstanceについて
  4. Classの継承
  5. Private変数
  6. Iteratorについて

1. Classとは

クラスはデータと機能を組み合わせる方法を提供します。

クラスを作成することで:

  • 新しいオブジェクトの型を作成、その型を持った新しいインスタンスを作成することができる。
  • インスタンスは自身の状態を保持する属性を持てる
  • インスタンスは自身の状態を変更するためのメソッドを持てる

といったことが可能となります。

Python は、他のプログラミング言語と比較して、最小限の構文と意味付けを使ってクラスを言語に追加しています。Python のクラスは、C++ と Modula-3 のクラスメカニズムを混ぜたものです。Python のクラス機構はオブジェクト指向プログラミングの標準的な機能を全て提供しています。

ここでオブジェクト指向プログラミングについてはこちらを参照していただき、基本概念

  • カプセル化 (Encapsulation)
  • 継承 (Inheritance)
  • ポリモーフィズム (Polymorphism)

のみを紹介します。以下はかなりざっくりとした説明になりますのでご注意ください。

  • カプセル化とは:
    オブジェクトの内部データへのアクセスを制限することで、外部からのデータの変更や誤用を防ぐ。カプセルというのはオブジェクトの内部と外部を隔てるイメージからきている(と思われる)。

  • 継承とは:
    共通する性質を持ったオブジェクトを記述する際に使用されるもので、コードの再利用という点においてメリットがある。(イメージとしては共通の性質で絞り込んだ抽象的なクラスを作成し、そのクラスを拡張して具体的なクラスを作成していく。)

  • ポリモーフィズムとは:
    クラスの継承が行われたクラスについて、そのクラスが有するメソッドを使用可能でありながら、ほかの異なるメソッドを作成することができる仕組み。

なお、ほかの言語で使用されるようなデータアクセスを制限するprivateといった予約語は持っておらず、そのような仕組みがない点に注意。(慣習としてprivate変数はアンダースコアを付けた_varNameで表現されている)

2. Scopeについて

スコープ (scope) とは、ある名前空間が直接アクセスできるような、 Python プログラムのテキスト上の領域です。 "直接アクセス可能" とは、修飾なしに (訳注: spam.egg ではなく単に egg のように) 名前を参照した際に、その名前空間から名前を見つけようと試みることを意味します。

ここで名前空間(namespace)とは、名前によるオブジェクトのマッピングであり、現状ではPythonの辞書として実装されている。
名前空間の寿命については

  • モジュール:モジュール定義が読み込まれたときに名前空間を作成、インタプリタ終了時まで残る。
  • 関数内:関数が呼び出されたときに作成、関数がreturnまたはExceptionの送出かつ関数内で処理されなかった場合削除。

となっている。

スコープに関連する、予約語として重要な:

  • global : 特定の変数がグローバルスコープに存在し、そこで再束縛されることを指示する。
  • nonlocal : 特定の変数が外側のスコープに存在し、そこで再束縛されることを指示する。

上記予約語を用いて、サンプルコードが以下の通り:

変数の束縛についての例
def scope_test():
    """Outer func scope"""
    def do_local():
        """Inner func scope"""
        spam = "local spam"

    def do_nonlocal():
        """Inner func scope"""
        nonlocal spam
        spam = "nonlocal spam"

    def do_global():
        """Inner func scope"""
        global spam
        spam = "global spam"

    spam = "test spam"
    do_local()
    print("After local assignment:", spam) # not assigned
    do_nonlocal()
    print("After nonlocal assignment:", spam) # assigned
    do_global()
    print("After global assignment:", spam) # not assigned

"""Module scope"""
scope_test()
print("In global scope:", spam) # assigned

上記コードの出力は次の通り:

After local assignment: test spam
After nonlocal assignment: nonlocal spam
After global assignment: nonlocal spam
In global scope: global spam

出力結果から

  1. "Inner func scope"で変数の束縛が実施されない(予約語なし)
  2. "Inner func scope"かつ"nonlocal"予約語ありの場合、外側の"Outer func scope"において変数の束縛を実施
  3. "Inner func scope"かつ"global"予約語ありの場合、"Outer func scope"の外側の"Module scope"において変数の束縛を実施

となっている。

3. Classのinstanceについて

Pythonにおいて、Classは予約語classを用いて、

class ClassName:
    <statement-1>
    ...
    ...
    ...
    <statement-N>

により定義される。(クラスオブジェクトの作成)クラス定義の内側では新たな名前空間が作成され、ローカルな名前空間として使用される。

クラスオブジェクトは属性参照とインスタンス生成をサポートしている。例えば、次のようなクラスオブジェクトがあったとする:

class MyClass:
    """A simple example class"""
    data = []
    def __init__(self, name):
        self.name = name
        self.data.append(self)

    def say_hello(self):
        return f"Hello {self.name}!"
    
    def __repr__(self):
        return f"\"{self.__class__} : ({self.name})\""

ここで__repr__メソッドは例えば、MyClassprintした時の表現を与える特殊メソッドです。

特殊メソッドについては、こちらを参照してください。

属性参照(attribute reference)

クラスのメンバーにアクセスするためには
Pythonにおけるすべての属性参照で使われている標準的な構文:obj.nameを使用する。
上記例では、MyClass.dataMyClass.say_hello、さらにはMyClass.__doc__(クラスに属しているdocstringを返す)のようにアクセスする。

インスタンス化(instantiation)

クラスのインスタンス化には関数のような表記法を使用する:

class1 = MyClass("Sample")

これによりclass1のインスタンスオブジェクトが作成される。ここで、インスタンス生成時には自動的に__init__(self, name)が呼び出されており、引数nameMyClass("Sample")の引数が渡されています。

インスタンスオブジェクトは属性参照が可能で、有効な属性名はデータ属性およびメソッドです。例えば、次のようにアクセスする:

class1_data = class1.data
class1_hello = class1.say_hello

class1.datalistであり、class1.say_hello methodです。通常、メソッドはバインドされた直後に呼び出されますが、後者のメソッドはすぐに呼び出されるわけではなく、メソッドオブジェクトとして変数に格納しておき、class1_hello()によって呼び出すことが可能です。
ここで、class1_hello()で呼び出すことが可能であると説明しましたが、MyClassでは引数selfが与えられています。引数がない場合は例外を送出するはずですが、正常に実行されます。

この理由はメソッドが第1引数にインスタンスオブジェクトを渡しているためです。つまり、次の2つの表現が等価です:

  • class1_hello()
  • MyClass.say_hello(class1)

実際に以下のコードを実行して確認してみて下さい:

class MyClass:
    """A simple example class"""
    data = []
    def __init__(self, name):
        self.name = name
        self.data.append(self)

    def say_hello(self):
        return f"Hello {self.name}!"
    
    def __repr__(self):
        return f"\"{self.__class__} : ({self.name})\""

class1 = MyClass("Sample")
class1_hello = class1.say_hell
print(f"type1: {class1_hello()}")
print(f"type2: {MyClass.say_hello(class1)}")

すると、以下の出力がターミナル上に表示されているはずです:

type1: Hello Sample!
type2: Hello Sample!

なお、クラスのメソッドに渡されている、引数selfという名前は慣習によるものです。

クラスとインスタンス変数

一般的にインスタンス変数はそれぞれのインスタンスについて固有のデータのためのもので、クラス変数はそのクラスのすべてのインスタンスによって共有される属性やメソッドのためのものです:

class Dog:
    kind = 'canine' # class variable shared by all instances
    def __init__(self, name):
        self.name = name # instance variable unique to each instance

この例では、kindDogクラスオブジェクトの変数、nameはインスタンスオブジェクトの変数となっています。
MyClassクラスではクラス変数dataが定義されているので、インスタンス生成されたオブジェクトの数だけリストにデータがあり、この情報はクラスオブジェクトまたはインスタンスオブジェクトの両方で取得することができます。例えば、

class MyClass:
    

class1 = MyClass("Sample")
class2 = MyClass("Python")
print(MyClass.data)
print(class2.data)

とすると、次のような出力が得られるはずです:

["<class '__main__.MyClass'> : (Sample)", "<class '__main__.MyClass'> : (Python)"]
["<class '__main__.MyClass'> : (Sample)", "<class '__main__.MyClass'> : (Python)"]

出力の表示を変更するためには、__repr__メソッドの返し値の形式を変更します。例えば、

class MyClass:
    中略
    def __repr__(self):
        return f"\"{self.__class__.__name__} : ({self.name})\""

このようにすると、出力はモジュール名(今回は__main__でした)を除いたクラス名が表示されるはずです:

["MyClass : (Sample)", "MyClass : (Python)"]
["MyClass : (Sample)", "MyClass : (Python)"]

こちらはあくまで例です。

属性名の注意点

インスタンスとクラスの両方で同じ属性名が使用されている場合、属性検索はインスタンスが優先されます。

例えば:

class Warehouse:
    purpose = "storage"
    region = "west"

というクラスに対して

w1 = Warehouse()
print(f"{w1.purpose} {w1.region}")
w2 = Warehouse()
w2.region = "east" # インスタンス変数の値を再設定
print(f"{w2.purpose} {w2.region}")

とすると、出力は次のようになります:

storage west
storage east

4. Classの継承

継承という表現を使う際、ほかの言語ではもととなるクラスを親クラス、継承するクラスを子クラスと表現しますが、Pythonではそれぞれ基底クラス(base class)派生クラス(derived class)と呼びます。
派生クラスは次のように定義される:

class DerivedClassName(BaseClassName):
    <statement-1>
    ...
    <statement-N>

または、引数の基底クラスをモジュール(modname)を指定した

class DerivedClassName(modname.BaseClassName):

で定義する。今回はMyClassを基底クラスとした派生クラスSubClassを定義します。

class SubClass(MyClass):
    def __init__(self, name, age):
        super().__init__(name)
        self.age = age

    def self_introduce(self):
        return f"(His/Her) name is {self.name}. (He/She) is {self.age} years old."

おおざっぱですが、年齢ageと自己紹介self_introduceの2つのメンバーを追加しました。

ここでsuper()関数によって親オブジェクト、今回の例では基底クラスの型を持った代替オブジェクトを返し、そのオブジェクトから基底クラスの__init__メソッドを呼び出しています。このことから派生クラスの__init__が呼び出された際に、基底クラスの初期化を同時に行うことができています。super()を用いることでメソッドのオーバーライド(override)を行うことが可能なことも注目の点です。
ちなみに、ドキュメントによると、

D -> B -> C -> A -> object

のように継承されていた時、仮にBのクラスでsuper()を使用した場合、C -> A -> objectの順にオブジェクトまたはメソッドが探索されるようです。

なお、Pythonは多重継承の形式もサポートしており、複数の基底クラスをもつクラス定義は次の通り:

class DerivedClassName(Base1, Base2, Base3):
    <statement-1>
    ...
    <statement-N>

5. Private変数

オブジェクトの中からしかアクセスすることのできない、いわゆる"プライベート"インスタンス変数は、Pythonには存在しません。ただし、プライベート変数に相当する変数を定義する慣習として、変数の前にアンダースコア(_)を付与した

_privateVar

をプライベートなAPIとみなして使用します。
クラスのプライベートメンバについて適切なユースケースがあるため、名前マングリング(name mangling)という、限定されたサポートも存在しています。__privateMemという形式の識別子を与えた場合、_classname__privateMemというテキストに置換されるというものです。これによりサブクラスで定義された名前との衝突を避けることができます。

名前マングリングは、サブクラスが内部のメソッド呼び出しを壊さずにメソッドをオーバーライドするのに便利です:

class Mapping:
    def __init__(self, iterable):
        self.items_list = []
        self.__update(iterable)

    def update(self, iterable):
        for item in iterable:
            self.items_list.append(item)

    __update = update   # private copy of original update() method

class MappingSubclass(Mapping):

    def update(self, keys, values):
        # provides new signature for update()
        # but does not break __init__()
        for item in zip(keys, values):
            self.items_list.append(item)

updateというメソッドが基底クラスで定義され、派生クラスにおいてオーバーライドが行われています。例えば、次のようにインスタンス生成してみます:

mapping = Mapping([1,2,3,4,5])
sub_mapping = MappingSubclass(["item1","item2","item3"])
sub_mapping.update( ["sample1","sample2","sample3"], [1,2,3])
print(mapping.__dict__)
print(sub_mapping.__dict__)

こちらのコードを実行すると次のように表示されるはずです:

{'items': [1, 2, 3, 4, 5]}
{'items': ['item1', 'item2', 'item3', ('sample1', 1), ('sample2', 2), ('sample3', 3)]}

この表示から、updateメソッドはiterableを与えた場合とkeys,valuesを与えた場合の両方を受け付けていることがわかります。これは内部的に_Mapping__update_MappingSubClass__updateの2つのメソッドが共存していることに依ります。
なお、名前マングリングが機能していることを確認するために__update = updateをコメントアウトし、上記コードを再実行してみると、次のようなエラーが送出されるはずです:

AttributeError: 'Mapping' object has no attribute '_Mapping__update'

これより、__update_classname__privateMemのように置換されていることが確認できます。

カプセル化について

Javaなどの言語ではgetter/setterを定義したクラスを作成していました。これをPythonで記述する場合を紹介します。そのために、まずはデコレータを紹介する必要があります。デコレータについては、英語の文献にはなりますが、こちらに詳しく記載されていました。

デコレータとは

別の関数を返す関数で、通常、 @wrapper 構文で関数変換として適用されます。デコレータの一般的な利用例は、 classmethod() と staticmethod() です。

とあります。具体的な例で確認していきますが、ざっくりいいますとデコレータは「関数を引数として受け取り、関数を返り値として返す関数」のことで、使用するためには@記号を使用するということに着目してください。

def do_twice(func):
    def wrapper_do_twice(*args, **kwargs):
        func(*args, **kwargs)
        func(*args, **kwargs)
    return wrapper_do_twice

@do_twice
def return_greeting(name):
    print(f"Hello {name}!")

上記コードではdo_twiceというメソッドは引数にfunc、つまり関数を受け取り、それを2回呼び出します。次に、return_greetingというメソッドには@do_twiceというデコレータが付与されています。
この場合にreturn_greeting("Python")を呼び出すと

Hello Python!
Hello Python!

と出力されるはずです。これはdo_twiceの内側でreturn_greetingが2度呼び出されたことに依ります。
このようにデコレータは関数を引数として受け取り、関数を返り値として返すことができます。(復習するまでは@はJavaでいうアノテーションかなんかだろうと考えていました...w)

さて、getter/setterの話に戻りますと、Pythonにはpropertyというクラスがあります:

class property(
    fget: ((Any) -> Any) | None = ...,
    fset: ((Any, Any) -> None) | None = ...,
    fdel: ((Any) -> None) | None = ...,
    doc: str | None = ...
)

引数はそれぞれ次のようになっています:

  • fget : 属性値を取得する関数
  • fset : 属性値をセットする関数
  • fdel : 属性値を削除する関数
  • doc : docstring

こちらを使用した実装は例えば次のような感じです:

class PropTest:
    def __init__(self):
        self._x = None

    def getx(self):
        return self._x
    
    def setx(self, value):
        self._x = value

    def delx(self):
        del self._x

    x = property(getx, setx, delx, "I'm the 'x' property.")

こちらをインスタンス生成して使用する場合は次の通り:

test = PropTest()
test.setx(12345)
print(test.getx()) # 12345
test.delx()
test.setx(123)
print(test.getx()) # 123

propertyとデコレータを組み合わせると次のようなクラスを定義することができます:

class DecoTest:
    def __init__(self):
        self._x = None

    @property
    def x(self):
        return self._x
    
    @x.setter
    def x(self, value):
        self._x = value

    @x.deleter
    def x(self):
        self._x = None

こちらのクラスをインスタンス生成後、

  • メンバにアクセスするには : InstanceObj.x
  • メンバに値をセットするには : InstanceObj.x = "Value"
  • メンバにセットされた値を削除するには : del(InstanceObj.x)

実際に使用した例は次の通り:

deco_test = DecoTest()
deco_test.x = "Python"
print(deco_test.x) # Python
del(deco_test.x)
print(deco_test.x) # None

このようにしてgetter/setterを定義することができます。最後に、少しコメント:

  • xというメソッド(getterのこと)は@propertyを使用しているため、返し値はpropertyクラス。
  • @x.setterの部分をコメントアウトすると、メンバxの値を設定することができない(エラーが生じる)、つまりこの場合はメンバの値についてRead-Onlyとなる。
  • @x.deleterは実際、使う機会はあまりなさそう。
  • propertyにはgetter/setter/deleterというメソッドはあるがdocstringに関するメソッドはなかった。また、x.の後ろのsetterおよびdeleterはこのメソッド名からきている。

6. Iteratorについて

公式ドキュメントにあった、クラスにiteratorの振る舞いを追加する方法をこちらで紹介。
その前に、for文を使うと

for element in [1, 2, 3]:
    print(element)
for element in (1, 2, 3):
    print(element)
for key in {'one':1, 'two':2}:
    print(key)
for char in "123":
    print(char)
for key, value in zip(["key1","key2","key3"],["value1","value2","value3"]):
    print(f"{key} : {value}")

ほとんどのコンテナオブジェクトにわたってループを行うことが可能です。ここでPythonにおけるコンテナにはdictlistsettupleがあり、さらに特殊なコンテナ型が実装されたcollectionsモジュールがあります。

for文のメカニズムは

  • コンテナオブジェクトに対してiter()関数を呼び出す
  • 呼び出された関数は、コンテナの中の要素に1つずつアクセスする__next()__メソッドが定義されているiteratorオブジェクトを返す
  • アクセスできる要素がない場合、__next__()メソッドはStopIterationを送出
  • forループが終了

となっています。ここで、iter関数を紹介:

iter(object, sentinel)

iteratorオブジェクトを返す。第2引数sentinelの有無によって関数の挙動が変わるので注意です。例えば、第1引数のみであれば

for i in iter([1,2,3,4,5]):
    print(i)

のようにリストを渡すことが可能です。しかし、第2引数に例えばsentinel(番兵)として3を渡してみると

TypeError: iter(v, w): v must be callable

といったエラーが生じます。このエラーはリストが関数のような呼び出し可能なオブジェクトでないことから生じています。ちなみに、番兵は与えられた値が渡されたときに、forループを終了させるように働きます。

番兵を使用した例が次の通り(参照にしたのはこちら):

from functools import partial
from random import randint

for i in iter(partial(randint,1,6), 6):
    print(i)
print("Stop for-loop by sentinel(6)")

partial(func,/,*args,**kwargs)オブジェクトは呼び出されたときに、argskwargs付きで呼び出されたfuncのように振る舞う、つまり今回のコードではrandint(1,6)を呼び出します。

iteratorの仕組みから、クラスにiteratorの振る舞いを追加するには次の2つのメソッドを定義します:

  • __next__()メソッド
  • __iter__()メソッド:__next__()メソッドをもつオブジェクトを返す

上記メソッドを定義したクラスが以下の通り:

class Reverse:
    """Iterator for looping over a sequence backwards."""
    def __init__(self, data):
        self.data = data
        self.index = len(data)

    def __iter__(self):
        return self # __next__()を持つReverseを返す

    def __next__(self):
        if self.index == 0:
            raise StopIteration
        self.index = self.index - 1
        return self.data[self.index]

こちらのクラスは

for char in Reverse("Python"):
    print(char)

のように使用することができます。

Generator

generatorを用いると上記クラスについて、もっとコンパクトに実装が可能です。

ジェネレータ は、イテレータを作成するための簡潔で強力なツールです。ジェネレータは通常の関数のように書かれますが、何らかのデータを返すときには yield 文を使います。そのジェネレータに対して next() が呼び出されるたびに、ジェネレータは以前に中断した処理を再開します (ジェネレータは、全てのデータ値と最後にどの文が実行されたかを記憶しています)。

generatorは次のような機能を有しています:

  • __iter__()メソッドと__next__()メソッドが自動で作成される
  • 呼び出しごとにローカル変数と実行状態が自動的に保存される
  • 終了時に自動的にStopIterationを送出する

この機能を使用すると、Reverseクラスと同じようなiteratorを次のように実現可能です:

def reverse(data):
    for index in range(len(data)-1, -1, -1):
        yield data[index] # 関数定義内で`yield`を使用することで、`reverse`関数はgenerator関数となる

出力はReverseクラスと同じになるように次のようにしました:

chars = reverse("Python")
for char in chars:
    print(char)

print(type(chars)) # <class generator>

7. まとめ

今回は、PythonのClassについて復習したものに関する記事でした。Python特有の事情や定義方法があるため調べる対象として非常に捗りました。
そのせいもあって、思ったよりも寄り道が多くなってしまいました。
恐らく、一通りは復習できたと思います。
次回は、現在放置中のFletのクラス化に取り組んでいきます。

9
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
11