Python __init__方法中的DRY原理

时间:2019-04-03 14:31:49

标签: python dry

在该类定义中,每个参数出现3次,这似乎违反了DRY(请勿重复)的原则:

class Foo:
    def __init__(self, a=1, b=2.0, c=(3, 4, 5)):
        self.a = int(a)
        self.b = float(b)
        self.c = list(c)

DRY可以这样应用(Python 3):

class Foo:
    def __init__(self, **kwargs):
        defaults = dict(a=1, b=2.0, c=[3, 4, 5])
        for k, v in defaults.items():
            setattr(self, k, type(v)(kwargs[k]) if k in kwargs else v)
        # ...detect illegal keywords here...

但是,这会破坏IDE的自动完成功能(尝试过Spyder和Elpy),如果以后再尝试访问属性,pylint将会抱怨。

有没有一种干净的方法来处理这个问题?

编辑:该示例具有三个参数,但是当有15个参数时,我发现自己需要处理这些参数,而在这些参数中,我几乎不需要覆盖默认值;通常需要比较复杂的类型

if not isinstance(kwargs['x'], SomeClass):
    raise TypeError('x: must be SomeClass')
self.x = kwargs['x']

每个。而且,我不能使用可变变量作为关键字参数的默认值。

2 个答案:

答案 0 :(得分:2)

作为序言,您的代码

class Foo:
    def __init__(self, a=1, b=2.0, c=(3, 4, 5)):
        self.a = int(a)
        self.b = float(b)
        self.c = list(c)
如若干评论中所述,

仍然可以。代码的读取远胜于编写的代码,除了在首次定义代码时要小心避免名称中出现错别字外,其意图也很清楚。 (尽管请参见答案的结尾,有关c的默认值。)


如果您使用的是Python 3.7,则可以使用数据类来减少对每个变量的引用数量。

from dataclasses import dataclass, field
from typing import List

@dataclass
class Foo:
    a: int = 1
    b: float = 2.0
    c: List[int] = field(default_factory=lambda: [3,4,5])

这不会阻止您违反类型提示(Foo("1")会愉快地设置a = "1"而不是a = 1或引发错误),但这通常是调用方的责任提供正确类型的参数。)如果您真的想在运行时强制执行此操作,则可以添加__post_init__方法:

def __post_init__(self):
    self.a = int(self.a)
    self.b = float(self.b)
    self.c = list(self.c)

但是,如果这样做,则最好返回原始的手工编码__init__方法。


顺便说一句,可变默认参数的标准习惯用法是

def __init__(self, a=1, b=2.0, c=None):
    ...
    if c is None:
        c = [3, 4, 5]

您的方法有两个问题:

  1. 它要求每个实例都运行list,而不是让编译器硬编码[3,4,5]
  2. 如果要对__init__的参数进行类型提示,则默认值与预期类型不匹配。您必须编写类似

    的内容

    def init (a:int = 1,b:float = 2.0,c:Union [List [Int],Tuple [Int,Int,Int]] =(3,4,5 ))

默认值None自动导致类型的“提升”为相应的可选类型。以下是等效的:

def __init__(a: int = 1, b: float = 2.0, c : List[Int] = None):
def __init__(a: int = 1, b: float = 2.0, c : Optional[List[Int]] = None):

答案 1 :(得分:2)

像DRY这样的原理很重要,但是重要的是在盲目应用它之前要牢记这种原理的原理-可以说DRY代码的最大优点是您只需修改即可增加代码的可维护性在一个地方,而不必冒险在一个地方而不是另一个地方修改的代码可能引起的细微错误。 DRY可能与YAGNI和KISS等其他常见原理相反,因此为您的应用选择正确的平衡很重要。

尤其是,DRY通常适用于默认值,应用程序逻辑,以及如果在一个位置而不是另一个位置更改可能会导致错误的其他事物。 IMO变量名称的使用方式不同,因为重构代码以更改Foo的实例变量a的每次出现都不会通过不更改初始化程序中的名称而实际上破坏任何内容,例如好吧。

考虑到这一点,我们对您的代码进行了简单的测试。这些变量是否可能会一起更改,或者Foo的初始化程序是一个抽象层,可以独立于类的实例变量来重构输入?

一起改变::我更喜欢@chepner的回答,我会更进一步。如果您的类不只是数据传输对象,则可以使用@chepner的解决方案作为对相关数据进行逻辑分组的方法(诚然,在您的情况下这可能是不必要的,并且在没有上下文的情况下,很难选择一种最佳的方法来引入数据这样的想法),例如

from dataclasses import dataclass, field

@dataclass
class MyData:
    a: int
    b: float
    c: list

class Foo:
    def __init__(self, my_data):
        self.wrapped = my_data

单独更改:然后就不管它了,还是如他们所说的那样接吻。