最初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メソッドだと表明しています.
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'
エラーメッセージに使うために,staticmethod
かclassmethod
かメンバ関数かを調べているだけです.
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.isfunction
かinspect.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
- 実行時のオーバーヘッドは多分ある(計測はしていませんが)
- かなり実装がナイーブなので,setとか使って管理すれば多少速くなりそう(実際
abc
は弱参照とか使ってキャッシュを作っているっぽい https://github.com/python/cpython/blob/3.7/Lib/_py_abc.py )
- かなり実装がナイーブなので,setとか使って管理すれば多少速くなりそう(実際
- この方法で定数を作るのは無理そう.
補足
abc
の場合はインスタンス化時に例外が上がるが,これはクラス定義時に例外を上げるようにすると,抽象クラスの定義ができなくなるという都合だと思います.
今回のfinal
には関係のない話なので,こちらはクラス定義時に例外が上がるようにしました.
まとめ
オーバーライドを抑制するための手段を二つ紹介しました.
抑制と書いているのは,setattr
を使ったりすれば,結局のところ抜け道はあるからです.結局それがPython的ということなのでしょうか.
関連リンク
- 自分と同じようなことを考える人はいるようで先例がいくつかありました.(実装がどれも気に入らないまたは,ぱっと理解できなかったかで結局あまり参考にはしていません.)
3つ目のリンク先のこれが一番好き.
# We'll fire you if you override this method.