Preventing a class from direct instantiation in Python
Asked Answered
F

6

55

I have a super class with a method that calls other methods that are only defined in its sub classes. That's why, when I create an instance of my super class and call its method, it cannot find the method and raises an error.

Here is an example:

class SuperClass(object):

  def method_one(self):
    value = self.subclass_method()
    print value


class SubClassOne(SuperClass):

  def subclass_method(self):
    return 'subclass 1'


class SubClassTwo(SuperClass):

  def subclass_method(self):
    return 'nubclass 2'


s1 = SubClassOne()
s1.method_one()

s2 = SubClassTwo()
s2.method_one()

c = SuperClass()
c.method_one()

# Results:
# subclass 1
# nubclass 2
# Traceback (most recent call last):
#   File "abst.py", line 28, in <module>
#     c.method_one()
#   File "abst.py", line 4, in method_one
#     value = self.subclass_method()
# AttributeError: 'SuperClass' object has no attribute 'subclass_method'

I was thinking about changing the __init__ of super class and verify the type of object, when a new instance is created. If the object belongs to super class raise an error. However, I'm not too sure if it's the Pythonic way of doing it.

Any recommendations?

Fool answered 3/11, 2011 at 0:37 Comment(6)
The Pythonic way is to write good documentation explaining how your class is to be used.Tautomerism
@chown: This is actually done quite frequently in C++. What he's doing is essentially calling virtual methods. There's nothing inherently wrong with them, if they are the proper solution to the problem.Irrespective
I will agree that it isn't very PythonicIrrespective
@chown: I have one super class and 8 sub classes. All sub classes share one method and all of them will have a method with the same name, but different implementations that are called from the shared method. You don't think my approach is pythonic for this scenario?Fool
@chown You seem to imply that the pattern of using abstract classes is "non-programmingonic in general", which most OO programmers would disagree with. How would this be any different if BaseClass defined subclass_method to raise NotImplementedError, other than a slightly nicer error message?Angst
@chown The use of abstract classes is hardly a "1-in-a-million scenario". It's a core part of OO programming. Python doesn't require classes to be formally declared abstract; all you have to do is just not implement the methods you require a subclass to implement. But it's exactly the same pattern. You claimed that referring to an unimplemented abstract method in a base class was "non-programmingonic in general", but it's done all the time (and encouraged) in every OO language. All the OP was after is a way of detecting misuse of the abstract class.Angst
S
93

I would override __new__() in the base class and simply fail to instantiate at all if it's the base class.

class BaseClass:    # Py3

    def __new__(cls, *args, **kwargs):
        if cls is BaseClass:
            raise TypeError(f"only children of '{cls.__name__}' may be instantiated")
        return object.__new__(cls, *args, **kwargs)

This separates concerns a little better than having it in __init__(), and "fails fast."

Succubus answered 3/11, 2011 at 4:35 Comment(5)
The abc module is probably right for 90% of cases, but this technique is useful in some others. A few I've encountered: 1.) you want a reusable mixin class but it actually has no abstract methods, so ABCMeta won't prevent it from being instantiated; 2.) you're defining a "base" class or mixin that actually subclasses something else, but you don't want it to be directly instantiable.Rawley
This is indeed very useful. I'd like to add that if BaseClass inherited VeryBaseClass, then return VeryBaseClass.__new__() instead of object.__new__()Avina
Also in this case, note that if you override __init__ as well as overrding __new__ then you can't pass extra params to __new__ you have to call just return object.__new__(cls) instead.Brandie
@Avina a more dynamic implementation: return super().__new__(cls, *args, **kwargs)Charentemaritime
Getting TypeError: object.__new__() takes exactly one argument (the type to instantiate)Macrocosm
B
29

Your approach is a typical framework pattern.

Using __init__ to verify that type(self) is not SuperClass is a reasonable way to make sure the SuperClass hasn't been instantiated directly.

The other common approach is to provide stub methods that raise NotImplementedError when called. That is more reliable because it also validates that subclasses have overridden the expected methods.

Barbiturism answered 3/11, 2011 at 0:46 Comment(0)
C
17

This is what I might do:

class SuperClass(object):
    def __init__(self):
        if type(self) == SuperClass:
            raise Exception("<SuperClass> must be subclassed.")
        # assert(type(self) == SuperClass)

class SubClass(SuperClass):
    def __init__(self):
        SuperClass.__init__(self)

subC = SubClassOne()
supC = SuperClass() # This line should throw an exception

