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

Geo*_*iuc 5 python inheritance constructor python-3.x

我有一个特殊的问题,但我会让这个例子更普遍。我有一个带有强制构造函数参数和一些可选参数的父类,每个参数都有一个默认值然后,我从它继承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
Run Code Online (Sandbox Code Playgroud)

问题是这看起来相当难看,特别是如果我想从 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
Run Code Online (Sandbox Code Playgroud)

但是,我不确定这是否是正确的方法。

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

在此输入图像描述

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

在此输入图像描述

我认为在这种情况下使用 **kwargs 不是一个好的做法,因为人们必须深入研究父类的代码来检查它有哪些可选参数。

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

关于如何解决这个构造函数问题有什么想法吗?

aba*_*ert 4

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

\n\n

而且,理想情况下,它可以轻松添加 PyCharm 可用于弹出提示和/或静态类型检查的类型注释。1

\n\n

而且,当您这样做时,为什么不构建一个__repr__显示相同值的呢?甚至可能是__eq__、 和 a __hash__,也可能是字典比较运算符,以及与dict每个 JSON 持久性的键匹配属性的 a 之间的转换,以及 \xe2\x80\xa6

\n\n

或者,更好的是,使用一个可以为您处理此问题的库。

\n\n

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

\n\n

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

\n\n
from dataclasses import dataclass\n\n@dataclass\nclass Parent:\n    arg1: str\n    opt_arg1: str = \'opt_arg1_default_val\'\n    opt_arg2: str = \'opt_arg2_default_val\'\n    opt_arg3: str = \'opt_arg3_default_val\'\n    opt_arg4: str = \'opt_arg4_default_val\'\n\n@dataclass\nclass Child(Parent):\n    arg2: str\n
Run Code Online (Sandbox Code Playgroud)\n\n

\ xe2 \x80\xa6 你会得到一个错误,因为它试图arg2通过.opt_arg1opt_arg4

\n\n

dataclasses没有任何方法可以重新排序参数 ( Child(arg1, arg2, opt_arg1=\xe2\x80\xa6),或强制它们成为仅关键字参数 ( Child(*, arg1, opt_arg1=\xe2\x80\xa6, arg2))。attrs没有现成的功能,但您可以添加它。

\n\n

所以,它并不像您希望的那么微不足道,但它是可行的。

\n\n
\n\n

但如果您想自己编写这个函数,您将如何__init__动态创建该函数?

\n\n

最简单的选择是exec

\n\n

您可能听说过这exec很危险。但只有当您传递来自用户的值时,这才是危险的。在这里,您仅传递来自您自己的源代码的值。

\n\n

它仍然很难看\xe2\x80\x94,但有时它无论如何都是最好的答案。标准库namedtuple曾经是一个巨大的exec模板。甚至当前版本也使用了exec大多数方法,也是如此dataclasses

\n\n

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

\n\n

这是一个非常简单的实现,它不处理attrsor的大部分功能dataclasses,但会将所有强制参数排序在所有可选参数之前。

\n\n
def makeinit(cls):\n    fields = ()\n    optfields = {}\n    for base in cls.mro():\n        fields = getattr(base, \'_fields\', ()) + fields\n        optfields = {**getattr(base, \'_optfields\', {}), **optfields}\n    optparams = [f"{name} = {val!r}" for name, val in optfields.items()]\n    paramstr = \', \'.join([\'self\', *fields, *optparams])\n    assignstr = "\\n    ".join(f"self.{name} = {name}" for name in [*fields, *optfields])\n    exec(f\'def __init__({paramstr}):\\n    {assignstr}\\ncls.__init__ = __init__\')\n    return cls\n\n@makeinit\nclass Parent:\n    _fields = (\'arg1\',)\n    _optfields = {\'opt_arg1\': \'opt_arg1_default_val\',\n                  \'opt_arg2\': \'opt_arg2_default_val\',\n                  \'opt_arg3\': \'opt_arg3_default_val\',\n                  \'opt_arg4\': \'opt_arg4_default_val\'}\n\n@makeinit\nclass Child(Parent):\n    _fields = (\'arg2\',)\n
Run Code Online (Sandbox Code Playgroud)\n\n

现在,您已经获得了__init__您想要的方法ParentChild,完全可检查2(包括help),并且无需重复。

\n\n
\n\n

1. 我不使用 PyCharm,但我知道早在 3.7 发布之前,他们的开发人员就参与了讨论,@dataclass并且已经在致力于在他们的 IDE 中添加对它的明确支持,所以它甚至没有评估类定义以获得所有信息。我不知道它是否在当前版本中可用,但如果没有,我想它会的。同时,@dataclassIPython 自动完成、emacs Flycheck 等已经对我有用了,这对我来说已经足够好了。:)

\n\n

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

\n