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

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

https://qiita.com/oriefield/items/473049ca2cbc05ce4c6a

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

主に、[Pythonの公式ドキュメント](https://docs.python.org/ja/3/tutorial/classes.html)を参考にし、その概要の紹介となります。

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

## 1. Classとは

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

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

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

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

ここでオブジェクト指向プログラミングについては[こちら](https://ja.wikipedia.org/wiki/%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E6%8C%87%E5%90%91%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0)を参照していただき、基本概念

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

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

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

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

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

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


## 2. Scopeについて

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

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

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

となっている。

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

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

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

```python: 変数の束縛についての例
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
```

上記コードの出力は次の通り：
```python
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`を用いて、

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

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

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

```python
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__`メソッドは例えば、`MyClass`を`print`した時の表現を与える特殊メソッドです。

特殊メソッドについては、[こちら](https://docs.python.org/ja/3/reference/datamodel.html)を参照してください。


**属性参照（attribute reference）**

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


**インスタンス化（instantiation）**

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

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

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

```python
class1_data = class1.data
class1_hello = class1.say_hello
```

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

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

 * class1_hello()
 * MyClass.say_hello(class1)

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

```python
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)}")
```

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

```python
type1: Hello Sample!
type2: Hello Sample!
```

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


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

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

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

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

```python
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__`メソッドの返し値の形式を変更します。例えば、

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

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

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

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


**属性名の注意点**

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

例えば：

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

というクラスに対して

```python
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)`と呼びます。
派生クラスは次のように定義される：

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

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

```python
class DerivedClassName(modname.BaseClassName):
```

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

```python
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）を行うことが可能なことも注目の点です。
ちなみに、[ドキュメント](https://docs.python.org/3/library/functions.html#super)によると、

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

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



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

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


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

```
_privateVar
```

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

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

```python
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`というメソッドが基底クラスで定義され、派生クラスにおいてオーバーライドが行われています。例えば、次のようにインスタンス生成してみます：

```python
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で記述する場合を紹介します。そのために、まずは`デコレータ`を紹介する必要があります。デコレータについては、英語の文献にはなりますが、[こちら](https://realpython.com/primer-on-python-decorators/)に詳しく記載されていました。

デコレータとは

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

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

```python
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`というクラスがあります：

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

引数はそれぞれ次のようになっています：
 
 * fget : 属性値を取得する関数
 * fset : 属性値をセットする関数
 * fdel : 属性値を削除する関数
 * doc  : docstring

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

```python
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.")
```

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

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

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

```python
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)`

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

```python
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文`を使うと

```python
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におけるコンテナには`dict`や`list`、`set`、`tuple`があり、さらに特殊なコンテナ型が実装された`collections`モジュールがあります。

`for文`のメカニズムは

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

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

**iter(object, sentinel)**

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

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

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

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

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

`番兵`を使用した例が次の通り（参照にしたのは[こちら](https://stackoverflow.com/questions/38087427/what-are-the-uses-of-itercallable-sentinel)）：
```python
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)`オブジェクトは呼び出されたときに、`args`と`kwargs`付きで呼び出された`func`のように振る舞う、つまり今回のコードでは`randint(1,6)`を呼び出します。

iteratorの仕組みから、クラスにiteratorの振る舞いを追加するには次の2つのメソッドを定義します：
 
 * `__next__()`メソッド
 * `__iter__()`メソッド：`__next__()`メソッドをもつオブジェクトを返す
 
上記メソッドを定義したクラスが以下の通り：

```python
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]
```

こちらのクラスは

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

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


**Generator**

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

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

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

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

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

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

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

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

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

# 7. まとめ

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