When run (exception is thrown!):

[ 18:32 jon@hozbox ~/so/python ]$ ./preventing-direct-instantiation.py
Traceback (most recent call last):
  File "./preventing-direct-instantiation.py", line 15, in <module>
    supC = SuperClass()
  File "./preventing-direct-instantiation.py", line 7, in __init__
    raise Exception("<SuperClass> must be subclassed.")
Exception: <SuperClass> must be subclassed.

Edit (from comments):

[ 20:13 jon@hozbox ~/SO/python ]$ cat preventing-direct-instantiation.py 
#!/usr/bin/python

class SuperClass(object):
    def __init__(self):
        if type(self) == SuperClass:
            raise Exception("<SuperClass> must be subclassed.")

class SubClassOne(SuperClass):
    def __init__(self):
        SuperClass.__init__(self)

class SubSubClass(SubClassOne):
    def __init__(self):
        SubClassOne.__init__(self)

class SubClassTwo(SubClassOne, SuperClass):
    def __init__(self):
        SubClassOne.__init__(self)
        SuperClass.__init__(self)

subC = SubClassOne()

try:
    supC = SuperClass()
except Exception, e:
    print "FAILED: supC = SuperClass() - %s" % e
else:
    print "SUCCESS: supC = SuperClass()"

try:
    subSubC = SubSubClass()
except Exception, e:
    print "FAILED: subSubC = SubSubClass() - %s" % e
else:
    print "SUCCESS: subSubC = SubSubClass()"

try:
    subC2 = SubClassTwo()
except Exception, e:
    print "FAILED: subC2 = SubClassTwo() - %s" % e
else:
    print "SUCCESS: subC2 = SubClassTwo()"

Prints:

[ 20:12 jon@hozbox ~/SO/python ]$ ./preventing-direct-instantiation.py 
FAILED: supC = SuperClass() - <SuperClass> must be subclassed.
SUCCESS: subSubC = SubSubClass()
SUCCESS: subC2 = SubClassTwo()
Ciccia answered 3/11, 2011 at 1:32 Comment(2)
Not saying this isn't pretty cool, but I think it shows exactly why you shouldn't try to do this sort of thing. I'm pretty sure both examples fail with multiple inheritance.Firepower
@sdolan I just tested, and it actually worked with class SubSubClass(SubClassOne): and calling SubClassOne.__init__(self) from its __init__!Ciccia
I
10

You're talking about Abstract Base Classes, and the Python language does not support them natively.

However, in the standard library, there is a module you can use to help you along. Check out the abc documentation.

Irrespective answered 3/11, 2011 at 0:41 Comment(2)
Actually there are a lot more ways do implement his on Python, but I'm going to just import abc module and set my class as an abstract class. I noticed even though my class is abstract, I can still implement methods in it, which is very nice.Fool
Abstract classes created with the abc module can still be instantiated, so it doesn't solve the problem. Instantiation prevented only if there are unimplemented abstract methods in the class. To simple prevent instantiation use one of the solutions below that add checks to the constructor.Quaky
K
1

All the current answers are from 2011. I'm unsure when exactly this was introduced, but it's definitely possible now if you use the @abstractmethod decorator. This presumes your ABC has a method that must be implemented by subclasses (but if it doesn't, I don't get the point of making it abstract)

from abc import ABC, abstractmethod


class SuperClass(ABC):

  def method_one(self):
    value = self.subclass_method()
    print(value)

  @abstractmethod
  def subclass_method(self):
    raise NotImplementedError


class SubClassOne(SuperClass):

  def subclass_method(self):
    return 'subclass 1'


# Works
s1 = SubClassOne()
s1.method_one()

# TypeError prevents super class instantiation as desired
# "Can't instantiate abstract class SuperClass with abstract methods subclass_method"
c = SuperClass()
Kennykeno answered 21/9, 2023 at 17:52 Comment(0)
A
1

@Addison Klinke agree with your suggestion

If you create an ABC with at least one @abstractmethod, then that automatically prevents your abstract class from being directly instantiated.

You don't even need to raise NotImplementedError in your abstract method

from abc import ABC, abstractmethod


class SuperClass(ABC):

  def method_one(self):
    value = self.subclass_method()
    print(value)

  @abstractmethod
  def subclass_method(self):
    pass

will work the same way, i.e. if it is not overridden, an exception will be raised.

Alvinia answered 1/12, 2023 at 11:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.