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

Checking of **kwargs in concrete implementation of abstract class method. Interface issue?

我正在尝试实施策略设计模式,为要以模块化方式实施的基础算法创建接口。

目前,根据下面的代码,我有一个 top-level/parent 抽象 class (ParentAbstractStrategy) 定义了 strategy 方法的基本接口。

我还从这个摘要 class (ChildAbstractStrategy) 下一级。

我有两个抽象 classes 的原因是因为它们需要包含的属性;请参阅 __init__ 方法。 ChildAbstractStrategyParentAbstractStrategy 的特例,因为它存储了一个附加属性:attr2。 否则它的接口是相同的,正如相同的 strategy 方法签名所见。

有时,我希望能够直接subclass ParentAbstractStrategy并实现strategy方法(见ConcreteStrategyA),但其他时候我想成为能够子class ChildAbstractStrategy,因为需要额外的属性(参见ConcreteStrategyB)。

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

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

当前在 subclasses 中使用 **kwargs for "method overloading" 的解决方案感觉真的很乱,我不确定这是否意味着 [=] 有问题59=]继承方案或接口设计,或两者兼而有之。

有没有一种方法可以让我以更简洁的方式实现这些设计目标。感觉好像我在这里遗漏了一些大图,也许 class/interface 设计不好。也许为 strategy 方法创建两个具有不同签名的不相交的抽象 classes?

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)

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

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

回答我自己的问题。

在这种情况下,我对 **kwargs 的用法是 'bad'。为什么? 据我所知,**kwargs 通常用于:

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

在函数调用中使 **kwargs 成为 必需的 违背了他们的目的;应该使用需要显式提供的位置参数。这样,函数 提供的接口必须 由调用者显式满足。

在界面中使用 **kwargs 还有另一个问题,就像我一样。它涉及到LSP(Liskov Substitution Principle,见https://en.wikipedia.org/wiki/Liskov_substitution_principle)。当前的实现滥用 **kwargs 试图为子类中的 strategy 方法定义一个变量接口。尽管在语法上所有 strategy 方法的函数签名都匹配,但在语义上接口是不同的。这违反了 LSP,这要求我可以,例如在考虑它们的接口时,对 ParentAbstractStrategy 的任何后代都一视同仁,例如我应该能够同样对待 ConcreteStrategyAConcreteStrategyBstrategy 方法。

我的解决方案是什么? 我已将 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)

与父代 类 的接口相匹配。