Problem creating a child class using an alternative constructor of parent

I have a class that creates blocks in a 2d board. I defined it so it does so given their height, length and location on the board but also made an alternative constructor to create blocks by passing its coordinates in the board. class Block: """ Create a block of given height and length in a starting location.""" def __init__(self, name: str, h: int, l: int, location: Tuple[int,int]): self.name = name self.coords = [tuple_sum(t1=location, t2=(i//l , i%l)) for i in range(h*l)] # tuple_sum does (a, b)+(c, d) -> (a+c, b+d) @classmethod def from_coords(cls, name: str, coords: List[Tuple]): block = cls.__new__(cls) block.name = name block.coords = coords return block ... def __str__(self) -> str: return f'Name: {self.name}\nCoords: {self.coords}' I'm trying to create a child class for the blank spaces in the board. I thought that using the from_coords constructor would do everything but for some reason I don't understand the created elements are not initialized, i.e don't have name or coords attributes. class SpacesBlock(Block): """ Make a block of spaces from a list of coordenates """ def __init__(self, coords): super().from_coords(" ",coords) ... spaces = SpacesBlock([(0,0),(0,1)]) print(spaces) AttributeError Traceback (most recent call last) \block.py in <module> space = SpacesBlock([(0,0),(0,1)]) ----> print(spaces) AttributeError: 'SpacesBlock' object has no attribute 'name' I thought it was the from_coords constructor but it works fine a = Block.from_coords("A",[(0,0),(1,0)]) print(a) Name: A Coords: [(0, 0), (1, 0)] I know I can just define the name and coords in the init of SpacesBlock and everything is fine but I am very curious about why it doesn't work as I thought it would. What am I missing?


from_coords is not a constructor. It is a factory method. __init__ actually initializes an already-existing (i.e., allocated) object. Because it's an ordinary method, receiving a self parameter, it is able to do so. A classmethod cannot initialize an already-existing object unless you provide it one explicitly. What happens in your code is that super().from_coords(" ",coords) creates a separate instance of Block, which is then discarded. The self instance of SpacesBlock doesn't get its .name set; that happened to the other instance. The point of @classmethod (as opposed to @staticmethod) is that, because you receive a parameter which is the class, it can still behave polymorphically even though you don't have an object instance. (As you've found, you can use this to make __new__ work polymorphically.) Your factory method can already behave polymorphically: SpacesBlock.from_coords will call the method, and pass SpacesBlock as the cls, so that cls.__new__ creates a new SpacesBlock instance. However, __init__ doesn't get called this way; and with your current organizational structure, it's not clear how you would call it or what you would pass. Real justified uses for __new__ are rare. The normal way to use factory methods is to have them call __init__, by determining parameters to use for the __init__ call. In your case, a list of coordinates could be whatever arbitrary positions; but a width, height and location give you a way to specify multiple coordinates. It would be easier, therefore, to use the actual constructor for the list-of-coordinates approach to construction. With that setup, the code looks something like: class Block: def __init__(self, name: str, coords: List[Tuple]): self.name = name self.coords = coords @classmethod def from_grid(cls, name: str, h: int, l: int, location: Tuple[int,int]): coords = [tuple_sum(location, (i//l , i%l)) for i in range(h*l)] return cls(name, coords) class SpacesBlock(Block): pass # thus far, doesn't actually do anything different The important thing to note here is that from_grid can be used for either class now. Block.from_grid creates a Block instance, and SpacesBlock.from_grid creates a SpacesBlock instance - in either case, using the (height, length, location) approach. To create either class from a list of coordinates directly, simply call the constructor directly.

When you execute spaces = SpacesBlock([(0,0),(0,1)]) you create an instance SpaceBlock. Then __init__ runs to initialize this object. The problem is, that the __init__ function in SpaceBlock does not modify the instance you have just created. Instead, it creates and instantiates another object. This new object is not used for anything, and the one you originally created it left without any modifications, in particular without the name and coords attributes. You could fix it by modifying __init__ e.g. as follows: def __init__(self, coords): x = super().from_coords(" ",coords) self.name = x.name self.coords = x.coords but this would be an unnecessarily convoluted code.