在继承时避免长的构造函数而不隐藏构造函数,可选参数或功能 [英] Avoiding long constructors while inheriting without hiding constructor, optional arguments or functionality

查看:108
本文介绍了在继承时避免长的构造函数而不隐藏构造函数,可选参数或功能的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个特殊的问题,但我将使示例更一般。
我有一个 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 $ b 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是一个好习惯,因为必须深入研究代码,直到Parent类,以检查其具有哪些可选参数。 / p>

我也研究过使用Builder模式,但是我要做的就是将参数列表从我的类移动到Builder类,而我也遇到了同样的问题,builders具有很多参数,这些参数在继承时会在现有参数的基础上创建更多参数。同样在Python中,就我所知,考虑到所有类成员都是公开的,并且不需要设置者和获取者就可以访问,Builder并没有多大意义。



任何

解决方案

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



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



并且,当您使用它时,为什么不构建一个显示相同值的 __ repr __ 呢?甚至可能是 __ eq __ __ hash __ ,以及字典比较运算符,以及与<$之间的转换c $ c> dict ,其键与每个JSON持久性的属性匹配,并且…



或者,甚至更好的是,使用照顾到问题的库



Python 3.7带有这样的库 数据类 。或者您可以使用第三方库,例如 attrs ,可在Python 3.4和2.7(有一些限制)下使用。或者,对于简单的情况(对象是不可变的,并且您希望它们按照指定的顺序像其属性的元组一样工作),可以使用 namedtuple ,该版本可恢复到3.0和2.6。



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

 从数据类导入数据类

@dataclass
类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



数据类没有任何方法可以对参数( Child(arg1,arg2,opt_arg1 =…)进行重新排序,也不能强制它们成为仅关键字的参数( Child(*,arg1,opt_arg1 = ...,arg2))。 attrs 并没有该功能,但您可以添加它。



因此,它并不像您希望的那么琐碎,但是我






但是如果您想自己编写此代码,您将如何创建 __ init__ 是否动态运行?



最简单的选项是 exec



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



这仍然很难看,但有时还是最好的答案。 标准库的 namedtuple 曾经是一个巨大的 exec 模板。即使当前版本的大多数方法都使用 exec ,并且 dataclasses



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



这是一个死的简单实现,无法处理 attrs dataclasses 的大多数功能,

  def makeinit(cls):
字段=()
optfields = {}
用于cls.mro()中的base:
字段= getattr(base,'_fields',())+字段
optfields = {** getattr(base ,'_optfields',{}),** optfields
optparams = [f {name} = {val!r}作为名称,val在optfields.items()中
paramstr =' ,'.join(['self',* fields,* optparams])
Assignstr = \n .join(f self。{name} = {name}表示[* fields, * optfields])
exec(f'def __init __({paramstr}):\n {assignstr} \ncls .__ init__ = __init__')
return cls

@ makeinit
类的父级:
_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',)

现在,您已经在<$ c $上获得了想要的 __ init __ 方法c>父母孩子,完全可检查的 2 (包括帮助),而不必重复自己。






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



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


I have a particular problem, but I will make the example more general. I have a Parent class with a mandatory constructor parameter and a few optional ones, each with a default value. Then, I inherit Child from it and add a mandatory parameter, and inherit GrandChild from Child and add another mandatory parameter to the constructor. The result is similar to this:

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

The problem is that this looks rather ugly, especially if I want to inherit more classes from Child, I'd have to copy/paste all the arguments in that new class's constructor.

In search for a solution, I found here that I can solve this problem using **kwargs like so:

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

However, I am not sure if this is the right way.

There is also a slight inconvenience while creating objects of these classes. I am using PyCharm to develop, and in this case the IDE has a useful method of displaying a function/class constructor arguments. For instance, in the first example,

This makes it much easier to develop and can help future developers as well since they can see what other arguments the function has. However, in the second example, the optional arguments are not shown anymore:

And I do not think it is a good practice to use **kwargs in this case, since one would have to dig deeper into the code up to the Parent class to check what optional arguments it has.

I've also looked into using the Builder pattern, but then all I do is move the arguments list from my classes to builder classes, and I have the same problem, builders with lots of arguments that when inherited will create even more arguments on top of the already existing ones. Also in Python, as much as I see, Builder doesn't really make much sense considering all class members are public and can be accessed without needing setters and getters.

Any ideas on how to solve this constructor problem?

解决方案

The basic idea is to write code that generates the __init__ method for you, with all the parameters specified explicitly rather than via *args and/or **kwargs, and without even needing to repeat yourself with all those self.arg1 = arg1 lines.

And, ideally, it can make it easy to add type annotations that PyCharm can use for popup hints and/or static type checking.1

And, while you're at it, why not build a __repr__ that displays the same values? And maybe even an __eq__, and a __hash__, and maybe lexicographical comparison operators, and conversion to and from a dict whose keys match the attributes for each JSON persistence, and…

Or, even better, use a library that takes care of that for you.

Python 3.7 comes with such a library, dataclasses. Or you can use a third-party library like attrs, that works with Python 3.4 and (with some limitations) 2.7. Or, for simple cases (where your objects are immutable, and you want them to work like a tuple of their attributes in specified order), you can use namedtuple, which works back to 3.0 and 2.6.

Unfortunately, dataclasses doesn't quite work for your use case. If you just write this:

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

… you'll get an error, because it tries to place the mandatory parameter arg2 after the default-values parameters opt_arg1 through opt_arg4.

dataclasses doesn't have any way to reorder parameters (Child(arg1, arg2, opt_arg1=…), or to force them to be keyword-only parameters (Child(*, arg1, opt_arg1=…, arg2)). attrs doesn't have that functionality out of the box, but you can add it.

So, it's not quite as trivial as you'd hope, but it's doable.


But if you wanted to write this yourself, how would you create the __init__ function dynamically?

The simplest option is exec.

You've probably heard that exec is dangerous. But it's only dangerous if you're passing in values that came from your user. Here, you're only passing in values that came from your own source code.

It's still ugly—but sometimes it's the best answer anyway. The standard library's namedtuple used to be one giant exec template., and even the current version uses exec for most of the methods, and so does dataclasses.

Also, notice that all of these modules store the set of fields somewhere in a private class attribute, so subclasses can easily read the parent class's fields. If you didn't do that, you could use the inspect module to get the Signature for your base class's (or base classes', for multiple inheritance) initializer and work it out from there. But just using base._fields is obviously a lot simpler (and allows storing extra metadata that doesn't normally go in signatures).

Here's a dead simple implementation that doesn't handle most of the features of attrs or dataclasses, but does order all mandatory parameters before all optionals.

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',)

Now, you've got exactly the __init__ methods you wanted on Parent and Child, fully inspectable2 (including help), and without having to repeat yourself.


1. I don't use PyCharm, but I know that well before 3.7 came out, their devs were involved in the discussion of @dataclass and were already working on adding explicit support for it to their IDE, so it doesn't even have to evaluate the class definition to get all that information. I don't know if it's available in the current version, but if not, I assume it will be. Meanwhile, @dataclass already just works for me with IPython auto-completion, emacs flycheck, and so on, which is good enough for me. :)

2. … at least at runtime. PyCharm may not be able to figure things out statically well enough to do popup completion.

这篇关于在继承时避免长的构造函数而不隐藏构造函数,可选参数或功能的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