What's the recommended way to provide default values to specific instances in Python?

Viewed 63

I'd like certain instances to initialize with certain default attribute values depending on the initialization parameters of the object. I'm considering using a nested dictionary as a class attribute, but it feels convoluted for some reason. Is there a best practice for this type of situation?

class Shape:

    metadata = {
        3: {"names": ["Triangle", "Triforce"], "color": "sage"},
        4: {"names": ["Square", "Box", "Cube"], "color": "dusty rose"},
        12: {"names": ["Dodecagon", "Crude circle"], "color": "gold"}
    }

    colors = ["red", "orange", "yellow", "green", "blue", "indigo", "violet"]

    def __init__(self, sides, *names):
        # All instances will certainly have the same attributes
        self.sides = sides
        self.names = list(names)
        # Most attributes will have automatically generated values based on
        # the init parameters
        self.color = self.colors[sides % 7]
        self.names += [str(sides) + "-gon"]
        # But a few will have commonly recognized values which I'd like to set
        # manually.
        # This is the part I'm not sure how to handle
        data = __class__.metadata.get(sides)
        if data is not None:
            self.names += data["names"]
            self.color = data["color"]

I could add the custom values after creating the objects, but if I ever create another object with the same initialization parameters, it won't retain those custom values (i.e. I want all my Shape(3) objects to posses the name "Triangle").

2 Answers

I think the reason it feels complicated is because your Shape class is trying to do too many things at once. Ideally, a class should be responsible for a single part of your programs behavior (this is the Single Responsibility Principle).

I'd recommend two main changes to your code.

Don't make the Shape class responsible for creating itself

A shape doesn't really need to know about all other possible kinds of shapes, or the rules required for deciding which kind of shape it is. This code can be abstracted out into another class, so the shape can focus on containing sides, shapes and colours. I'd recommend using something like the Factory Pattern for this (https://en.wikipedia.org/wiki/Factory_method_pattern).

Consider using polymorphism

If you plan on only ever having shapes be containers for sides names and colours, your current class will work fine. However, if you ever want to add functionality that changes depending on the kind of shape (if you wanted to calculate it's area, say), you'll wind up with some complicated logic in your shapes class which will mean it's back doing too many things again.

Example:

class Shape:
    def __init__(self, sides, color, *names):
        self.sides = sides
        self.color = color
        self.names = names

    def __str__(self):
       return "Sides: {}\nNames:{}\nColor: {}\n".format(self.sides, self.names, self.color)

class Triangle(Shape):
    def __init__(self, color, *names):
        super().__init__(3, color, *names)

class Square(Shape):
    def __init__(self, color, *names):
        super().__init__(4, color, *names)

class BlueShapeFactory:
    def createShapes(sides):
        if sides == 3:
            return Triangle("Blue", "PointyBoy", "Triangle")
        elif sides == 4:
            return Square("Blue", "Uncool", "Square")
        else:
            return Shape(sides, "Blue", str(sides) + "-o-gon")
            
class DefaultShapeFactory:
    def createShapes(sides):
        if sides == 3:
            return Triangle("green", "Triforce", "Triangle")
        elif sides == 4:
            return Square("blue", "Box", "Cube", "Square")
        else:
            return Shape(sides, "purple", str(sides) + "-o-gon")
            
print("Blueshapes:\n")
print(BlueShapeFactory.createShapes(3))
print(BlueShapeFactory.createShapes(4))
print(BlueShapeFactory.createShapes(42))

print("Your shapes:\n")
print(DefaultShapeFactory.createShapes(3))
print(DefaultShapeFactory.createShapes(4))
print(BlueShapeFactory.createShapes(42))

I found a solution: Use The Flyweight design pattern.

Using this design pattern, for each initialization parameter, an instance is only instantiated once, and referenced if construction is attempted again with the same init parameter(s). This is similar to object caching of certain immutable Python built-ins.

Rather than saving default attribute values inside the class definition, simply set those values after instantiation. Any future object with the same initialization parameters will retain those custom values because it will reference the first object.

class Shape:

    _instances = {}

    colors = ["red", "orange", "yellow", "green", "blue", "indigo", "violet"]

    def __new__(cls, sides, *names):
        # Retrieve the Shape instance if previously instantiated
        self = cls._instances.get(sides)
        # If it has not yet been instantiated...
        if self is None:
            # Save this Shape instance
            self = cls._instances[sides] = object.__new__(Shape)
            # Initialize here for efficiency (because __init__ gets called
            # automatically, even when returning a previously instantiated
            # object)
            self.sides = sides
            self.color = cls.colors[sides % 7]
            self.names = list(names)
            self.names += [str(sides) + "-gon"]
        return self

triangle = Shape(3)
triangle.names += ["triangle", "triforce"]
triangle.color = "sage"

square = Shape(4)
square.names += ["Square", "Box", "Cube"]
square.color = "dust rose"

dodecagon = Shape(12)
dodecagon.names += ["Dodecagon", "Crude circle"]
dodecagon.color = "gold"

Usually, this pattern should be used for immutable objects, because modifying attributes of one object causes that change to be made everywhere that object is referenced, which could cause unexpected results. However, in this case, that is desirable behavior.

Related