Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Method accessible only from class descendants in python

Let's say I have the following two classes

class A:
    def own_method(self):
        pass
    def descendant_method(self):
        pass

class B(A):
    pass

and I want descendant_method to be callable from instances of B, but not of A, and own_method to be callable from everywhere.

I can think of several solutions, all unsatisfactory:

  1. Check some field and manually raise NotImplementedError:
class A:
    def __init__(self):
        self.some_field = None
    def own_method(self):
        pass
    def descendant_method(self):
        if self.some_field is None:
            raise NotImplementedError

class B(A):
    def __init__(self):
        super(B, self).__init__()
        self.some_field = 'B'
    pass

But this is modifying the method's runtime behaviour, which I don't want to do

  1. Use a mixin:
class A:
    def own_method(self):
        pass

class AA:
    def descendant_method(self):
        pass

class B(AA, A):
    pass

This is nice as long as descendant_method doesn't use much from A, or else we'll have to inherit AA(A) and this defies the whole point

  1. make method private in A and redefine it in a metaclass:
class A:
    def own_method(self):
        pass
    def __descendant_method(self):
        pass

class AMeta(type):
    def __new__(mcs, name, parents, dct):
        par = parents[0]
        desc_method_private_name = '_{}__descendant_method'.format(par.__name__)
        if desc_method_private_name in par.__dict__:
            dct['descendant_method'] = par.__dict__[desc_method_private_name]

        return super(AMeta, mcs).__new__(mcs, name, parents, dct)

class B(A, metaclass=AMeta):
    def __init__(self):
        super(B, self).__init__()

This works, but obviously looks dirty, just like writing self.descendant_method = self._A__descendant_method in B itself.

What would be the right "pythonic" way of achieving this behaviour?

UPD: putting the method directly in B would work, of course, but I expect that A will have many descendants that will use this method and do not want to define it in every subclass.

like image 859
Gosha F Avatar asked Nov 01 '25 18:11

Gosha F


1 Answers

What is so bad about making AA inherit from A? It's basically an abstract base class that adds additional functionality that isn't meant to be available in A. If you really don't want AA to ever be instantiated then the pythonic answer is not to worry about it, and just document that the user isn't meant to do that. Though if you're really insistent you can define __new__ to throw an error if the user tries to instantiate AA.

class A:
    def f(self):
        pass

class AA(A):
    def g(self):
        pass
    def __new__(cls, *args, **kwargs):
        if cls is AA:
            raise TypeError("AA is not meant to be instansiated")
        return super().__new__(cls)

class B(AA):
    pass

Another alternative might be to make AA an Abstract Base Class. For this to work you will need to define at least one method as being abstract -- __init__ could do if there are no other methods you want to say are abstract.

from abc import ABCMeta, abstractmethod

class A:
    def __init__(self, val):
        self.val = val
    def f(self):
        pass

class AA(A, metaclass=ABCMeta):
    @abstractmethod
    def __init__(self, val):
        super().__init__(val)
    def g(self):
        pass

class B(AA):
    def __init__(self, val):
        super().__init__(val)

Very finally, what's so bad about having the descendant method available on A, but just not using it. You are writing the code for A, so just don't use the method... You could even document the method that it isn't meant to be used directly by A, but is rather meant to be available to child classes. That way future developers will know your intentions.

like image 91
Dunes Avatar answered Nov 04 '25 08:11

Dunes