[Python] クラス メモ

単語帳＝毎回検索するのが面倒なので転載多め．元URLあり．
主に自作クラス関連．


# クラスの定義


### 抽象クラス

テンプレート

```Python
from abc import ABCMeta, abstractmethod

# 抽象クラス
class AbstClass(metaclass=ABCMeta):

    @abstractmethod
    def my_abstract_method(self):
        pass

    @property
    @abstractmethod
    def my_abstract_property(self):
        pass
```

[Qiita@kaneshin: PythonのABC - 抽象クラスとダック・タイピング]
(https://qiita.com/kaneshin/items/269bc5f156d86f8a91c4)
[stackoverflow: How to create abstract properties in python abstract classes]
(https://stackoverflow.com/questions/5960337/how-to-create-abstract-properties-in-python-abstract-classes)


※親クラス (含抽象クラス) ではメソッドの引数の個数などを拘束できない
(オーバーライドすると，サブクラスでの定義が優先される)

```Python
from abc import ABCMeta, abstractmethod

class AbstClass(metaclass=ABCMeta):
    @abstractmethod
    def mymethod(self, arg1, arg2):
        pass

class SuperClass:
    def mymethod(self, arg1, arg2):
        pass

class MyClass(AbstClass):
#class MyClass(SuperClass): 同じく拘束できない
    def __init__(self):
        pass

    def mymethod(self, arg1):
        print(arg1)


obj = MyClass()
obj.mymethod(1)
obj.mymethod(1, 2) # TypeError: mymethod() takes 2 positional arguments but 3 were given
```

[stackoverflow: Abstract classes with varying amounts of parameters]
(https://stackoverflow.com/questions/42778784/abstract-classes-with-varying-amounts-of-parameters)




### `@abstractmethod`の'__' (アンダースコア2つ) で始まるメソッドをオーバーライドするには

通常の方法ではオーバーライドできない．
(抽象クラスの継承時にこの問題が起きると`Can't instantiate abstract class … with abstract methods`のエラー．)
`_親クラス名__メソッド名`でオーバーライド．

`@abstractmethod`を使わない場合，この問題は起こらない:

```Python
class BaseClass0:

    def __init__(self): pass

    def mymethod0(self):
        print('BaseClass0.mymethod0')

    def _mymethod1(self):
        print('BaseClass0._mymethod1')

    def __mymethod2(self):
        print('BaseClass0.__mymethod2')

    def mymethod2(self):
        print('BaseClass0.mymethod2')
        self.__mymethod2()

class BaseClass1(metaclass=ABCMeta):

    def __init__(self): pass

    def mymethod0(self):
        print('BaseClass1.mymethod0')

    def _mymethod1(self):
        print('BaseClass1._mymethod1')

    def __mymethod2(self):
        print('BaseClass1.__mymethod2')

    def mymethod2(self):
        print('BaseClass1.mymethod2')
        self.__mymethod2()

class SubClass0(BaseClass0):

    def __init__(self): pass

    def mymethod0(self):
        print('SubClass0.mymethod0')

    def _mymethod1(self):
        print('SubClass0._mymethod1')

    def __mymethod2(self):
        print('SubClass0.__mymethod2')

    def mymethod2(self):
        print('SubClass0.mymethod2')
        self.__mymethod2()


class SubClass1(BaseClass1):

    def __init__(self): pass

    def mymethod0(self):
        print('SubClass1.mymethod0')

    def _mymethod1(self):
        print('SubClass1._mymethod1')

    def __mymethod2(self):
        print('SubClass1.__mymethod2')

    def mymethod2(self):
        print('SubClass1.mymethod2')
        self.__mymethod2()

for obj in [BaseClass0(), SubClass0(), BaseClass1(), SubClass1()]:
    obj.mymethod0()
    obj._mymethod1()
    obj.mymethod2()
```


しかし，親クラスに`@abstractmethod`をつけた途端，インスタンスの生成も出来なくなる:

```Python
class BaseClass2(metaclass=ABCMeta):

    @abstractmethod
    def mymethod0(self): pass

    @abstractmethod
    def _mymethod1(self): pass

    @abstractmethod
    def __mymethod2(self): pass

    @abstractmethod
    def mymethod2(self): pass

class SubClass2(BaseClass2):

    def __init__(self): pass

    def mymethod0(self):
        print('SubClass2.mymethod0')

    def _mymethod1(self):
        print('SubClass2._mymethod1')

    def __mymethod2(self):
        print('SubClass2.__mymethod2')

    def mymethod2(self):
        print('SubClass2.mymethod2')
        self.__mymethod2()

obj = SubClass2() # TypeError: Can't instantiate abstract class SubClass2 with abstract methods _BaseClass2__mymethod2
```

これを解決するためには，

- `__mymethod2`の代わりに`_BaseClass2__mymethod2`をオーバーライド
- `self.__mymethod2()`では呼び出しが出来ないので，これを`self._BaseClass2__mymethod2()`に変更

の2点を行う:

```Python
class SubClass2_ver2(BaseClass2):

    def __init__(self): pass

    def mymethod0(self):
        print('SubClass2.mymethod0')

    def _mymethod1(self):
        print('SubClass2._mymethod1')

    def _BaseClass2__mymethod2(self):
        print('SubClass2.__mymethod2')

    def mymethod2(self):
        print('SubClass2.mymethod2')
        self._BaseClass2__mymethod2()
        # self.__mymethod2() AttributeError: 'SubClass2_ver2' object has no attribute '_SubClass2_ver2__mymethod2'

obj = SubClass2_ver2()
obj.mymethod0()
obj._mymethod1()
obj.mymethod2()
```





[stackoverflow: Can't instantiate abstract class … with abstract methods]
(https://stackoverflow.com/a/31458576)





### `class MyClass(object):`の`(object)`は省略可能 (Python3のみ)

元々クラスには2種類 (Python2.1以前と以降) の2種類があり，
Python2だと`(object)`を

- 明記: 新スタイル
- 省略: 旧スタイル，`@property`のsetterが動かなかったり

として区別される．Python3では新スタイルに統一されている．

[Qiita@alt-core: Python2.7で class C: と class C(object): の違いに大ハマりした話]
(https://qiita.com/alt-core/items/d20e9a1864ff3a35946e)





### クラスのキャスト

[民主主義に乾杯: クラスをキャストする]
(https://python.ms/cast/)













# メソッド

## 特殊メソッド

一覧:
[公式: 3. Data model > 3.3. Special method names]
(https://docs.python.org/3/reference/datamodel.html#special-method-names)

`__contains(self, item)__`: `item in self`
[stackoverflow: Override Python's 'in' operator?]
(https://stackoverflow.com/questions/2217001/override-pythons-in-operator)

`__getattr__`と`__getattribute__`

- `__getattr__`: 未定義のメンバーにアクセスするとき
    - 明示的に定義していない特殊メソッドにアクセスしたときも
- `__getattribute__`: 未定義・定義済み関わらず、すべてのメンバーアクセスで

[SDN開発エンジニアを目指した活動ブログ: Python勉強メモ：特殊メソッド__getattribute__とは？]
(http://ttsubo.hatenablog.com/entry/2014/05/04/215202)
[Qiita@fumitoh: ドットによるオブジェクトの属性の取得をカスタマイズするための\_\_getattr\_\_と\_\_getattribute\_\_]
(https://qiita.com/fumitoh/items/b090ae6ccd52e6a88d9e)

`__call__`の使い方

[Qiita@ganariya: 【Python中級者への道】\_\_call\_\_でクラスインスタンスを関数のように呼び出す]
(https://qiita.com/ganariya/items/27ade6b149728f6aae88)



### `__getattr__`の副作用


1つ1つ属性やゲッターを定義しなくても良くなる半面，副作用もある:

- pickle化できなくなる
- `copy.copy`や`copy.deepcopy`が動作しなくなる

解決策としては,

- `__getattr__`内で特殊メソッドを除外する (色々な問題に効果あり)
- 各問題に対して処方箋


#### pickle化できなくなる例

##### pickle化できるかどうかの確認方法

```Python
import pickle
def is_picklable(obj):
    try:
        pickle.dumps(obj)
    except:
        return False
    return True
```

[stackoverflow: How to check if an object is pickleable]
(https://stackoverflow.com/questions/17872056/how-to-check-if-an-object-is-pickleable)
では`pickle.PicklingError`という例外をキャッチしているが，今回の例では他の例外 (`'str' object is not callable`) も発生する．


##### 基本ケース

```Python
class MyClass:
    def __init__(self, val):
        self.val = val

obj = MyClass(1)
print(is_picklable(obj)) # True
```

##### `__getattr__`を追加で定義

```Python
class MyClassGA:
    def __init__(self, val):
        self.val = val

    def __getattr__(self, attr):
        return '{0} {1}'.format(attr, self.val)

obj = MyClassGA(1)
print(obj.foo) # foo1
print(is_picklable(obj)) # False
```

##### 解決策1: `__getattr__`内で特殊メソッドを除外する

```Python
class MyClassPickleble:
    def __init__(self, val):
        self.val = val

    def __getattr__(self, attr):
        if attr.startswith('__') and attr.endswith('__'):
            raise AttributeError
        return '{0} {1}'.format(attr, self.val)

obj = MyClassPickleble(1)
print(obj.foo) # foo 1
print(is_picklable(obj)) # True
```

##### 解決策2: `__getstate__`と`__setstate__`を追加で定義

```Python
class MyClassPickleble2:
    def __init__(self, val):
        self.val = val

    def __getattr__(self, attr):
        return '{0} {1}'.format(attr, self.val)

    def __getstate__(self):
        return self.__dict__
    
    def __setstate__(self, d):
        return self.__dict__.update(d)

obj = MyClassPickleble2(1)
print(obj.foo) # foo
print(is_picklable(obj)) # True
```

[stackoverflow: Pickle of object with \_\_getattr\_\_ method in Python returns `TypeError, object not callable`]
(https://stackoverflow.com/questions/50888391/pickle-of-object-with-getattr-method-in-python-returns-typeerror-object-no)
[銀月の符号: pickle できない属性をもつクラスへの対策例]
(https://fgshun.hatenablog.com/entry/20080812/1218559308)
[Python: Python deepcopy with custom \_\_getattr\_\_ and \_\_setattr\_\_]
(https://stackoverflow.com/questions/40583131/python-deepcopy-with-custom-getattr-and-setattr)










## 文字列による動的なメソッドの呼び出し

`getattr(オブジェクト, 'メソッド名')(引数)`でOK．

```Python
adict = {'a': 1}
getattr(adict, 'get')('a', None)
```

[やつらかやつらだ: Pythonで動的にメソッドを呼ぶ]
(https://themorthem.hatenadiary.org/entry/20110209/1297264984)








## クラス/インスタンスに後からメソッドを追加する

```Python
def fooFighters(self):
    print "fooFighters"

A.fooFighters = fooFighters # クラス自体にfooFightersメソッドを追加
a.barFighters = types.MethodType(barFighters, a) # インスタンスだけにbarFightersメソッドを追加

a.barFighters = barFighters # これではselfが使えない (単なるプロパティとして追加されてしまう→メソッドだとは思われない)
```

[stackoverflow: Adding a Method to an Existing Object Instance]
(https://stackoverflow.com/questions/972/adding-a-method-to-an-existing-object-instance)
[Ian Lewis: Pythonでメソッドをクラスまたはインスタンスに動的に追加する]
(https://www.ianlewis.org/jp/python-add-class-method)


インスタンスへの追加に`setattr`を使う時

```Python
methods = [method0, method1]
for method in methods:
    setattr(obj, method.__name__, types.MethodType(method, obj))
```


**※これらの方法は特殊メソッドには適用不可**
e.g. `__add__`をある`obj`に追加すると，`obj.__add__(item)`はできても`obj + item`は不可
上記はインスタンスにメソッドを追加するが，特殊メソッドを追加・上書きするにはクラスでの定義自体を変更する必要がある．
[stackoverflow: Setting special methods using setattr()]
(https://stackoverflow.com/questions/10484304/setting-special-methods-using-setattr)
[stackoverflow: Overriding special methods on an instance]
(https://stackoverflow.com/questions/10376604/overriding-special-methods-on-an-instance)







## イテレータ

[Qiita@tchnkmr: [Python] イテレータを実装する]
(https://qiita.com/tchnkmr/items/e740173d7400f8672d75)
[くろのて: [Python] 部屋とYシャツとイテレータとジェネレータと私]
(https://note.crohaco.net/2016/python-iterator-generator-and-tshirt-me/)
[デジタリ: PythonのIteratorのちょっと詳しい説明]
(https://digitali.biz/python%E3%81%AEiterator%E3%81%AE%E3%81%A1%E3%82%87%E3%81%A3%E3%81%A8%E8%A9%B3%E3%81%97%E3%81%84%E8%AA%AC%E6%98%8E/)

### `list()`で`__iter__`の全要素を取得可能

```Python
class MyClass:

    def __init__(self): pass

    def __iter__(self):
        for i in range(3):
            yield i

obj = MyClass()
for i in obj:
    print(i)
# 0
# 1
# 2

obj = MyClass()
print(list(obj)) # [0, 1, 2]
```

`__call__`経由で引数を渡しても期待通りの動作になる．

```Python
class MyClass:

    def __init__(self, n=None):
        self.n = n

    def __iter__(self):
        for i in range(self.n):
            yield i
    
    def __call__(self, n):
        self.n = n
        return self

obj = MyClass()
for i in obj(3):
    print(i)
# 0
# 1
# 2

obj = MyClass()
print(list(obj(5))) # [0, 1, 2, 3, 4]
```


# デコレータ

### classmethod, staticmethod

[DjangoBrothers: 【Python】インスタンスメソッド、staticmethod、classmethodの違いと使い方](https://djangobrothers.com/blogs/class_instance_staticmethod_classmethod_difference/)

### property

Effective Pythonに












# その他

### `getattr(obj, attr_name, default_value)`

attributeが存在しないときにはデフォルト値を返す

```Python
import datetime as dt
a = dt.date(2000, 1, 1)
print(a.year)
print(getattr(a, 'years', 'not found'))
```



### あるオブジェクトがiterableか確認

```Python
from collections.abc import Iterable   # drop `.abc` with Python 2.7 or lower
isinstance(obj, Iterable)
```

[stackoverflow: In Python, how do I determine if an object is iterable?]
(https://stackoverflow.com/questions/1952464/in-python-how-do-i-determine-if-an-object-is-iterable)


時々紹介されている`hasattr(obj, '__iter__')`は必ずしも正しくない．
イテレータは`__iter__`ではなく`__getitem__`でも動作するからである．
（但し，`__getitem__`はint以外でも動作するため，`__getitem__`があるからiterableだ，というのは誤り．）

[民主主義に乾杯: イテラブル, iterable ってなに？]
(https://python.ms/iterable/)