是 subclassing a python class 来更改初始化程序的最佳模式吗?

Is subclassing a python class to change the initialiser the best pattern to use?

我正在设计一个功能相当强大的库 class - 我们可以将其视为一个列表。

我希望用户能够创建自己的列表,并预先填充特定内容。例如,诸如“向列表中添加 n 个相同的项目”或“使用 a->b 中的数字进行初始化”之类的事情。请注意,这些东西并没有真正改变列表的工作方式,它们只是以某种复杂的方式进行设置。

用户将需要从不同的代码位使用这些,以便将来能够重用它们。

我可以看到用户执行此操作的两种可能方式:

创建一个为他们构建对象的函数,例如:

def make_item_list(n,x):
    ls = [x]
    ls *= n
    return ls

或者,子class列表以添加初始化程序:

class RepeatList(List):
    def __init__(self,n,x):
        super().__init__()
        self.listitems = [x]*n

显然这是一个玩具示例,但其中哪个更明智 'pythonic'?如果 Python 中还有其他模式可以做到这一点,它们是什么?如果模式的选择取决于我没有提到的其他因素,它是什么以及它将如何影响决策制定?如果您需要,我很乐意提供更多信息。谢谢!

我倾向于使用 class,因为您可以扩展它的功能,例如添加对用户有帮助的方法。

class List:
    pass


class GenerateList(List):
    def __init__(self, n, x, repeat=True):
        super().__init__()
        if repeat:
            self.listitems = [x] * n
        else:
            self.listitems = list(range(n, x+1))

    def get_even(self, flip=False):
        ret = [v for v in self.listitems if v%2 == 0]
        return ret if not flip else ret[::-1]

    def get_odd(self, flip=False):
        ret = [v for v in self.listitems if v%2 != 0]
        return ret if not flip else ret[::-1]

# Request a repeated list.
m = GenerateList(3, 2, repeat=True)
repeatlist = m.listitems

# Request a list in a range.
a = GenerateList(1, 10, repeat=False)
mylist = a.listitems
mylistodd = a.get_odd()
mylisteven = a.get_even(flip=True)

print(repeatlist)
print(mylist)
print(mylistodd)
print(mylisteven)

输出:

[2, 2, 2]
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
[1, 3, 5, 7, 9]
[10, 8, 6, 4, 2]

大多数风格指南都不赞成过多的子classing。 pylint 的默认配置会在没有至少几种方法的情况下抱怨 classes 是有原因的。 Python 有足够多的轻量级方法来实现相同的目的。正如您自己所说,工厂功能做同样的工作。

也有点别扭。您是否真的希望您的用户创建 class 类 ListFromFileListFromString 而不是简单的函数 make_list_from_filemake_list_from_string

除了简单的风格,还有功能上的原因需要考虑:

  1. 子class 将允许您检查类型信息。这有关系吗?可能不是,但当然有些情况下你 subclass 只是为了创建一个新类型的简单目的,例如异常 classes
  2. 子class 具有更高的运行时开销。特别是方法查找会比较慢。
  3. Subclassing 可能会导致混乱的行为,或者在编程时可能需要额外注意。例如,假设您要实现 __getitem__(self, slice) 并喜欢 Python 的列表,return 给定切片的副本(例如 list[0:10])。副本是您的基础 class 还是您的子 class 的实例?如果是subclass的实例,如何确保init方法接受正确的参数?如果它将是基 class 的一个实例,任何其他被覆盖的行为都将消失。

我建议如下:

  1. 创建一个简单而健壮的 init 方法并告诉人们不要覆盖它,或者只用相同的参数覆盖它
  2. 使用 class 方法作为某些类型参数的附加构造函数,例如来自文件,来自 python 列表,无论什么
  3. 对于简单的情况提倡简单的独立函数
  4. 仅在添加新行为时建议 subclassing
  5. 提倡在为这些复杂情况添加新的构造函数时添加新的class方法

最后一个想法:了解你的用户。如果你和我一样,他们更熟悉使用自由函数和模块的结构化编程,而不是使用继承的面向对象编程。当然,如果你的用户对OOP比较熟悉,可能是因为你曾经是Java店铺,可能恰恰相反

附录:作为 subclassing 出错的案例研究,请查看 numpy 及其 ndarray subclasses。特别是矩阵和内存映射。找个时间尝试一下,看看在出现问题之前你能走多远,因为语义不一样。如果将矩阵传递给函数,您可能需要一个矩阵作为 return 值。但是如果你传递一个 memmap,你就不能这样做,因为你不能简单地创建一个新的 memmap。现在,您还必须仔细考虑每个函数是否要使用 np.asarraynp.asanyarray 来清理您的输入,并了解您可能调用或不调用的不同语义。

如果连 numpy 的人都搞砸了,您和您的用户正确的可能性有多大?