Python __init__方法中的DRY原则

问题描述 投票:-1回答:2

在这个类定义中,每个参数都出现三次,这似乎违反了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']

对于他们每个人。此外,我不能使用mutables作为关键字参数的默认值。

python dry
2个回答
2
投票

像DRY这样的原则很重要,但是在盲目应用之前记住这个原则的基本原理很重要 - 可以说DRY代码的最大优点是你只需要在一个地方修改它就可以提高代码的可维护性。并且不必冒着在一个地方而不是另一个地方修改的代码可能发生的微妙错误的风险。 DRY可能与YAGNI和KISS等其他常见原则相对立,为您的应用选择正确的平衡非常重要。

特别是,DRY通常适用于默认值,应用程序逻辑和其他可能导致错误的事情,如果在一个地方而不是另一个地方更改。 IMO变量名称不能以相同的方式拟合,因为重构代码以更改Fooa实例变量的每次出现都不会通过不更改初始化程序中的名称来实际破坏任何内容。

考虑到这一点,我们对您的代码进行了简单的测试。这些变量是否可能一起变化,或者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

另外改变:然后就是单独留下,或者像他们说的那样亲吻。


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):
© www.soinside.com 2019 - 2024. All rights reserved.