Pythonのクラス（Python学習メモ⑦）

# スコープと名前空間について
## 名前空間
ポイント

* 名前空間とは、名前とオブジェクトの対応付けの事
* 名前空間に紐づく属性は`obj.attribute`のようにドットでつなぐ

## スコープ
ポイント

* スコープとは、スコープある名前空間から直接アクセスできるプログラムテキスト上の範囲の事
* スコープの参照順は以下
    * ローカル名の入ったスコープ
    * ↑を取り囲む関数がある場合、その関数のスコープ
    * 今いるモジュールのグローバルなスコープ
    * 最も外側のスコープはビルトイン名の入ったスコープ
* global文が使用されていない限り、代入や削除は常に最も内側のスコープにおいて行われる
* global文を使うと、その変数がグローバルスコープにあることを示す
* nonlocal分は、その変数が現在のスコープを取り囲むスコープにあることを示す

```Python:スコープと名前空間の例
def scope_test():
    def do_local():
        spam = "local spam"
    def do_nonlocal():
        nonlocal spam
        spam = "nonlocal spam"
    def do_global():
        global spam
        spam = "global spam"
    spam = "test spam"
    
    do_local()
    print("After local assignment:", spam)
    # After local assignment: test spam
    
    do_nonlocal()
    print("After nonlocal assignment:", spam)
    # After nonlocal assignment: nonlocal spam
    
    do_global()
    print("After global assignment:", spam)
    # After global assignment: nonlocal spam

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

```

# クラス定義
## クラス定義の構文
```python:クラス定義
class className:
    <文1>
    .
    .
    .
    <文N>
```

ポイント

* クラス定義に入ると新しい名前空間が生成され、  
ローカルスコープとして使われる(ローカル変数、ローカル関数はこのスコープ)
* クラス定義の実行が終わるとクラスオブジェクトが生成される(基本的には名前空間を包むラッパーに過ぎない)

## クラスオブジェクト
ポイント

* クラスオブジェクトは、属性参照、インスタンス化の2種の操作をサポートする
* 属性参照は`obj.name`の形で行われる
* 属性参照はクラスの名前空間に存在するすべての名前(以下のような場合、`MyClass.i`と`MyClass.f`が有効)

```python:属性参照
class MyClass:
    """A simple example class"""
    i = 12345
    def f(self):
        return 'hello world'

```

* クラスのインスタンス化は関数のように呼び出す

```python:クラスのインスタンス化
x = MyClass()
```

* この時、`__init__()`メソッドが定義してあると、新規作成したインスタンスに対し自動的に`__init()__`がコールされる

```python:__init__()関数
    def __init__(self):
        self.data = []
```

* `__init__()`関数には引数を与えることができる
* 引数はクラスのインスタンス化の時に与えた引数が使用される

```python:クラスのインスタンス化(引数あり)
class Human:
    def __init__(self, height, weight):
        self.height = height
        self.weight = weight

x = Human(165, 55)
print('height:', x.height, 'weight:', x.weight)
# height: 165 weight: 55
```

## インスタンスオブジェクト
ポイント

* インスタンスには「データ属性」と「メソッド」の2つの属性参照がある

```python:データ属性とメソッドの参照
class MyClass:
    def __init__(self):
        pass
    def f(self, word):
        print(word)
        
x = MyClass()

# データ属性の追加・参照
x.counter = 1 # データ属性は事前に宣言する必要はない
while x.counter < 10:
    x.counter = x.counter * 2
print(x.counter) # 16
del x.counter

# メソッドの参照
x.f("hello kawauso")
```

## メソッドオブジェクト

* メソッドは変数に格納できる
* メソッドの特徴は第1引数にインスタンスのオブジェクトが渡されること
* x.f()のコールはMyClass.f(x)と等価である

```python:メソッドオブジェクトを変数に格納
xf = x.f
xf('hello kawauso again') # 出力: hello kawauso again
```

## クラス変数とインスタンス変数
ポイント

* クラスで共通のクラス変数とインスタンス毎に固有のインスタンス変数がある
* インスタンス毎に値の変更が必要なものはクラス変数にはしないこと

```python:クラス変数とインスタンス変数の例

class Dog:
    
    kind = '柴' # クラス変数
    
    def __init__(self, name):
        self.name = name # インスタンス変数
        
        
taro = Dog('太郎')
jiro = Dog('次郎')

# クラス変数の参照
print(taro.kind) # 柴
print(jiro.kind) # 柴

# インスタンス変数の参照
print(taro.name) # 太郎
print(jiro.name) # 次郎
```

## その他いろいろ
ポイント

* データ属性は同名のメソッドをオーバーライドしてしまうので名前の重複は命名規則などで避ける
* データ属性はどこからでも操作できる(クラスによる隠ぺいがされない)
* メソッドの第1引数は慣習的に`self`とする(予約語ではないがこの方がみんな慣れているから)
* メソッドのグローバルスコープはそのメソッドが定義されたモジュール(クラスがグローバルスコープとして使われることはない)
* グローバルスコープにインポートされた関数やモジュールももちろん使用可能
* メソッドのクラスそのものもグローバルスコープに定義されているため参照可能

