How to raise an error if child class override parent's method in python?

Question:

I’m doing a class that will be the basis for another ones and i want to prohibit some methods to be override in some situations and I just don’t know how to do it.

Asked By: sbb

||

Answers:

You are looking for the super() function

Answered By: tibipin

In python functions are just members of a class. You can replace them (What is monkey patching) to do somehting completely different.

So even code that is NOT a subclass can substitute a classes function to do different things.

You can name-mangle functions – but that is also circumventable – and they also can be monkey-patched:

class p:
  def __init__(self):
    pass

  def __secret(self):
    print("secret called")

  def __also_secret(self):
    print("also_secret called")

  def not_secret(self):
    def inner_method():
      print("inner called")
    inner_method()


class r(p):
  def __secret(self):  # override existing function
    print("overwritten")

Demo:

a = p()
b = r()

# get all the respective names inside the class instance
c = [n for n in dir(a) if n.startswith("_p__")]
d = [n for n in dir(b) if n.startswith("_r__")]

# call the hidden ones and monkey patch then call again
for fn in c:
    q = getattr(a, fn)
    q() # original executed although "hidden"
    q = lambda: print("Monkey patched " + fn)
    q() # replaced executed

# call the hidden ones and monkey patch then call again
for fn in d:
    q = getattr(b, fn)
    # original executed although "hidden"
    q = lambda: print("Monkey patched " + fn)
    q() # replaced executed


# call public function
a.not_secret()
try:
    # a.inner_method() # does not work
    a.not_secret.inner_method() # does also not work as is it scoped inside
except AttributeError as e:
    print(e)


a.not_secret = lambda: print("Monkey patched")
a.not_secret()

Output:

also_secret called                       # name mangled called
Monkey patched _p__also_secret           # patched of name mangled called
secret called                            # name mangled called
Monkey patched _p__secret                # patched of name mangled called
Monkey patched _r__secret                # overwritten by subclass one called
inner called                             # called the public that calls inner
'function' object has no attribute 'inner_method'  # cannot get inner directly
Monkey patched

If you want this feature you need to use a different language – not python.

Answered By: Patrick Artner

I’m going to show you three approaches from "not so bad" to the cleanest (in my opinion).

1. class decorator

One way of doing this is using "class decorator" to compare methods of the class itself and its parent:

from inspect import isfunction


def should_not_override_parents_method(cls):
    parents_methods = set(k for k, v in cls.__base__.__dict__.items() if isfunction(v))
    class_methods = set(k for k, v in cls.__dict__.items() if isfunction(v))
    intersection = parents_methods & class_methods

    if intersection:
        raise Exception(
            f"class {cls.__name__} should not implement parents method: "
            f"'{', '.join(intersection)}'"
        )
    return cls


class A:
    def fn_1(self):
        print("A : inside fn_1")


@should_not_override_parents_method
class B(A):
    def fn_1(self):
        print("B : inside fn_1")

    def fn_2(self):
        print("B : inside fn_2")

2. __init_subclass__

from inspect import isfunction


class A:
    def __init_subclass__(cls, **kwargs):
        parents_methods = set(
            k for k, v in cls.__base__.__dict__.items() if isfunction(v)
        )
        class_methods = set(k for k, v in cls.__dict__.items() if isfunction(v))
        intersection = parents_methods & class_methods

        if intersection:
            raise Exception(
                f"class {cls.__name__} should not implement parents method: "
                f"'{', '.join(intersection)}'"
            )

    def fn_1(self):
        print("A : inside fn_1")


class B(A):
    def fn_1(self):
        print("B : inside fn_1")

    def fn_2(self):
        print("B : inside fn_2")

Note: These are only gonna prevent subclasses from overriding parent’s method in creation phase. This means after the class has been created, you can dynamically add those methods to them. To also prevent that you can create a custom metaclass and override __setattr__ method as well:

3. Metaclass

from inspect import isfunction


class Prevent(type):
    @staticmethod
    def _check_methods(parent_dict, child_dict, child_name):
        parents_methods = set(k for k, v in parent_dict.items() if isfunction(v))
        class_methods = set(k for k, v in child_dict.items() if isfunction(v))
        intersection = parents_methods & class_methods
        if intersection:
            raise Exception(
                f"class {child_name} should not implement parents method: "
                f"'{', '.join(intersection)}'"
            )

    def __new__(cls, name, bases, mapping, **kwargs):
        if bases:
            parent = bases[0]
            Prevent._check_methods(parent.__dict__, mapping, name)
        class_object = super().__new__(cls, name, bases, mapping)
        return class_object

    def __setattr__(cls, name, value) -> None:
        if name in cls.__base__.__dict__:
            raise Exception(
                f"class {cls.__name__} should not have parents method: {name}"
            )
        super().__setattr__(name, value)


class A(metaclass=Prevent):
    def fn_1(self):
        print("A : inside fn_1")


class B(A):
    def fn_1(self):
        print("B : inside fn_1")

    def fn_2(self):
        print("B : inside fn_2")

output:

Traceback (most recent call last):
  File "<>", line 36, in <module>
    class B(A):
  File "<>", line 19, in __new__
    Prevent._check_methods(parent.__dict__, mapping, name)
  File "<>", line 11, in _check_methods
    raise Exception(
Exception: class B should not implement parents method: 'fn_1'

Now it’s worth mentioning that since Python 3.8, there is decorator called @typing.final which hints you that you shouldn’t override this method in the child class. Of course there is no restriction by doing so at runtime.

from typing import final


class A:
    @final
    def fn_1(self):
        print("A : inside fn_1")


class B(A):
    def fn_1(self):
        print("B : inside fn_1")

    def fn_2(self):
        print("B : inside fn_2")
Answered By: S.B