美文网首页程序员Android开发
让人眼花缭乱的无限继承

让人眼花缭乱的无限继承

作者: 岛上码农 | 来源:发表于2020-12-05 16:11 被阅读0次

    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行。

    相关文章

      网友评论

        本文标题:让人眼花缭乱的无限继承

        本文链接:https://www.haomeiwen.com/subject/ttugwktx.html