# 継承
## クラスの継承
ポイント

* 継承はクラスの引数に基底クラス名を渡す
* 別モジュールからも渡すことができる

```python:継承のサンプル
class base():
    def a(self):
        print('私はbase.aです.base.bをコールします')
        self.b()
    def b(self):
        print('私はbase.bです.der.bでオーバーライドされます')

class der(base):
    def b(self):
        print('私はder.bです.')


b = base()
d = der()

b.a()
# 私はbase.aです.base.bをコールします
# 私はbase.bです.der.bでオーバーライドされます

d.a()
# 私はbase.aです.base.bをコールします
# 私はder.bです.
```

* インスタンスの型をチェックするにはビルトイン関数、`isinstance(obj, int)`を使用する
* クラス継承のチェックには`issubclass()`を使用する

## 多重継承
ポイント

* Pythonでは複数の基底クラスを持つクラス定義が可能

※ボリュームありそうなので、これについては別でまとめる

# プライベート変数
ポイント

* Pythonにはオブジェクト内部からしかアクセスできないプライベートなインスタンス変数は存在しない
* 慣習として`_`が前置された名前はAPIの非公開部分とする
* クラスプライベートメンバについては、名前マンダリングというサブクラスとの名前衝突を防ぐ仕組みがある
* これは`__spam`のような形の識別子をすべて`_classname__spam`の形に置き換えるというもの

```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 # ↑のupdate()メソッドのプライベートコピー

class MappingSubclass(Mapping):
    # update()の新しいシグネチャを提供しつつ
    # 既存の__init__()は破壊せずに利用できる
    def update(self, iterable):
        for item in zip(keys, values):
            self.items_list.append(item)
```


# その他のあれこれ
* 名前の付いたデータアイテムを埋めていく場合は空のクラス定義を使うと便利

```python
class Employee:
    pass

john = Employee()

john.name = 'Jhon Doe'
john.dept = 'computer lab'
john.salary = 1000
```

* 特定のデータ型を想定したPythonコードに、そのデータ型のメソッドをエミュレートしたクラスを渡すことはよくある。例えば、文字列バッファからデータを得るクラスに`read()`または`readline()`メソッドを定義すると、ファイルオブジェクトからデータを受け取って整形する関数に、引数として渡せるようになる
* インスタンスメソッドオブジェクトにも属性がある。メソッド`m()`に対して、インスタンスオブジェクトは`m.__self__`であり、メソッドに対応した関数オブジェクトは`m.__func__`である

# 例外もクラス
ポイント

* ユーザー定義例外もクラスとされるため、これを利用して拡張可能な階層型の例外体系が作れる

# 反復子(iterator)
ポイント

* for文では内部的に、コンテナオブジェクトに対して、`iter()`をコールするようになっている
* 反復子オブジェクトには`__next__()`が備わっており、要素が尽きると`StopIteration`例外を送出し、forループはこれを受けて終了する
* 自作のクラスに反復子のふるまいを追加するためにはこの仕組みを利用して以下のように定義すればよい

```python:__iter__()と__next__()
# __next__()メソッドの付いたオブジェクトを返す__iter__()メソッドを定義する
# すでに__next()__が定義してあるクラスでは__iter__()はselfを返すだけでよい
class Reverse:
    def __init__(self, data):
        self.data = data
        self.index = len(data)
    def __iter__(self):
        return self
    def __next__(self):
        if self.index == 0:
            raise StopIteration
        self.index = self.index - 1
        return self.data[self.index]

rev = Reverse('spam')
iter(rev)

for char in rev:
    print(char)

# m
# a
# p
# s
```

# ジェネレータ
ポイント

* ジェネレータはシンプルに反復子を作るためのツール
* 普通の関数と同じように書くが、データを返す部分で`yield`を使う
* next()がコールされるたびにジェネレータは前回抜けたところに戻る
* ジェネレータは`__iter__()`と`__next__()`のメソッドを自動的に生成するため、簡単に反復子を生成する
* ジェネレータはローカル変数と実行状態をコール間で自動保存する
* ジェネレータは終了時に、自動的にStopIterationを送出する

```python:ジェネレータの例
def reverse(data):
    for index in range(len(data)-1, -1, -1):
        yield data[index]

for char in reverse('golf'):
    print(char)

# f
# l
# o
# g
```

## ジェネレータ式
ポイント

* 簡単なジェネレータであれば`()`を使って簡単にかける

```python:ジェネレータ式の例
sum(i*i for i in range(10)) # 2乗して合計

xvec = [10, 20, 30]
yvec = [7, 5, 3]
sum(x*y for x,y in zip(xvec, yvec)) # 内積

from math import pi, sin
# sin表
sine_table = {x: sin(x*pi/180) for x in range(0, 91)}

# ページの中の重複しない単語
unique_words = set(word for line in page for word in line.split())

# 卒業生総代
valedictorian = max((student.gpa, student.name) for student in graduates)

data = 'golf'
list(data[i] for i in rage(len(data)-1, 1, -1))
```