在抽象类方法的具体实现中检查 **kwargs。接口问题?

Spa*_*ace 7 python abstract-class strategy-pattern keyword-argument python-3.x

我正在尝试实现 Strategy 设计模式,以便为以模块化方式实现的底层算法创建一个接口。

目前,根据下面的代码,我有一个顶级/父抽象类 ( ParentAbstractStrategy),它定义了该strategy方法的基本接口。

我也有一个从这个抽象类 ( ChildAbstractStrategy)向下一级。

我有两个抽象类的原因是因为它们需要保存的属性;见__init__方法。 ChildAbstractStrategy是一种特殊情况,ParentAbstractStrategy因为它存储了一个附加属性:attr2。否则它的接口是相同的,如相同的strategy方法签名所见。

有时,我希望能够直接子类化ParentAbstractStrategy并实现strategy方法(请参阅ConcreteStrategyA),但有时我希望能够子类化ChildAbstractStrategy,因为需要额外的属性(请参阅ConcreteStrategyB)。

另一个复杂问题是,在任一抽象类的某些子类中,我希望能够处理strategy方法中的其他参数。这就是为什么我添加**kwargs到该strategy方法的所有签名中的原因,以便我可以根据具体情况将我想要的任何其他参数传递给子类。

这就产生了最后一个问题:这些额外的参数在子类中不是可选的。例如,在strategy方法ConcreteStrategyB我想肯定的是,来电者在第三个参数传递。我基本上是在滥用**kwargs提供可能应该是位置参数的内容(因为我不能给它们合理的默认值并且需要强制执行它们的存在)。

这种**kwargs在子类中用于“方法重载”的当前解决方案感觉非常混乱,我不确定这是否意味着类继承方案或接口设计存在问题,或两者兼而有之。

有没有办法以更简洁的方式实现这些设计目标。感觉就像我在这里错过了一些大图,也许类/界面设计很糟糕。也许为该strategy方法创建两个具有不同签名的不相交抽象类?

import abc


class ParentAbstractStrategy(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def __init__(self, attr1):
        self.attr1 = attr1

    @abc.abstractmethod
    def strategy(self, arg1, arg2, **kwargs):
        raise NotImplementedError


class ChildAbstractStrategy(ParentAbstractStrategy, metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def __init__(self, attr1, attr2):
        super().__init__(attr1)
        self.attr2 = attr2

    @abc.abstractmethod
    def strategy(self, arg1, arg2, **kwargs):
        raise NotImplementedError


class ConcreteStrategyA(ParentAbstractStrategy):
    def __init__(self, attr1):
        super().__init__(attr1)

    def strategy(self, arg1, arg2, **kwargs):
        print(arg1, arg2)


class ConcreteStrategyB(ChildAbstractStrategy):
    def __init__(self, attr1, attr2):
        super().__init__(attr1, attr2)

    def strategy(self, arg1, arg2, **kwargs):
        print(arg1, arg2)
        arg3 = kwargs.get("arg3", None)

        if arg3 is None:
            raise ValueError("Missing arg3")
        else:
            print(arg3)
Run Code Online (Sandbox Code Playgroud)

这是一个解释器会话,演示了它当前的工作方式:

>>> a = ConcreteStrategyA(1)
>>> a.attr1
1
>>> a.strategy("a", "b")
a b
>>> b = ConcreteStrategyB(1, 2)
>>> b.attr1
1
>>> b.attr2
2
>>> b.strategy("a", "b")
a b
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/space/strategy.py", line 42, in strategy
    raise ValueError("Missing arg3")
ValueError: Missing arg3
>>> b.strategy("a", "b", arg3="c")
a b
c
Run Code Online (Sandbox Code Playgroud)

Spa*_*ace 6

回答我自己的问题。

在这种情况下我的使用**kwargs是“坏”的。为什么?据我所知,**kwargs通常用于:

  1. 包装函数,例如装饰器。
  2. 收集函数了解的函数的额外关键字参数(例如,请参阅https://matplotlib.org/api/_as_gen/matplotlib.pyplot.plot.html?highlight=plot#matplotlib.pyplot.plot中的用法)。在这种情况下,这些**kwargs参数是可以传递到函数中的可选参数,并且它们具有合理的默认值。

函数调用中要求**kwargs它违背了它们的目的;应使用需要显式提供的位置参数。这样,调用者必须明确满足函数提供的接口。

**kwargs正如我所遇到的那样,在界面中使用还有另一个问题。它涉及LSP(里氏替换原则,参见https://en.wikipedia.org/wiki/Liskov_substitution_principle)。当前的实现正在滥用**kwargs试图为strategy子类之间的方法定义变量接口。尽管在语法上所有方法的函数签名都strategy匹配,但在语义上接口是不同的。这违反了 LSP,这要求我ParentAbstractStrategy在考虑它们的接口时可以处理相同的任何后代,例如我应该能够处理相同strategy的方法。ConcreteStrategyAConcreteStrategyB

我的解决方案是什么?我已更改该方法的接口,strategy不再包含**kwargs而是使用位置参数和带有默认值的关键字参数的混合。例如,如果ConcreteStrategyB仍然需要第三个参数arg3ConcreteStrategyA不需要,我可以将类更改为如下所示:

class ConcreteStrategyA(ParentAbstractStrategy):
    def __init__(self, attr1):
        super().__init__(attr1)

    def strategy(self, arg1, arg2, arg3=None):
        print(arg1, arg2)


class ConcreteStrategyB(ChildAbstractStrategy):
    def __init__(self, attr1, attr2):
        super().__init__(attr1, attr2)

    def strategy(self, arg1, arg2, arg3=None):
        print(arg1, arg2)
        assert arg3 is not None
        print(arg3)
Run Code Online (Sandbox Code Playgroud)

两个父类的接口都更改为匹配。