Python类层次结构问题

6

我有一个类层次结构:

class ParentClass:

    def do_something(self):
        pass # child classes have their own implementation of this

class ChildClass1(ParentClass):

    def do_something(self):
        <implementation here>

class ChildClass2(ParentClass):

    def do_something(self, argument_x):
        <implementation here>

class ChildClass3(ParentClass):

    def do_something(self, argument_y):
        <implementation here>

这里有两个问题:
  • 在子类中,方法do_something()的接口不同:它在子类2和3中接受一个参数,但在子类1中没有参数
  • do_something()的参数在子类2和3中具有不同的名称,以强调它们在不同的子类中具有不同的含义。下面的用法示例将更加清晰明了。
这是如何使用这些类:
有一个工厂类返回实例:
class ChildFactory:

    def get_child(self, argument):
        if argument == '1':
            return ChildClass1()
        elif argument == '2':
            return ChildClass2()
        elif argument == '3':
            return ChildClass3()

在代码的后面:
...
# pseudocode, not python
child_type = ? # can have values '1', '2' or '3' at this moment
var1 = 1
var2 = 'xxx'
# var1 and var2 have different types, just to emphasize the difference in their
# meaning when being passed as arguments to do_something()
# this was mentioned above (the second problem)
child = ChildFactory.get_child(child_type)
if child is an instance of ChildClass1, child.do_something() is called
if child is an instance of ChildClass2, child.do_something(var1) is called
if child is an instance of ChildClass3, child.do_something(var2) is called
# end of pseudocode

问题:

  1. 上述两个问题是糟糕设计的标志吗?如果是,正确的层次结构设计方法是什么?
  2. 如何在Python中统一编写伪代码片段?主要问题是避免为每个特定情况使用巨大的if/else语句,因为它将使ChildFactory.get_child()的if/else语句加倍。
4个回答

14

方法名称相同但参数不同是一种代码异味。

"在子类中,方法 do_something() 的接口不同:在子类2和3中它接受一个参数,但在子类1中没有参数"

你没有解释为什么会这样。有两个很好的原因:

  • 子类1有一个默认值。

  • 子类2忽略了该值。

几乎任何其他原因都表明do_something真正不同的,并且应该有不同的名称。

如果子类1有一个默认值,则可以在方法函数的参数中显式地编写默认值。

class ChildClass1( ParentClass ):
    def do_something( argument_x= None )
        ....
如果子类1忽略该值,则直接忽略该值。不要费力地去忽略一个值。
class ChildClass1( ParentClass ):
    def do_something( argument_x )
        return True

一个多态方法函数未使用所有参数值并不是什么神奇的事情。

"do_something() 的参数在子类2和子类3中拥有不同的名称以强调它们在不同类中具有不同的含义。"

这仅仅是糟糕的设计。你不能使用不同的参数名称来定义相同的方法函数,因为它们执行不同的操作。

给相同的方法函数命名是错误的。如果它们是类似操作的不同实现,则参数本质上会具有相同的含义。

如果它们确实执行不同的操作,则你没有多态,而且不应该将这些方法命名为相同的名称。

当两个类中的方法执行根本不同的操作时——需要具有不同名称的不同参数以使其明显——这些方法必须不具有相同的名称。当名称不能描述方法实际执行的操作时,名称就失去了含义。

注意

你的代码在Python中可以工作,由于鸭子类型的原理。只要方法名称匹配,参数类型甚至不必接近匹配。然而,这是非常糟糕的设计,因为方法之间的基本差异如此之大。


考虑实现卡牌的类。一个卡牌类有一个方法,接受一个参数(另一张卡牌),并返回一个布尔值。如果它能打败那张卡牌,则返回True。小丑可以打败任何其他卡牌,它的方法将始终返回True,并且不需要输入参数。不确定这个例子是否合适。重点是在我的示例中,ChildClass1.self() 没有默认值。 - Alex_coder
1
@Alex_coder:Joker的卡牌比较仍然接受一个参数,只是不使用该参数。def beats(self, aCord): return True。尽管忽略了参数,但卡牌比较是该方法的含义 - S.Lott
感谢您清晰的解释。您关于含义的陈述是目前最好的AHA候选人。这是否意味着在这种情况下,明智的做法是不将pylint警告视为命令,只需将其静音并让常识占上风?我可以看到优点:如果我故意忽略一些冗余问题,代码就变得统一了。 - Alex_coder
@Alex_coder: "little redundancy issues"? 你是指未使用的参数警告吗?是的,你必须消除它才能实现正确的多态性。 - S.Lott
如果我在子类中有名为from_file的静态工厂方法,而基类中的工厂方法需要一个子类方法没有的额外参数,那该怎么办呢? 实际上,对于静态方法来说,这是否重要? - Ray
1
有趣的是,@S.Lott说了四次同样的话,但没有一个解释为什么这是不好的。他只是重复说这是不好的,它们不应该有相同的名称等等。 - episodeyang

2

像这样的抽象问题很难回答。如果我们知道你试图解决什么问题,那么回答会更容易。我可以告诉你通常看到这样的东西意味着有问题:

if isinstance(obj, cls1):
    ...
elif isinstance(obj, cls2):
    ...

通常情况下,这意味着你应该定义一个新方法而不是使用 if/elif 语句。在 Python 中,如果需要的话,你可以在类定义之外定义方法。如果两个可互换的类具有相同名称但需要不同数量参数的方法,则通常表示这些类并非真正可互换。不同的方法应该使用相同的参数或者它们应该有不同的名称。在 ParentClass 中定义从未被调用的方法(例如 do_something)也是不必要的——这是从 C++/Java/C# 转入 Python 的程序员常见的错误。

我还看到一种选择,就是让do_something()接受两个带有默认值的参数,但类似pylint的代码检查器将会抱怨传入函数的参数未被使用。因此陷入了进退两难的境地。 - Alex_coder
“*args, **kwargs” 可用于变量参数计数,但我同意你的观点,这似乎是糟糕的设计。 - Georg Schölly
应该有一种方法告诉pylint你打算让变量未使用。不要使用奇怪的控制结构只是为了让pylint高兴。 - Dietrich Epp
+1 是 Python 中关注特定实例名称通常是一个不良信号。 - Francesco

1

如果要交替使用某些东西,它们应该拥有相同的接口。对于一个方法来说,这意味着相同数量的参数、相同的含义和相同的名称以及相同的顺序。如果它们的行为不完全相同,只需给它们不同的名称,并让它们看起来可以互换。


-4
你可以这样做,使签名相同:
class ParentClass:
    pass

class ChildClass1(ParentClass):

    def do_something(self, **kwargs):
        <implementation here>

class ChildClass2(ParentClass):

    def do_something(self, **kwargs):
        argument_x = kwargs[argument_x]
        <implementation here>

class ChildClass3(ParentClass):

    def do_something(self, **kwargs):
        argument_y = kwargs[argument_y]
        <implementation here>

工厂可以只是一个字典:

childfactory = {1:ChildClass1, 2:ChildClass2, 3:ChildClass3}

然后稍后:

...
# pseudocode, not python
child_type = ? # can have values '1', '2' or '3' at this moment
var1 = 1
var2 = 'xxx'
# var1 and var2 have different types, just to emphasize the difference in their
# meaning when being passed as arguments to do_something()
# this was mentioned above (the second problem)
child = childfactory[child_type]()
child.do_something(argument_x=val1, argument_y=var2)
# end of pseudocode

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接