Python语言的一个优势是简洁易用。是否简洁易用仅仅是Python语言本身的一个话题,但“好消息”是如果你想创造那种一大堆继承、混乱的内部关系的代码,也是可以的!
今天烦人的代码来自于验证某些math-y
数学分析代码。一开始,他们是发现文档和代码对应不上,只得去阅读代码看看代码到底做了什么事情。在一个文件中,找到了一个业务关注的核心类,定义如下所示。
class WeibullFitter(KnownModelParametricUnivariateFitter):
# snip: some math
表面上看,这个数学方法看上去多少是对的,但有个问题是:父类是怎么调用的?没办法,只能往上查看其父类,父类的代码如下:
class KnownModelParametricUnivariateFitter(ParametricUnivariateFitter):
_KNOWN_MODEL = True
这个所谓的基类压根就算不上基类,“鸡肋”还差不多!仅仅是设置了一个属性为True,沿着继承树再往上走,找到基类代码如下:
class ParametricUnivariateFitter(UnivariateFitter):
# ...snip...
呃,虽然这还不是最终的基类,但至少这个类还实现了某些方法。这样的写法肯定让你怀疑代码结构的问题了,目前为止还没找到真正烦人的代码。但目前我们可以讨论一下为什么说继承在某种程度上是有害的。继承会自动产生依赖,这意味着如果要理解子类的行为必须同时了解父类的行为。当然,好的继承的实现会划定这些边界,但显然我们看到的是一个反面例子。
除此之外,由于Python是一个弱类型语言,因此继承的优点之一的多态在Python里都算不上优点了。没错,我们可以通过Python的类型注解去做类型检查,这会让多态派上用场,但这没法判断整个继承树。
撇开这一切不谈,使用继承的主要原因是我们可以将公共的业务逻辑部分抽离开,让子类系统无需处理这些公共的业务逻辑。因此,即便存在多种可能的类型,我们仍然可以调用具体实例的某个方法,并且能够保证如期望那样运行,且可以呈现不同的行为。接着来看ParametricUnivariateFitter
这个类,类中定义了如下方法:
def _fit_model(self, Ts, E, entry, weights, show_progress=True):
if utils.CensoringType.is_left_censoring(self): # Oh no.
negative_log_likelihood = self._negative_log_likelihood_left_censoring
elif utils.CensoringType.is_interval_censoring(self): # Oh no no no.
negative_log_likelihood = self._negative_log_likelihood_interval_censoring
elif utils.CensoringType.is_right_censoring(self): # This is exactly what I think it is isn't it.
negative_log_likelihood = self._negative_log_likelihood_right_censoring
# ...snip...
注释是问题发现人提供的。为了满足整个子类树,每个子类都使用了类型检查,因此子类不同的行为是通过类型检查来实现的。这可以说是100%的臭代码!当我们去阅读CensoringType
代码的时候,让我们再次确信了这一点。
class CensoringType(Enum): # enum.Enum from the standard library
LEFT = "left"
INTERVAL = "interval"
RIGHT = "right"
@classmethod
def right_censoring(cls, function: Callable) -> Callable:
@wraps(function) # functools.wraps from the standard library def f(model, *args, **kwargs):
cls.set_censoring_type(model, cls.RIGHT)
return function(model, *args, **kwargs)
return f
@classmethod
def left_censoring(cls, function: Callable) -> Callable:
@wraps(function)
def f(model, *args, **kwargs):
cls.set_censoring_type(model, cls.LEFT)
return function(model, *args, **kwargs)
return f
@classmethod
def interval_censoring(cls, function: Callable) -> Callable:
@wraps(function)
def f(model, *args, **kwargs):
cls.set_censoring_type(model, cls.INTERVAL)
return function(model, *args, **kwargs)
return f
@classmethod
def is_right_censoring(cls, model) -> bool:
return cls.get_censoring_type(model) == cls.RIGHT
@classmethod
def is_left_censoring(cls, model) -> bool:
return cls.get_censoring_type(model) == cls.LEFT
@classmethod
def is_interval_censoring(cls, model) -> bool:
return cls.get_censoring_type(model) == cls.INTERVAL
@classmethod
def get_censoring_type(cls, model) -> str:
return model._censoring_type
@classmethod
def str_censoring_type(cls, model) -> str:
return model._censoring_type.value
@classmethod
def set_censoring_type(cls, model, censoring_type) -> None:
model._censoring_type = censoring_type
即便是你不懂Python代码,你也会想到这是一个枚举。@classmethod
是Python注解静态方法的修饰符,就像类成员方法中使用self
作为第一个参数一样,静态方法使用cls
作为方法的第一个参数,以代表类本身。
去看一下right_censoring
方法也很重要,因为这些方法看起来是“装饰器”。他们使用了@wraps
来修饰定义的局部方法。这个right_censoring
方法需要接收一个可调用的函数(也许是一个构造函数),然后将该方法的实现用内部以“f”方法替换。并且在这里面,在调用构造函数之前,修改了构造方法的参数值。
如果你不经常使用Python编程的话,你可能会觉得十分困惑,因此我们来看看这个方法如何使用的:
@CensoringType.right_censoringclass SomeCurveFitterType(SomeHorribleTreeOfBaseClasses):
def __init__(self, model, *args, **kwargs):
# snip
instance = SomeCurveFitterType(model, *args, **kwargs)
在最后一行代码,并没有调用```init``构造函数,而是首先通过内部的f函数,这个函数最重要的一件事就是在调用构造函数前,调用静态方法cls.set_censoring_type(model, cls.RIGHT)
。
如果你对此完全不理解,也不用感到糟糕。装饰器是Python的一种独特的方式去修改类和方法的实现。这个特性允许你在传统的方式中混合使用声明式编程。
最终,为了理解WeibullFilter
这个类的行为,你必须阅读半大祖先类代码才能看到BaseFitter
类型,然后你必须注意应用了什么装饰器以及装饰器对应的祖先类,只有这样才真正知道这个业务的功能是什么。
如果你是写这些代码的人,也许会觉得这种混入装饰器和继承的写法扩展性很高。可以快速轻松地在框架里插入一个曲线拟合方法。你甚至会以为你的大脑创造了这个星球上最具工程化的框架。而余下的我们,必须像盲人那样使用棍子去探路。
最后我们的问题提交人总结到:
我对此感到很糟糕。这个库看着不错,数学方法本身也不错,并且这个库非常有用,减轻了我很多工作……
这一系列的行为导致了性能问题,我不得不重新做不同的实现。因为这一系列的嵌套调用将简单的处理过程的性能给破坏了——执行时间可能超过10分钟。而新写的代码几乎是瞬间完成,包含注释也就不到20行。
网友评论