当前位置: 首页 > 面试题库 >

在继承时避免长的构造函数而不隐藏构造函数,可选参数或功能

姚淳
2023-03-14
问题内容

我有一个特殊的问题,但我将使示例更一般。我有一个带有强制构造函数参数的 Parent 类和一些可选的参数,每个参数都有一个默认值。然后,我从中继承
Child 并添加一个必需参数,并从 Child 继承 GrandChild 并将另一个必需参数添加到构造函数。结果类似于此:

class Parent():

    def __init__(self, arg1, opt_arg1='opt_arg1_default_val', opt_arg2='opt_arg2_default_val',
                 opt_arg3='opt_arg3_default_val', opt_arg4='opt_arg4_default_val'):
        self.arg1 = arg1
        self.opt_arg1 = opt_arg1
        self.opt_arg2 = opt_arg2
        self.opt_arg3 = opt_arg3
        self.opt_arg4 = opt_arg4


class Child(Parent):
    def __init__(self, arg1, arg2, opt_arg1, opt_arg2, opt_arg3, opt_arg4):
        super().__init__(arg1, opt_arg1, opt_arg2, opt_arg3, opt_arg4)
        self.arg2 = arg2

class GrandChild(Child):
    def __init__(self, arg1, arg2, arg3, opt_arg1, opt_arg2, opt_arg3, opt_arg4):
        super().__init__(arg1, arg2, opt_arg1, opt_arg2, opt_arg3, opt_arg4)
        self.arg3 = arg3

问题是,这看起来很难看,尤其是如果我想从Child继承更多的类时,我将不得不复制/粘贴该新类的构造函数中的所有参数。

在寻找解决方案时,我在这里发现可以使用** kwargs来解决此问题,如下所示:

class Parent():

    def __init__(self, arg1, opt_arg1='opt_arg1_default_val', opt_arg2='opt_arg2_default_val',
                 opt_arg3='opt_arg3_default_val', opt_arg4='opt_arg4_default_val'):
        self.arg1 = arg1
        self.opt_arg1 = opt_arg1
        self.opt_arg2 = opt_arg2
        self.opt_arg3 = opt_arg3
        self.opt_arg4 = opt_arg4


class Child(Parent):
    def __init__(self, arg1, arg2, **kwargs):
        super().__init__(arg1, **kwargs)
        self.arg2 = arg2

class GrandChild(Child):
    def __init__(self, arg1, arg2, arg3,**kwargs):
        super().__init__(arg1, arg2,**kwargs)
        self.arg3 = arg3

但是,我不确定这是否正确。

创建这些类的对象时也有一些不便之处。我正在使用PyCharm进行开发,在这种情况下,IDE具有一种显示函数/类构造函数参数的有用方法。例如,在第一个示例中,

在此处输入图片说明

这使开发变得更加容易,并且还可以为将来的开发人员提供帮助,因为他们可以看到该函数还有哪些其他参数。但是,在第二个示例中,不再显示可选参数:

在此处输入图片说明

而且我认为在这种情况下使用** kwargs不是一个好习惯,因为必须深入研究代码直到Parent类,以检查其具有哪些可选参数。

我也研究过使用Builder模式,但是我要做的就是将参数列表从我的类移动到Builder类,而且我遇到了同样的问题,带有很多参数的生成器在继承时会在顶部创建更多的参数。已经存在的。同样,在Python中,就我所知,考虑到所有类成员都是公共的,并且无需使用setter和getter即可访问,Builder并没有多大意义。

关于如何解决此构造函数问题的任何想法?


问题答案:

基本思想是编写__init__为您生成该方法的代码,其中使用显式指定的所有参数,而不是通过*args和/或**kwargs,并且甚至无需在所有这些self.arg1 = arg1行中重复进行操作。

并且,理想情况下,它可以使添加PyCharm可用于弹出提示和/或静态类型检查的类型注释变得容易。1个

而且,当您使用它时,为什么不构建一个__repr__显示相同值的?甚至可能还有__eq__,和__hash__,以及字典比较操作符,以及往返于的转换,dict其键与每个JSON持久性的属性相匹配,并且…

或者,甚至更好的是,使用可以为您解决这些问题的库。

Python
3.7附带了这样的库dataclasses。或者,您可以使用第三方库(例如)attrs,该库可与Python
3.4和2.7(但有一定限制)一起使用。或者,对于简单的情况(对象是不可变的,并且您希望它们按照指定的顺序像其属性的元组一样工作),可以使用namedtuple,其适用于3.0和2.6。

不幸的是,dataclasses它不适用于您的用例。如果您只是这样写:

from dataclasses import dataclass

@dataclass
class Parent:
    arg1: str
    opt_arg1: str = 'opt_arg1_default_val'
    opt_arg2: str = 'opt_arg2_default_val'
    opt_arg3: str = 'opt_arg3_default_val'
    opt_arg4: str = 'opt_arg4_default_val'

@dataclass
class Child(Parent):
    arg2: str

…你会得到一个错误,因为它试图强制参数将arg2默认值参数后opt_arg1通过opt_arg4

