8one6
8one6

Reputation: 13768

Can a class in python behave like a specified class based on a passed parameter?

I have an abstract base class (called Shape below). And I derive a couple classes from it (Circle and Square below).

I would like to create a "dispatching class" (called MagicShape below) so that when I instantiate a new object of this new class, it magically becomes one of the derived classes above based on a passed parameter.

I.e. if Circle and Square both get initialized with 2 parameters, I want MagicShape to take 3 parameters at instantiation so that the first parameter would either be the string circle or the string square and would result in creating either a Circle or Square with the subsequently specified parameters.

So for specifics, I have:

from numpy import pi as PI

class Shape(object):
    def __init__(self, color, size):
        self.color=color
        self.size = size

    def describe(self):
        return 'I am a {color:s} {kind:s} of size {size:0.1f}'.format(color=self.color, 
                                                                      kind=self.kind,
                                                                      size=self.size)

class Circle(Shape):
    def __init__(self, color, size):
        self.kind = 'circle'
        super(Circle, self).__init__(color, size)

    def area(self):
        return PI * self.size * self.size

class Square(Shape):
    def __init__(self, color, size):
        self.kind = 'square'
        super(Square, self).__init__(color, size)

    def area(self):
        return self.size * self.size

And I would like to have something like:

class MagicShape(???):
    def __init__(self, kind, color, size):
        # what goes in here?

So that when I run ms = MagicShape('circle', 'red', 3), ms is a Circle but when I run ms = MagicShape('square', 'blue', 2), ms is a Square.

I know that I could do something like this:

def make_shape(kind, color, size):
    if 'circle'==kind:
        return Circle(color, size)
    elif 'square'==kind:
        return Square(color, size)
    else:
        raise ValueError

and do the "dispatching" via function. But somehow this felt like it should be doable with classes. Can someone set me straight?

Upvotes: 1

Views: 86

Answers (4)

brettb
brettb

Reputation: 809

You want to use a factory method. Pretty much just as you did, but you can pull it under your super Shape class.

from numpy import pi as PI

class Shape(object):
    #we'll use this below factory method to return the correct subclass:
    @classmethod
    def getShape(self,kind,color,size):
        if 'circle'==kind:
            return Circle(color, size)
        elif 'square'==kind:
            return Square(color, size)
        else:
           raise ValueError

    def __init__(self, color, size):
        self.color=color
        self.size = size

    def describe(self):
        return 'I am a {color:s} {kind:s} of size {size:0.1f}'.format(color=self.color, 
                                                                      kind=self.kind,
                                                                      size=self.size)

class Circle(Shape):
    def __init__(self, color, size):
        self.kind = 'circle'
        super(Circle, self).__init__(color, size)

    def area(self):
        return PI * self.size * self.size

class Square(Shape):
    def __init__(self, color, size):
        self.kind = 'square'
        super(Square, self).__init__(color, size)

    def area(self):
        return self.size * self.size

Upvotes: 0

Martijn Pieters
Martijn Pieters

Reputation: 1121834

You could just use a function, no class required:

shapes = {shape.__name__.lower(): shape 
          for shape in Shape.__subclasses__()}

def MagicShape(kind, color, size):
    try:
        return shapes[kind](color, size)
    except KeyError:
        raise ValueError(kind)

The class.__subclasses__() method here returns all subclasses of Shape, making for a quick and handy way to build a map from kind string to class.

Remember that creating a class is just another call. There is no difference between:

class Foo(object):
    def __init__(self, arg1, arg2):
        pass

and

def Foo(arg1, arg2):
    return something_that_is_an_instance

from the caller's point of view; they'd just use:

result = Foo(value1, value2)

for both the class and the function.

Upvotes: 4

a p
a p

Reputation: 3208

The thing you'd probably want to do (factory/dispatch function) is already outlined in other answers/comments here, but if you actually wanted to return a different class instance and make it look like you're only making a MagicClass, you could do something fancy/awful with the __new__ method, like so:

class MagicClass(object):
    def __new__(cls, *args, **kwargs): 
        # if args[whatever] == 'square': return Square(*args, **kwargs)

...or something like that. It's not good practice, but it is doable.

Upvotes: 0

cmaynard
cmaynard

Reputation: 2860

You want to use the factory pattern, this example does almost exactly what you are asking: http://python-3-patterns-idioms-test.readthedocs.org/en/latest/Factory.html

Upvotes: 0

Related Questions