Pythonでメソッドのオーバーライドの抑制

最初3で投稿しましたが，1の昨日分が未投稿で空いていたので移動させました．

継承時のメソッドのオーバーライドの禁止は，例えばJavaやC++では`final`キーワードで実現できますが，Pythonの標準機能としてはありません．
ということで，Python3においてどうやってやればよいかを調べたり，考えたりしました．

## 環境

- Ubuntu 18.04
- Python 3.6
- mypy: 0.750

## 実装

今回の実装は全て，https://github.com/eduidl/python-sandbox/tree/master/prohibit_override に置いています．

## mypy

- https://github.com/python/mypy

Python 3.5から型ヒントが導入されましたが，その型ヒントを使って静的に型チェックを行うためのツールです．
そんなmypyですが，`@final`というデコレータを用いて，オーバーライドをしてはいけないメソッドであることを表明することができます．

使い方としては以下の通りです．`@final`を使って，`hello`というfinalメソッドだと表明しています．

```py:mypy.py
from typing_extensions import final

class Base:
    @final
    def hello(self) -> None:
        print("hello")

class Derived(Base):
    def hello(self) -> None:
        print("こんにちは")

Base().hello()
Derived().hello()
```

このファイルに対してmypyを使うと警告を出してくれます．

```console
$ mypy mypy.py
mypy.py:13: error: Cannot override final attribute "hello" (previously declared in base class "Base")
Found 1 error in 1 file (checked 1 source file)
```

ただ，Type Hintsと同様に実行時に影響は無いのでその点注意が必要です．

```console
$ python3 mypy.py
hello
こんにちは
```

### Pros
- 既にmypyを使っている人にとっては導入が楽
- 定数とかにも使えるっぽい

### Cons
- 実行時にエラーにならないし，警告も出ない
- 導入自体はpipで入れられるものの，型ヒントを書いていない人にとっては導入コストが高い

### 参考
- https://mypy.readthedocs.io/en/latest/final_attrs.html

### 追記

Python 3.8以降では，標準ライブラリの `typing` モジュールにも実装されたようです．とはいえ，mypyを含めた何らかのtype checkerが必要なことには変わりありませんが．

https://docs.python.org/ja/3.8/library/typing.html#typing.final

※ 追記ここまで

## `__init_subclass__` の利用

