Suppressing method overrides in Python

I posted it in 3 at first, but I moved it because yesterday's part of 1 was unposted and was vacant.

Prohibition of method overriding at the time of inheritance can be realized by the final keyword in Java or C ++, for example, but it is not a standard function of Python. So, I researched and thought about how to do it in Python3.

environment

Implementation

All of this implementation is located at https://github.com/eduidl/python-sandbox/tree/master/prohibit_override.

mypy

Type hints were introduced from Python 3.5, but it is a tool for statically checking types using the type hints. It's mypy, but you can use the @final decorator to declare that it's a method that shouldn't be overridden.

The usage is as follows. We use @final to declare that it is a final method called hello.

mypy.py


from typing_extensions import final

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

class Derived(Base):
    def hello(self) -> None:
        print("Hello")

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

If you use mypy on this file, it will warn you.

$ 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)

However, as with Type Hints, there is no effect at runtime, so be careful about that.

$ python3 mypy.py
hello
Hello

Pros --Easy to introduce for those who are already using mypy ――It seems that it can also be used for constants

Cons --No error and no warning at runtime --Although the introduction itself can be entered with pip, the introduction cost is high for those who have not written type hints.

reference

Postscript

Since Python 3.8, it seems that it is also implemented in the typing module of the standard library. That said, you still need some type checker, including mypy.

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

Use of __init_subclass__

Static analysis by mypy is good, but I'm happy if I check it at runtime and raise an exception. As an image, it looks like ʻabc` (https://docs.python.org/ja/3/library/abc.html), which is an abstract base class module of the standard library.

So, I tried to implement it with reference to https://github.com/python/cpython/blob/3.8/Lib/abc.py (Although it is a reference, only __isfinalmethod__ remains. I feel like that). It's a simple implementation that matches all the methods with the base class.

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))

Implementation details

I usually skip it, but since it is an Advent calendar, I will explain it briefly.

final

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

ʻAbc.abstractmethodimplementation (https://github.com/python/cpython/blob/3.8/Lib/abc.py#L7-L25) Because PyCharm spits out a warning. I usedsetattr`.

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'

I'm just looking for a static method, a class method, or a member function to use for error messages.

Final

I wrote it using a metaclass at first, but remembering "Effective Python" item 33 in Python 3.6 changes like this, Rewritten using __init_subclass__ (certainly easy to use).

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) takes a predicate as the second argument and returns what it is true .. This time, I want all the member functions, static method, and class method, so I will collect the ones for which ʻinspect.isfunction or ʻinspect.ismethod` is true.

Final.__init_subclass__

First of all, regarding __init_subclass__, it is a function introduced in Python 3.6, and for details, refer to the official document (https://docs.python.org/ja/3/reference/datamodel.html#object.init_subclass). I will quote.

This method is called whenever the class in which it is defined is inherited. cls is a new subclass.

That's all, but in short, what I used to write using metaclasses is easier to write.

# Python 3.Up to 5
#Actually, I only use Exampale, not Exampale Meta directly
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):
        #various

# Python 3.From 6
class Exampale:
    def __init__(self, args):
        #various

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

So, if you can use Python 3.6, I think you should use __init_subclass__ as much as possible. So, about this implementation.

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}')

Get all methods with Final.get_methods, get inherited classes with __bases__, and for each class

--Do you have a method with the same name? --If you have it, it has the __isfinalmethod__ attribute and it is not True --If __isfinalmethod__ is True, is it overridden?

Check and if applicable, raise Type Error. By the way, ʻabc also throws TypeError`.

Example of use

Prepare the following class.

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

Member function override

I've tried a few cases and it seems to work.

--Inherit A directly

class C(A):

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

--Inherit class (B) that inherits A

class D(B):

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

--Multiple inheritance

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

--Override with class method

class G(A):

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

--Override with 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 override

Only one case.

class J(A):

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

static method override

This is also only one case.

class K(A):

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

normal

Finally, let's look at the case where no exception occurs. Sounds okay.

class L(A):

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


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

Pros

--Can detect overrides and raise exceptions at runtime

Cons

--Maybe there is run-time overhead (although not measured) --Since the implementation is quite naive, it seems to be a little faster if you manage it using set etc. (Actually ʻabc` seems to make a cache using weak references https://github.com/python/cpython/blob/ 3.7 / Lib / _py_abc.py) ――It seems impossible to make a constant by this method.

Supplement

In the case of ʻabc, an exception is raised at the time of instantiation, but I think that this is because if you raise an exception at the time of class definition, you will not be able to define an abstract class. This is a story that has nothing to do with final`, so I made an exception when defining the class.

Summary

I have introduced two ways to suppress overrides. I write it as suppression because there is a loophole after all if you use setattr. Does that mean it's Python-like after all?

Related Links

――There seemed to be some people who think the same as myself, and there were some precedents. (I don't really refer to it because I didn't like any of the implementations or didn't understand it at all.) - 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

I like this one, which is the third link.

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

Recommended Posts

Suppressing method overrides in Python
Simplex method (simplex method) in Python
Implement method chain in Python
Try implementing extension method in python
Implemented label propagation method in Python
Simulate Monte Carlo method in Python
Hash method (open address method) in Python
Quadtree in Python --2
Python in optimization
CURL in python
Metaprogramming in Python
Python 3.3 in Anaconda
SendKeys in Python
Method to build Python environment in Xcode 6
Electron Microscopy Simulation in Python: Multislice Method (1)
Epoch in Python
Discord in Python
Sudoku in Python
DCI in Python
quicksort in python
nCr in python
N-Gram in Python
Electron Microscopy Simulation in Python: Multislice Method (2)
Programming in python
Constant in python
Lifegame in Python.
FizzBuzz in Python
Sqlite in python
StepAIC in Python
N-gram in python
LINE-Bot [0] in Python
Csv in python
Disassemble in Python
Reflection in Python
Constant in python
nCr in Python.
Scons in Python3
Puyo Puyo in python
python in virtualenv
PPAP in Python
Quad-tree in Python
Alignment algorithm by insertion method in Python
Reflection in Python
Chemistry in Python
Johnson method (python)
Hashable in python
DirectLiNGAM in Python
LiNGAM in Python
Flatten in python
[Python] Semi-Lagrange method
flatten in python
Learn the design pattern "Template Method" in Python
I tried the least squares method in Python
To dynamically replace the next method in python
Learn the design pattern "Factory Method" in Python
Try implementing the Monte Carlo method in Python
Sorted list in Python
Daily AtCoder # 36 in Python
Clustering text in Python
Daily AtCoder # 2 in Python
Implement Enigma in python