使用父项的替代构造函数创建子项 class 时出现问题

Problem creating a child class using an alternative constructor of parent

我有一个 class 可以在二维板上创建块。我定义了它,所以它在给定高度、长度和在板上的位置的情况下这样做,但也制作了一个替代构造函数,通过在板上传递其坐标来创建块。


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}'

我正在尝试为版块中的空白区域创建子版 class。我认为使用 from_coords 构造函数可以完成所有操作,但出于某种原因我不明白创建的元素未初始化,即没有名称或坐标属性。

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'

我以为是 from_coords 构造函数,但它工作正常

a = Block.from_coords("A",[(0,0),(1,0)])
print(a)

Name: A
Coords: [(0, 0), (1, 0)]

我知道我可以在 SpacesBlock 的初始化中定义名称和坐标,一切都很好,但我很好奇为什么它不能像我想象的那样工作。我错过了什么?

当你执行

spaces = SpacesBlock([(0,0),(0,1)])

您创建了一个实例 SpaceBlock。然后 __init__ 运行以初始化此对象。问题是,SpaceBlock 中的 __init__ 函数不会修改您刚刚创建的实例。相反,它创建并实例化另一个对象。这个新对象没有任何用途,您最初创建它的对象没有任何修改,特别是没有 namecoords 属性。

您可以通过修改 __init__ 来修复它,例如如下:

def __init__(self, coords):
    x = super().from_coords("  ",coords)
    self.name  = x.name
    self.coords = x.coords

但这将是一个不必要的复杂代码。

from_coords 不是构造函数。这是一个工厂方法

__init__ 实际上初始化了一个已经存在的(即已分配的)对象。因为它是一个普通的方法,接收一个self参数,所以它能够这样做。 classmethod 不能初始化一个已经存在的对象,除非你明确地提供它。

您的代码中发生的事情是 super().from_coords(" ",coords) 创建了 Block 单独 实例,然后将其丢弃。 SpacesBlockself 实例没有设置 .name;发生在另一个实例上。

@classmethod(与 @staticmethod 相对)的要点是,因为您收到的参数是 ,所以 class,即使您没有对象实例,它仍然可以表现出多态性。 (正如您所发现的,您可以使用它来使 __new__ 以多态方式工作。)

您的工厂方法已经可以表现出多态性:SpacesBlock.from_coords 将调用该方法,并将 SpacesBlock 作为 cls 传递,以便 cls.__new__ 创建一个新的 SpacesBlock 实例。但是,__init__ 不会以这种方式调用;并且根据您当前的组织结构,不清楚您将如何称呼它或通过什么。

__new__ 真正合理的用途很少见。

使用工厂方法的正常方法是让它们调用 __init__,方法是 确定要使用 的参数以进行 __init__ 调用。在您的情况下,坐标列表可以是任意位置;但是宽度、高度和位置为您提供了一种指定多个坐标的方法。因此,使用实际构造函数进行坐标列表构造方法会更容易。

使用该设置,代码如下所示:

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

这里要注意的重要一点是 from_grid 现在可以用于任何 class。 Block.from_grid 创建一个 Block 实例,而 SpacesBlock.from_grid 创建一个 SpacesBlock 实例 - 在任何一种情况下,都使用 (height, length, location) 方法。要直接从坐标列表创建 class,只需直接调用构造函数即可。