mypyによる静的解析もよいのですが，実行時にチェックして例外が上がる方が自分としては嬉しいものです．
イメージとしては，標準ライブラリの抽象基底クラスモジュールである`abc` (https://docs.python.org/ja/3/library/abc.html) のような感じでしょうか．

ということで，https://github.com/python/cpython/blob/3.8/Lib/abc.py を参考に実装してみたのがこれです（参考と言っても`__isfinalmethod__`くらいしか残っていない気がしますが）．
全てのメソッドを基底クラスと突き合わせているだけの単純な実装です．

```py
import inspect
from typing import Any, Callable, List, Tuple

AnyCallable = Callable[..., Any]


def final(funcobj: AnyCallable) -> AnyCallable:
    setattr(funcobj, '__isfinalmethod__', True)
    return funcobj


def get_func_type(cls: type, func_name: str) -> str:
    func = getattr(cls, func_name)

    if isinstance(func, classmethod):
        return 'class method'
    elif isinstance(func, staticmethod):
        return 'static method'
    else:
        return 'member function'


class Final:

    def __init_subclass__(cls, **kwargs) -> None:
        for func_name, func in cls.get_methods():
            for ancestor in cls.__bases__:
                if ancestor == object or not hasattr(cls, func_name):
                    continue
                ancestor_func = getattr(ancestor, func_name, None)
                if not ancestor_func or not getattr(ancestor_func, '__isfinalmethod__', False) or \
                        type(func) == type(ancestor_func) and \
                        getattr(func, '__func__', func) == getattr(ancestor_func, '__func__', ancestor_func):
                    continue

                func_type = get_func_type(ancestor, func_name)
                raise TypeError(f'Fail to declare class {cls.__name__}, for override final {func_type}: {func_name}')

    @classmethod
    def get_methods(cls) -> List[Tuple[str, AnyCallable]]:
        return inspect.getmembers(cls, lambda x: inspect.isfunction(x) or inspect.ismethod(x))
```


### 実装の詳細

いつもならサボるところですが，アドベントカレンダーなので簡単に解説します．

#### `final`

```py
def final(funcobj: AnyCallable) -> AnyCallable:
    setattr(funcobj, '__isfinalmethod__', True)
    return funcobj
```

`abc.abstractmethod` の実装（https://github.com/python/cpython/blob/3.8/Lib/abc.py#L7-L25 ）まんまです．PyCharmが警告を吐いてくるので．`setattr`を使いました．

#### `get_func_type`

```py
def get_func_type(cls: type, func_name: str) -> str:
    func = getattr(cls, func_name)

    if isinstance(func, classmethod):
        return 'class method'
    elif isinstance(func, staticmethod):
        return 'static method'
    else:
        return 'member function'
```

エラーメッセージに使うために，`staticmethod`か`classmethod`かメンバ関数かを調べているだけです．

#### `Final`

最初メタクラスを使って書いていましたが，[Python 3.6における『Effective Python』 項目33はこう変わる](https://xaro.hatenablog.jp/entry/2017/01/09/225403) を思い出して，`__init_subclass__` を使って書き換えました（確かに使いやすい）．

#### `Final.get_methods`

```py
@classmethod
def get_methods(cls) -> List[Tuple[str, AnyCallable]]:
    return inspect.getmembers(cls, lambda x: inspect.isfunction(x) or inspect.ismethod(x))
```

`inspect.getmembers` (https://docs.python.org/ja/3/library/inspect.html#inspect.getmembers) は第2引数にpredicateを取り，それが真であるものを返してくれます．
今回は，メンバ関数，static method，class method全てが欲しいので，`inspect.isfunction`か`inspect.ismethod`が真になるものを集めます．


#### `Final.__init_subclass__`

まず，`__init_subclass__`についてですがPython 3.6で導入された機能で，詳細については公式ドキュメント（https://docs.python.org/ja/3/reference/datamodel.html#object.__init_subclass__ ）の記述を引用します．

> このメソッドは、それが定義されたクラスが継承された際に必ず呼び出されます。cls は新しいサブクラスです。

これが全てですが，要するに今までメタクラスを使って書いていたのが，より書きやすくなるわけです．

```py
# Python 3.5まで
# 実際にはExampaleを使うばかりで，ExampaleMetaを直接使うことはない
class ExampaleMeta(type):
    def __new__(mcs, name, bases, attrs):
        cls = super().__new__(mcs, name, bases, attrs)
        some_func(cls)
        return cls

class Exampale(metaclass=ExampaleMeta):
    def __init__(self, args):
        # 色々

# Python 3.6から
class Exampale:
    def __init__(self, args):
        # 色々

    def __init_subclass__(cls, **kwargs):
        some_func(cls)
```

ということで，Python 3.6が使えるのなら，できるだけ `__init_subclass__` を使ったほうがよいと思います．ということで，今回の実装について．

```py
def __init_subclass__(cls, **kwargs) -> None:
    for func_name, func in cls.get_methods():
        for ancestor in cls.__bases__:
            if ancestor == object or not hasattr(cls, func_name):
                continue
            ancestor_func = getattr(ancestor, func_name, None)
            if not ancestor_func or not getattr(ancestor_func, '__isfinalmethod__', False) or \
                    type(func) == type(ancestor_func) and \
                    getattr(func, '__func__', func) == getattr(ancestor_func, '__func__', ancestor_func):
                continue

            func_type = get_func_type(ancestor, func_name)
            raise TypeError(f'Fail to declare class {cls.__name__}, for override final {func_type}: {func_name}')
```

`Final.get_methods`で全てのメソッド，`__bases__`で継承クラスを取得し，それぞれのクラスについて

- 同名のメソッドを持っているか
- 持っていたら`__isfinalmethod__`属性を持っていてそれが`True`でないか
- `__isfinalmethod__`が`True`だったら，オーバーライドしていないか

を調べ該当していたら`raise TypeError`します．ちなみに`abc`も，`TypeError`を投げます．


### 使用例
以下のようなクラスを準備します．

```py
class A(metaclass=FinalMeta):

    @final
    def final_member(self):
        pass

    @classmethod
    @final
    def final_class(cls):
        pass

    @staticmethod
    @final
    def final_static():
        pass

    def overridable(self):
        print("from A")


class B(A):
    pass
```

#### メンバ関数のオーバーライド

いくつかのケースを試してみましたが，上手くいっていそうです．

- Aを直接継承

```py
class C(A):

    def final_member(self) -> None:
        pass
#=> Fail to declare class C, for override final member function: final_member
```

- Aを継承したクラス（B）を継承

```py
class D(B):

    def final_member(self) -> None:
        pass
#=> Fail to declare class D, for override final member function: final_member
```

- 多重継承

```py
class E(A, int):

    def final_member(self) -> None:
        pass
#=> Fail to declare class E, for override final member function: final_member

class F(int, B):

    def final_member(self) -> None:
        pass
#=> Fail to declare class F, for override final member function: final_member
```

- class methodでオーバーライド

```py
class G(A):

    @classmethod
    def final_member(cls) -> None:
        pass
#=> Fail to declare class G, for override final member function: final_member
```

- static methodでオーバーライド

```py
class H(A):

    @staticmethod
    def final_member() -> None:
        pass
#=> Fail to declare class H, for override final member function: final_member
```

#### class methodのオーバーライド

1ケースだけですが．

```py
class J(A):

    @classmethod
    def final_class(cls) -> None:
        pass
#=> Fail to declare class J, for override final class method: final_class
```

#### static methodのオーバーライド

こちらも1ケースだけ．

```py
class K(A):

    @staticmethod
    def final_static() -> None:
        pass
#=> Fail to declare class K, for override final static method: final_static
```

#### 正常

最後に例外が起きないケースも見ておきます．大丈夫そうですね．

```py
class L(A):

    def overridable(self) -> None:
        print("from L")


L().overridable()
#=> from l
```

### Pros

- 実行時にオーバーライドを検出して例外を起こせる

### Cons

- 実行時のオーバーヘッドは多分ある（計測はしていませんが）
  - かなり実装がナイーブなので，setとか使って管理すれば多少速くなりそう（実際`abc`は弱参照とか使ってキャッシュを作っているっぽい https://github.com/python/cpython/blob/3.7/Lib/_py_abc.py ）
- この方法で定数を作るのは無理そう．

### 補足
`abc`の場合はインスタンス化時に例外が上がるが，これはクラス定義時に例外を上げるようにすると，抽象クラスの定義ができなくなるという都合だと思います．
今回の`final`には関係のない話なので，こちらはクラス定義時に例外が上がるようにしました．

## まとめ
オーバーライドを抑制するための手段を二つ紹介しました．
抑制と書いているのは，`setattr`を使ったりすれば，結局のところ抜け道はあるからです．結局それがPython的ということなのでしょうか．

## 関連リンク
- 自分と同じようなことを考える人はいるようで先例がいくつかありました．（実装がどれも気に入らないまたは，ぱっと理解できなかったかで結局あまり参考にはしていません．）
    - https://stackoverflow.com/questions/3948873/prevent-function-overriding-in-python
    - https://stackoverflow.com/questions/321024/making-functions-non-override-able
    - https://stackoverflow.com/questions/2425656/how-to-prevent-a-function-from-being-overridden-in-python

3つ目のリンク先のこれが一番好き．

> \# We'll fire you if you override this method.