dataclasses没有任何方法可以对参数(Child(arg1, arg2, opt_arg1=…)进行重新排序,也不能使其强制为仅关键字的参数(Child(*, arg1, opt_arg1=…, arg2))。attrs没有开箱即用的功能,但是您可以添加它。

因此,它并不像您希望的那么琐碎,但却是可行的。

但是,如果您想自己编写此代码,您将如何__init__动态创建函数?

最简单的选择是exec

您可能听说过这exec很危险。但这只是危险,如果您要传递来自用户的值。在这里,您只传递来自您自己的源代码的值。

它仍然很丑陋,但有时仍然是最好的答案。标准库namedtuple曾经是一个巨大的exec模板。,甚至当前版本都exec用于大多数方法,确实如此dataclasses

另外,请注意,所有这些模块都将字段集存储在私有类属性中的某个位置,因此子类可以轻松读取父类的字段。如果您不这样做,则可以使用该inspect模块获取Signature基类(或用于多重继承的基类)的初始化程序,然后从那里开始。但是,base._fields显然使用起来要简单得多(并允许存储通常不在签名中包含的额外元数据)。

这里是一个死的简单实现不处理大部分的功能attrsdataclasses,但不会为了所有自选前所有必须的参数。

def makeinit(cls):
    fields = ()
    optfields = {}
    for base in cls.mro():
        fields = getattr(base, '_fields', ()) + fields
        optfields = {**getattr(base, '_optfields', {}), **optfields}
    optparams = [f"{name} = {val!r}" for name, val in optfields.items()]
    paramstr = ', '.join(['self', *fields, *optparams])
    assignstr = "\n    ".join(f"self.{name} = {name}" for name in [*fields, *optfields])
    exec(f'def __init__({paramstr}):\n    {assignstr}\ncls.__init__ = __init__')
    return cls

@makeinit
class Parent:
    _fields = ('arg1',)
    _optfields = {'opt_arg1': 'opt_arg1_default_val',
                  'opt_arg2': 'opt_arg2_default_val',
                  'opt_arg3': 'opt_arg3_default_val',
                  'opt_arg4': 'opt_arg4_default_val'}

@makeinit
class Child(Parent):
    _fields = ('arg2',)

现在,你已经得到了完全__init__你想上的方法ParentChild,完全可检查2(含help),而不必重复自己。

1.我不使用PyCharm,但是我知道在3.7发行之前,他们的开发人员已经参与了讨论,@dataclass并且已经在为IDE添加对它的显式支持,因此它甚至不必评估类定义以获取所有这些信息。我不知道它是否在当前版本中可用,但是如果没有,我认为它将可用。同时,@dataclass已经可以通过IPython自动完成功能,emacs
flycheck等对我有效,这对我来说已经足够了。:)

2.…至少在运行时。PyCharm可能无法静态地很好地解决问题,无法完成弹出窗口。



 类似资料:
  • 人们有时会对类成员函数或成员变量的作用域问题感到困惑,尤其是,当基类与派生类的同名成员不在同一个作用域内时: struct B { void f(double); }; struct D : B { void f(int); }; B b; b.f(4.5); // OK // 调用的到底是B::f(doube)还是D::f(int)呢? // 实际情况往往会让人感到意外

  • 问题内容: 在为期末考试而学习时,我在正在学习的书中遇到了以下陈述。考虑以下代码: 是否必须在类B(super(x))的构造函数中调用类A的构造函数。本书指出这不是强制性的,因为它们具有确切数量和类型的参数。但是,当我在Java编译器中尝试此操作时,会抛出以下错误: 类A中的构造函数A不能应用于给定类型;必需:发现整数:无参数原因:实际和正式参数列表的长度不同 问题答案: 编译器会自动插入开头。

  • 问题内容: 我想知道为什么在Java中不继承构造函数?你知道当你上这样的课时: 稍后当你从继承时Super,java会抱怨没有定义默认的构造函数。解决方案显然是这样的: 这段代码是重复的,而不是干的和无用的(IMHO)…因此再次带来了问题: 为什么Java不支持构造函数继承?不允许这种继承有什么好处? 问题答案: 假设构造函数是继承的…则因为每个类最终都派生自Object,所以每个类最终都将带有无

  • 问题内容: 我想要一个构造函数,其参数会自动被所有子类继承,但是Java不允许我这样做 我不希望有写,等等。每个子类。有没有更聪明的方法来解决这个问题? 解决方案#1。构造一个可以在构造函数之后调用的方法。尽管对我的特定设计而言,这是可行的,但是我希望要求用户在构造函数中指定在编译时经过验证的某些参数(例如,不通过varargs / reflection)。 问题答案: 你不能 如果要在基类中有一

  • 在虚继承中,虚基类是由最终的派生类初始化的,换句话说,最终派生类的构造函数必须要调用虚基类的构造函数。对最终的派生类来说,虚基类是间接基类,而不是直接基类。这跟普通继承不同,在普通继承中,派生类构造函数中只能调用直接基类的构造函数,不能调用间接基类的。 下面我们以菱形继承为例来演示构造函数的调用: 运行结果: m_a=10, m_b=20 m_a=30, m_c=40 m_a=50, m_b=60

  • 我有一个继承自父类Point的子类行,我没有在子类中使用基类的构造函数,但是我得到了这个错误: '形状。点“不包含接受0个参数的构造函数 这是我的父类: 这是我的孩子班: