3

I have the following problem that I will attempt to illustrate with the following example.

class Brick():
    def __init__(self):
        self.weight = 1

class House():
    def __init__(self, number_bricks):
        self.bricks = [Brick() for i in range(number_bricks)]

    def get_weight(self):
        return reduce(lambda x,y: x+y, [brick.weight for brick in self.bricks])

But now suppose I create a new kind of Brick, StrongBrick, so that I make a house, a subclass StrongHouse, where StrongBrick plays exactly the same role in StrongHouse as Brick plays in House. How can I do this in a nice way (not just retyping all the class definitions)?

So the basic idea is, how can I change a class which is composed of some objects to the same class but composed of say a subclass of the original member objects?

Thanks very much for any help you can give me.

NKN
  • 6,482
  • 6
  • 36
  • 55
  • 1
    As a side note: `reduce(lambda x,y: x+y, foo)` is just a more verbose and slower way to do `sum(foo)`. Also, you can use a genexpr there rather than a comprehension. – abarnert Aug 22 '13 at 20:44
  • 1
    Another side note: Don't do `class Brick():`. If this is Python 2.x, that creates old-style classes; you want `class Brick(object):`. If this is Python 3.x, it does the right thing, but it's still better to do `class Brick:`—besides being a little more readable, `Brick()` implies that you're expecting something different than just `Brick`, and makes the reader wonder what you were expecting… – abarnert Aug 22 '13 at 20:46

5 Answers5

5

You could have a factory (a brickyard?) and pass that to House.__init__().

class Brick(object): pass

class StrongBrick(Brick): pass

class House(object):
    def __init__(self, brick_factory, num_bricks):
        self.bricks = [brick_factory() for i in range(num_bricks)]

house = House(Brick, 10000)
strong_house = House(StrongBrick, 10000)

As you can see, subclassing House isn't even necessary to be able to construct houses from different types of bricks.

NPE
  • 486,780
  • 108
  • 951
  • 1,012
  • +1: This is much the better design, as Kevin has pointed out, compared to the OP's suggestion, which is a parallel hierarchy and a bad thing. – quamrana Aug 22 '13 at 19:49
2

There are various ways to do this. You could make the relevant Brick class an attribute of the House class:

class House(object):
    brick_class = Brick

    def __init__(self, number_bricks):
        self.bricks = [self.brick_class() for i in range(number_bricks)]


class StrongHouse(House):
    brick_class = StrongBrick

Or, you could pass in the Brick class you want to use when constructing the House:

class House(object):

    def __init__(self, brick_class, number_bricks):
        self.bricks = [brick_class() for i in range(number_bricks)]
Daniel Roseman
  • 588,541
  • 66
  • 880
  • 895
2

One nice pattern could be this:

class Brick(object):
    weight = 1

class StrongBrick(Brick):
    weight = 42

class House(object):
    brick_type = Brick

    def __init__(self, number_bricks):
        self.bricks = [self.brick_type() for i in range(number_bricks)]

    def get_weight(self):
        return reduce(lambda x, y: x + y, [brick.weight for brick in self.bricks])

class StrongHouse(House):
    brick_type = StrongBrick

Another is to make a function making a factory, and using an argument for the brick_type with default value:

class House(object):
    def __init__(self, number_bricks, brick_type=Brick):
        self.bricks = [brick_type() for i in range(number_bricks)]

    def get_weight(self):
        return reduce(lambda x, y: x + y, [brick.weight for brick in self.bricks])

def make_house_factory(brick_type):
    def factory(number_bricks):
        return House(number_bricks, brick_type)

    return factory

StrongHouse = make_house_factory(StrongBrick)

Of course all such objects would be instances of the House only, even though I named StrongHouse here so that it resembles a class name.

  • 3
    Suppose that houses are made up of bricks (ordinary or strong), lumber (hard or soft), glass (clear or colored), piping (lead or copper), and siding (aluminum and plastic). Won't you have to make 32 different `House` subclasses, ex. `StrongHardClearCopperAluminumHouse`? – Kevin Aug 22 '13 at 19:46
  • :D why not, but that is what he asked, that "subclass StrongHouse, where StrongBrick plays exactly the same role in StrongHouse". – Antti Haapala -- Слава Україні Aug 22 '13 at 19:48
  • Oh, it's perfectly suitable for the OP's problem as stated. He should just be aware of the risks if he wants to scale up the complexity of the problem :-) – Kevin Aug 22 '13 at 20:06
  • 1
    @Kevin: To deal with that problem, you _could_ still use subclasses, building them on the fly out of policy mixins… although I'm not sure I'd ever actually do that. – abarnert Aug 22 '13 at 20:48
1

But now suppose I create a new kind of Brick, StrongBrick, so that I make a house, a subclass StrongHouse, where StrongBrick plays exactly the same role in StrongHouse as Brick plays in House. How can I do this in a nice way (not just retyping all the class definitions)?

As all of the other answers have explained, you really don't want to create this parallel hierarchy at all. But to answer your direct question: You can create classes dynamically, so you can create a parallel hierarchy without copying and pasting all the class definitions. Classes are, after all, first-class objects.

Again, let me stress that you almost certainly don't want to do this, and I'm just showing that it is possible.

def make_house_class(brick_type):
    class NewHouse(House):
        def __init__(self, number_bricks):
            self.bricks = [brick_type() for i in range(number_bricks)]
    return NewHouse

Now, you could statically create all the house types:

StrongHouse = make_house_class(StrongBrick)
CheapHouse = make_house_class(CheapHouse)
# ...

… or maybe build them dynamically from a collection of all of your brick type:

brick_types = (StrongBrick, CheapBrick)
house_types = {brick_type: make_house_class(brick_type) for brick_type in brick_types}

… or even add some hacky introspection to just create a new FooHouse type for every FooBrick type in the current module:

for name, value in globals().items():
    if name.endswith('Brick') and name != 'Brick' and isinstance(value, type):
        globals()[name.replace('Brick', 'House')] = make_house_class(value)

… or even create them on the fly as needed in the factory-maker:

def make_house_factory(brick_type):
    house_type = make_house_class(brick_type)
    def factory(number_bricks):
        return house_type(number_bricks, brick_type)    
    return factory

… or even the generated factory:

def make_house_factory(brick_type):
    def factory(number_bricks):
        return make_house_class(brick_type)(number_bricks, brick_type)
    return factory
abarnert
  • 354,177
  • 51
  • 601
  • 671
0

Add a parameter to the House.__init__ so that you can specify the Brick type:

import functools
class Brick():
    def __init__(self):
        self.weight = 1

class StrongBrick():
    def __init__(self):
        self.weight = 10

class House():
    def __init__(self, number_bricks,brick_type=Brick):
        self.bricks = [brick_type() for i in range(number_bricks)]

    def get_weight(self):
        return reduce(lambda x,y: x+y, [brick.weight for brick in self.bricks])

#not a new class, but an alias with a different default brick_type
StrongHouse = functools.partial(House,brick_type=StrongBrick) 
Marcin
  • 48,559
  • 18
  • 128
  • 201