Python继承场景下类方法工厂的责任边界与实践困惑
最近我在开发一个库的时候,对Python里@classmethod和@staticmethod的区别产生了一些实际的困惑——尤其是类方法里的cls参数用来支持子类实例构造这一点,遇到了不少问题。先给大家看个我写的示例代码:
class Line: def __init__(self, length): self.length = length @classmethod def unit(cls): return cls(1) def clone(self): return Line(self.length) def __mul__(self, other): return Square(self.length * other.length) class Square: def __init__(self, area): self.area = area
按照常规的做法,unit工厂用@classmethod并通过cls(...)来构造实例,其实是在传递一个信号:“这个类是为继承设计的,尽管放心子类化它”——不然直接用Line(1)构造就行了。但当用户真的去子类化这些类、添加自己的功能时,问题就来了:
class ColoredLine(Line): def __init__(self, length, color): super().__init__(length) self.color = color class ColoredSquare(Square): def __init__(self, area, color): super().__init__(area) self.color = color
问题1:子类构造函数兼容性强制要求
按道理来说,子类修改构造函数来添加参数是完全合理的,但Line的unit方法完全不知道这一点——它调用ColoredLine.__init__时不会传新增的必填color参数,直接就报错了。这就要求用户必须保持和父类构造函数的兼容性(哪怕父类构造逻辑很复杂还会随库更新变化)。当然,用户可以给color加个默认值比如color="red"来解决,但这终究是个妥协。
问题2:实例方法的类型一致性困境
像clone这类返回同类型实例的方法,也应该保持一致性——理论上应该用type(self)(self.length)替代Line(...)。但这么做的话,一个蓝色ColoredLine的克隆会变成一个默认红色的同长度ColoredLine,颜色信息丢失了。那是不是父类的clone方法应该用__dict__来通用复制所有属性呢?这又引入了新的复杂度。
问题3:跨类型返回方法的继承盲区
还有__mul__方法,它完全没法知道现在应该返回ColoredSquare而不是普通的Square。也就是说,Python式的继承考虑只在返回同类型对象的方法(比如__add__)上大多能生效,但像__mul__这类返回其他类型的方法就完全失效了,__sub__也可能面临同样的情况。
我现在纠结的是,到底该在父类里预先实现多少子类可能需要的功能,又该把哪些责任留给子类用户?难道用户必须通读父类的所有实现,逐个判断哪些方法需要重写吗?
其实Python标准库的datetime.date对象也面临着所有这些问题:它默默要求子类构造函数必须和它的类方法工厂兼容;有些方法用return type(self)(...),完全忽略子类新增的实例成员;__sub__永远返回timedelta类型。
在我的库开发里,我甚至在考虑放弃这种“Pythonic”的写法,转而更一致地使用@staticmethod和直接MyClass()来实现工厂方法,替代@classmethod、cls()和type(self)()。这样用户可以自由子类化,但所有适配工作都由他们自己负责,而且类型注解也会简单很多。
不过有没有明确的指导原则(不只是推荐用@classmethod做工厂,还要包含如何处理这些实际问题的方案)?或者我的思考里有没有什么概念上的误区?
备注:内容来源于stack exchange,提问作者mqnc

