Gosha F
Gosha F

Reputation: 427

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.

Upvotes: 4

Views: 455

Answers (3)

Dunes
Dunes

Reputation: 40703

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.

Upvotes: 2

pygeek
pygeek

Reputation: 7404

As far as I can tell, this may be the most Pythonic way of accomplishing what you want:

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

class B(A):
    def descendant_method(self):
        ...

Another option could be the following:

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

class B(A):
    def descendant_method(self):
        return self._descendant_method(self)

They're both Pythonic because it's explicit, readable, clear and concise.

  • It's explicit because it's not doing any unnecessary magic.
  • It's readable because one can tell precisely what your doing, and what your intention was at first glance.
  • It's clear because the leading single underscore is a widely used convention in the Python community for private (non-magic) methods—any developer that uses it should know to tread with caution.

Choosing between one of these approaches will depend on how you intend on your use case. A more concrete example in your question would be helpful.

Upvotes: 2

ClvrObj
ClvrObj

Reputation: 51

Try to check the class name using __class__.__name__ .

class A(object):

    def descendant_method(self):
        if self.__class__.__name__ == A.__name__:
            raise NotImplementedError
        print 'From descendant'


class B(A):
    pass


b = B()
b.descendant_method()
a = A()
a.descendant_method()

Upvotes: 0

Related Questions