15
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

PythonAdvent Calendar 2019

Day 5

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

Last updated at Posted at 2019-12-06

最初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

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

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

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を使うと警告を出してくれます.

$ 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と同様に実行時に影響は無いのでその点注意が必要です.

$ python3 mypy.py
hello
こんにちは

Pros

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

Cons

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

参考

追記

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

※ 追記ここまで

__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__くらいしか残っていない気がしますが).
全てのメソッドを基底クラスと突き合わせているだけの単純な実装です.

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

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

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'

エラーメッセージに使うために,staticmethodclassmethodかメンバ関数かを調べているだけです.

Final

最初メタクラスを使って書いていましたが,Python 3.6における『Effective Python』 項目33はこう変わる を思い出して,__init_subclass__ を使って書き換えました(確かに使いやすい).

Final.get_methods

@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.isfunctioninspect.ismethodが真になるものを集めます.

Final.__init_subclass__

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

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

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

# 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__ を使ったほうがよいと思います.ということで,今回の実装について.

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を投げます.

使用例

以下のようなクラスを準備します.

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を直接継承
class C(A):

    def final_member(self) -> None:
        pass
#=> Fail to declare class C, for override final member function: final_member
  • Aを継承したクラス(B)を継承
class D(B):

    def final_member(self) -> None:
        pass
#=> Fail to declare class D, for override final member function: final_member
  • 多重継承
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でオーバーライド
class G(A):

    @classmethod
    def final_member(cls) -> None:
        pass
#=> Fail to declare class G, for override final member function: final_member
  • static methodでオーバーライド
class H(A):

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

class methodのオーバーライド

1ケースだけですが.

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ケースだけ.

class K(A):

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

正常

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

class L(A):

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


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

Pros

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

Cons

  • 実行時のオーバーヘッドは多分ある(計測はしていませんが)
  • この方法で定数を作るのは無理そう.

補足

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

まとめ

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

関連リンク

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

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

15
10
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
15
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?